Repository: thomasdarimont/keycloak-project-example Branch: main Commit: 188ff5af3716 Files: 914 Total size: 3.2 MB Directory structure: gitextract_j1wzyoop/ ├── .github/ │ └── workflows/ │ ├── build.yml │ └── e2e-tests.yml ├── .gitignore ├── .run/ │ ├── Acme Backend API Quarkus.run.xml │ ├── Keycloak Remote.run.xml │ ├── OfflineSessionClient (logout).run.xml │ ├── OfflineSessionClient.run.xml │ ├── acme-webapp-saml-node-express.run.xml │ ├── backend-api-micronaut.run.xml │ ├── backend-api-springboot-reactive.run.xml │ ├── backend-api-springboot.run.xml │ ├── backend-api-springboot3.run.xml │ ├── frontend-webapp-springboot-otel.run.xml │ ├── frontend-webapp-springboot.run.xml │ └── frontend-webapp-springboot3.run.xml ├── .vscode/ │ └── settings.json ├── LICENSE ├── apps/ │ ├── account-svc/ │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── .gitignore │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── README.md │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── docker/ │ │ │ ├── Dockerfile.jvm │ │ │ ├── Dockerfile.legacy-jar │ │ │ ├── Dockerfile.native │ │ │ └── Dockerfile.native-micro │ │ ├── java/ │ │ │ └── com/ │ │ │ └── thomasdarimont/ │ │ │ └── keycloak/ │ │ │ └── training/ │ │ │ └── accounts/ │ │ │ ├── User.java │ │ │ ├── UserRepository.java │ │ │ └── UserResource.java │ │ └── resources/ │ │ └── application.properties │ ├── acme-account-console/ │ │ └── index.html │ ├── acme-greetme/ │ │ └── index.html │ ├── acme-webapp-saml-node-express/ │ │ ├── package.json │ │ ├── src/ │ │ │ ├── config.js │ │ │ ├── express.js │ │ │ ├── index.js │ │ │ ├── logging.js │ │ │ └── server.js │ │ └── views/ │ │ ├── pages/ │ │ │ ├── app.ejs │ │ │ ├── error.ejs │ │ │ ├── index.ejs │ │ │ └── page1.ejs │ │ └── partials/ │ │ ├── footer.ejs │ │ ├── head.ejs │ │ └── header.ejs │ ├── backend-api-dnc/ │ │ ├── api/ │ │ │ ├── .dockerignore │ │ │ ├── .gitignore │ │ │ ├── Controllers/ │ │ │ │ └── UsersController.cs │ │ │ ├── Dockerfile │ │ │ ├── JwtBearerOptions.cs │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── api.csproj │ │ │ ├── appsettings.Development.json │ │ │ └── appsettings.json │ │ └── backend-api-dnc.sln │ ├── backend-api-micronaut/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── README.md │ │ ├── micronaut-cli.yml │ │ ├── mvnw │ │ ├── mvnw.bat │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acme/ │ │ │ └── backend/ │ │ │ └── micronaut/ │ │ │ ├── Application.java │ │ │ └── api/ │ │ │ └── UsersResource.java │ │ └── resources/ │ │ ├── application.yml │ │ └── logback.xml │ ├── backend-api-node-express/ │ │ ├── package.json │ │ ├── readme.md │ │ └── src/ │ │ ├── api.js │ │ ├── config.js │ │ ├── express.js │ │ ├── index.js │ │ ├── logging.js │ │ └── server.js │ ├── backend-api-quarkus/ │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── docker/ │ │ │ ├── Dockerfile.jvm │ │ │ ├── Dockerfile.legacy-jar │ │ │ ├── Dockerfile.native │ │ │ └── Dockerfile.native-distroless │ │ ├── java/ │ │ │ └── com/ │ │ │ └── acme/ │ │ │ └── backend/ │ │ │ └── quarkus/ │ │ │ └── users/ │ │ │ └── UsersResource.java │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── resources/ │ │ │ └── index.html │ │ └── application.properties │ ├── backend-api-rust-actix/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── rustfmt.toml │ │ ├── rustup-toolchain.toml │ │ └── src/ │ │ ├── api/ │ │ │ ├── me_info.rs │ │ │ └── mod.rs │ │ ├── config.rs │ │ ├── main.rs │ │ └── middleware/ │ │ ├── cors.rs │ │ ├── jwt_auth.rs │ │ ├── mod.rs │ │ └── ssl.rs │ ├── backend-api-rust-rocket/ │ │ ├── .gitignore │ │ ├── .run/ │ │ │ └── Run backend-api-rust-rocket.run.xml │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Rocket.toml │ │ ├── rustfmt.toml │ │ ├── rustup-toolchain.toml │ │ ├── src/ │ │ │ ├── domain/ │ │ │ │ ├── mod.rs │ │ │ │ └── user.rs │ │ │ ├── main.rs │ │ │ ├── middleware/ │ │ │ │ ├── auth/ │ │ │ │ │ ├── jwt/ │ │ │ │ │ │ ├── auth.rs │ │ │ │ │ │ ├── claims.rs │ │ │ │ │ │ ├── config.rs │ │ │ │ │ │ ├── get_max_age.rs │ │ │ │ │ │ ├── jwks.rs │ │ │ │ │ │ ├── mod.rs │ │ │ │ │ │ └── verifier.rs │ │ │ │ │ ├── jwt_auth_request_guard.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── cors/ │ │ │ │ │ ├── cors.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── logging/ │ │ │ │ │ ├── logging.rs │ │ │ │ │ └── mod.rs │ │ │ │ └── mod.rs │ │ │ └── support/ │ │ │ ├── mod.rs │ │ │ └── scheduling/ │ │ │ ├── mod.rs │ │ │ └── use_repeating_job.rs │ │ └── tests/ │ │ └── fetch_keys.rs │ ├── backend-api-springboot/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── acme/ │ │ │ │ └── backend/ │ │ │ │ └── springboot/ │ │ │ │ └── users/ │ │ │ │ ├── BackendApiSpringbootApp.java │ │ │ │ ├── config/ │ │ │ │ │ ├── AcmeServiceProperties.java │ │ │ │ │ ├── JwtSecurityConfig.java │ │ │ │ │ ├── MethodSecurityConfig.java │ │ │ │ │ └── WebSecurityConfig.java │ │ │ │ ├── support/ │ │ │ │ │ ├── access/ │ │ │ │ │ │ └── AccessController.java │ │ │ │ │ ├── keycloak/ │ │ │ │ │ │ ├── KeycloakAudienceValidator.java │ │ │ │ │ │ ├── KeycloakGrantedAuthoritiesConverter.java │ │ │ │ │ │ └── KeycloakJwtAuthenticationConverter.java │ │ │ │ │ └── permissions/ │ │ │ │ │ ├── DefaultPermissionEvaluator.java │ │ │ │ │ └── DomainObjectReference.java │ │ │ │ └── web/ │ │ │ │ └── UsersController.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── acme/ │ │ └── backend/ │ │ └── springboot/ │ │ └── users/ │ │ └── BackendApiSpringbootAppTests.java │ ├── backend-api-springboot-reactive/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── acme/ │ │ │ │ └── backend/ │ │ │ │ └── springreactive/ │ │ │ │ ├── BackendApiSpringbootReactiveApp.java │ │ │ │ ├── config/ │ │ │ │ │ ├── AcmeServiceProperties.java │ │ │ │ │ ├── JwtSecurityConfig.java │ │ │ │ │ ├── MethodSecurityConfig.java │ │ │ │ │ ├── WebFluxConfig.java │ │ │ │ │ ├── WebFluxRoutes.java │ │ │ │ │ └── WebSecurityConfig.java │ │ │ │ ├── support/ │ │ │ │ │ └── keycloak/ │ │ │ │ │ ├── KeycloakAudienceValidator.java │ │ │ │ │ ├── KeycloakGrantedAuthoritiesConverter.java │ │ │ │ │ └── KeycloakJwtAuthenticationConverter.java │ │ │ │ └── users/ │ │ │ │ └── UserHandlers.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── acme/ │ │ └── backend/ │ │ └── springreactive/ │ │ └── BackendApiSpringbootReactiveAppTests.java │ ├── backend-api-springboot3/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ ├── MavenWrapperDownloader.java │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── acme/ │ │ │ │ └── backend/ │ │ │ │ └── springboot/ │ │ │ │ └── users/ │ │ │ │ ├── BackendApiSpringboot3App.java │ │ │ │ ├── config/ │ │ │ │ │ ├── AcmeServiceProperties.java │ │ │ │ │ ├── JwtSecurityConfig.java │ │ │ │ │ ├── MethodSecurityConfig.java │ │ │ │ │ └── WebSecurityConfig.java │ │ │ │ ├── support/ │ │ │ │ │ ├── access/ │ │ │ │ │ │ └── AccessController.java │ │ │ │ │ ├── keycloak/ │ │ │ │ │ │ ├── KeycloakAudienceValidator.java │ │ │ │ │ │ ├── KeycloakGrantedAuthoritiesConverter.java │ │ │ │ │ │ └── KeycloakJwtAuthenticationConverter.java │ │ │ │ │ └── permissions/ │ │ │ │ │ ├── DefaultPermissionEvaluator.java │ │ │ │ │ └── DomainObjectReference.java │ │ │ │ └── web/ │ │ │ │ └── UsersController.java │ │ │ └── resources/ │ │ │ └── application.yml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── acme/ │ │ └── backend/ │ │ └── springboot/ │ │ └── users/ │ │ └── BackendApiSpringboot3AppTests.java │ ├── bff-springboot/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── github/ │ │ │ └── thomasdarimont/ │ │ │ └── apps/ │ │ │ └── bff/ │ │ │ ├── BffApp.java │ │ │ ├── api/ │ │ │ │ └── UsersResource.java │ │ │ ├── config/ │ │ │ │ ├── OAuth2RestTemplateConfig.java │ │ │ │ ├── SessionConfig.java │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ └── keycloak/ │ │ │ │ └── KeycloakLogoutHandler.java │ │ │ ├── oauth/ │ │ │ │ ├── TokenAccessor.java │ │ │ │ ├── TokenIntrospector.java │ │ │ │ └── TokenRefresher.java │ │ │ └── web/ │ │ │ └── UiResource.java │ │ └── resources/ │ │ ├── application.yml │ │ ├── static/ │ │ │ └── app/ │ │ │ └── app.js │ │ └── templates/ │ │ └── app/ │ │ └── index.html │ ├── bff-springboot3/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── github/ │ │ │ └── thomasdarimont/ │ │ │ └── apps/ │ │ │ └── bff3/ │ │ │ ├── Bff3App.java │ │ │ ├── api/ │ │ │ │ └── UsersResource.java │ │ │ ├── config/ │ │ │ │ ├── OAuth2RestTemplateConfig.java │ │ │ │ ├── SessionConfig.java │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ └── keycloak/ │ │ │ │ └── KeycloakLogoutHandler.java │ │ │ ├── oauth/ │ │ │ │ ├── TokenAccessor.java │ │ │ │ ├── TokenIntrospector.java │ │ │ │ └── TokenRefresher.java │ │ │ ├── support/ │ │ │ │ ├── HttpServletRequestUtils.java │ │ │ │ └── HttpSessionOAuth2AuthorizedClientService.java │ │ │ └── web/ │ │ │ ├── AuthResource.java │ │ │ └── UiResource.java │ │ └── resources/ │ │ ├── application.yml │ │ ├── static/ │ │ │ └── app/ │ │ │ └── app.js │ │ └── templates/ │ │ └── app/ │ │ └── index.html │ ├── frontend-webapp-springboot/ │ │ ├── .gitignore │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── github/ │ │ │ │ └── thomasdarimont/ │ │ │ │ └── keycloak/ │ │ │ │ └── webapp/ │ │ │ │ ├── WebAppSpringBoot.java │ │ │ │ ├── config/ │ │ │ │ │ ├── KeycloakWebClientConfig.java │ │ │ │ │ ├── OidcUserServiceConfig.java │ │ │ │ │ └── WebSecurityConfig.java │ │ │ │ ├── domain/ │ │ │ │ │ ├── ApplicationEntry.java │ │ │ │ │ ├── CredentialEntry.java │ │ │ │ │ ├── SettingEntry.java │ │ │ │ │ └── UserProfile.java │ │ │ │ ├── support/ │ │ │ │ │ ├── OAuth2AuthorizedClientAccessor.java │ │ │ │ │ ├── TokenAccessor.java │ │ │ │ │ ├── TokenIntrospector.java │ │ │ │ │ ├── keycloakclient/ │ │ │ │ │ │ ├── DefaultKeycloakClient.java │ │ │ │ │ │ ├── KeycloakClient.java │ │ │ │ │ │ ├── KeycloakIntrospectResponse.java │ │ │ │ │ │ ├── KeycloakServiceException.java │ │ │ │ │ │ └── KeycloakUserInfo.java │ │ │ │ │ └── security/ │ │ │ │ │ └── KeycloakLogoutHandler.java │ │ │ │ └── web/ │ │ │ │ ├── AuthController.java │ │ │ │ └── UiController.java │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ └── templates/ │ │ │ ├── applications.html │ │ │ ├── fragments.html │ │ │ ├── index.html │ │ │ ├── profile.html │ │ │ ├── security.html │ │ │ └── settings.html │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── github/ │ │ └── thomasdarimont/ │ │ └── keycloak/ │ │ └── cac/ │ │ └── WebApplicationTestsSpringBoot.java │ ├── frontend-webapp-springboot3/ │ │ ├── otel-config.yaml │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── github/ │ │ │ │ └── thomasdarimont/ │ │ │ │ └── keycloak/ │ │ │ │ └── webapp/ │ │ │ │ ├── WebAppSpringBoot3.java │ │ │ │ ├── config/ │ │ │ │ │ ├── KeycloakWebClientConfig.java │ │ │ │ │ ├── OidcUserServiceConfig.java │ │ │ │ │ └── WebSecurityConfig.java │ │ │ │ ├── domain/ │ │ │ │ │ ├── ApplicationEntry.java │ │ │ │ │ ├── CredentialEntry.java │ │ │ │ │ ├── SettingEntry.java │ │ │ │ │ └── UserProfile.java │ │ │ │ ├── support/ │ │ │ │ │ ├── HttpServletRequestUtils.java │ │ │ │ │ ├── HttpSessionOAuth2AuthorizedClientService.java │ │ │ │ │ ├── TokenAccessor.java │ │ │ │ │ ├── TokenIntrospector.java │ │ │ │ │ ├── keycloakclient/ │ │ │ │ │ │ ├── DefaultKeycloakClient.java │ │ │ │ │ │ ├── KeycloakClient.java │ │ │ │ │ │ ├── KeycloakIntrospectResponse.java │ │ │ │ │ │ ├── KeycloakServiceException.java │ │ │ │ │ │ └── KeycloakUserInfo.java │ │ │ │ │ └── security/ │ │ │ │ │ └── KeycloakLogoutHandler.java │ │ │ │ └── web/ │ │ │ │ ├── AuthController.java │ │ │ │ └── UiController.java │ │ │ └── resources/ │ │ │ ├── application.yml │ │ │ └── templates/ │ │ │ ├── applications.html │ │ │ ├── fragments.html │ │ │ ├── index.html │ │ │ ├── profile.html │ │ │ ├── security.html │ │ │ └── settings.html │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── github/ │ │ └── thomasdarimont/ │ │ └── keycloak/ │ │ └── cac/ │ │ └── WebApplicationTestsSpringBoot.java │ ├── java-opa-embedded/ │ │ ├── .gitignore │ │ ├── jd-gui.cfg │ │ ├── pom.xml │ │ ├── readme.md │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── demo/ │ │ │ └── OpaEmbeddedDemo.java │ │ └── resources/ │ │ ├── data/ │ │ │ └── user_roles.json │ │ └── policy/ │ │ ├── app/ │ │ │ └── rbac/ │ │ │ └── policy.rego │ │ └── policy.wasm │ ├── jwt-client-authentication/ │ │ ├── .gitignore │ │ ├── .mvn/ │ │ │ └── wrapper/ │ │ │ └── maven-wrapper.properties │ │ ├── mvnw │ │ ├── mvnw.cmd │ │ ├── pom.xml │ │ ├── readme.md │ │ └── src/ │ │ └── main/ │ │ ├── java/ │ │ │ └── demo/ │ │ │ └── jwtclientauth/ │ │ │ └── JwtClientAuthApp.java │ │ └── resources/ │ │ └── application.properties │ ├── keycloak-js/ │ │ ├── package.json │ │ └── readme.md │ ├── oauth2-proxy/ │ │ ├── Dockerfile │ │ ├── app/ │ │ │ └── main.go │ │ ├── docker-compose.yml │ │ ├── oauth2-proxy.cfg │ │ └── readme.md │ ├── offline-session-client/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── demo/ │ │ └── OfflineSessionClient.java │ ├── site/ │ │ ├── accountdeleted.html │ │ ├── imprint.html │ │ ├── lib/ │ │ │ └── keycloak-js/ │ │ │ ├── keycloak-authz.js │ │ │ └── keycloak.js │ │ ├── privacy.html │ │ ├── site.html │ │ └── terms.html │ └── spring-boot-device-flow-client/ │ ├── .gitignore │ ├── .mvn/ │ │ └── wrapper/ │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src/ │ └── main/ │ ├── java/ │ │ └── demo/ │ │ └── SpringBootDeviceFlowApplication.java │ └── resources/ │ └── application.properties ├── bin/ │ ├── applyRealmConfig.java │ ├── createTlsCerts.java │ ├── envcheck.java │ ├── importCertificateIntoTruststore.java │ ├── installOtel.java │ ├── keycloakConfigCli.default.env │ ├── keycloakConfigCli.java │ └── realmImex.java ├── config/ │ └── stage/ │ └── dev/ │ ├── grafana/ │ │ └── provisioning/ │ │ ├── dashboards/ │ │ │ ├── dashboard.yml │ │ │ ├── keycloak-capacity-planning-dashboard.json │ │ │ ├── keycloak-metrics_rev1.json │ │ │ └── keycloak-troubleshooting-dashboard.json │ │ └── datasources/ │ │ └── datasources.yml │ ├── opa/ │ │ ├── iam/ │ │ │ ├── authzen/ │ │ │ │ └── interop/ │ │ │ │ ├── access/ │ │ │ │ │ ├── policy.rego │ │ │ │ │ └── v1/ │ │ │ │ │ └── search/ │ │ │ │ │ └── policy.rego │ │ │ │ └── data.json │ │ │ └── keycloak/ │ │ │ └── policy.rego │ │ ├── inputs/ │ │ │ └── input.json │ │ ├── opa.md │ │ ├── policies/ │ │ │ └── keycloak/ │ │ │ ├── realms/ │ │ │ │ ├── opademo/ │ │ │ │ │ └── access/ │ │ │ │ │ ├── policy.rego │ │ │ │ │ └── policy_test.rego │ │ │ │ └── opademo2/ │ │ │ │ └── access/ │ │ │ │ └── policy.rego │ │ │ └── utils/ │ │ │ └── kc/ │ │ │ └── helpers.rego │ │ └── watch-opa.sh │ ├── openldap/ │ │ └── demo.ldif │ ├── otel/ │ │ ├── otel-collector-config-tls.yaml │ │ └── otel-collector-config.yaml │ ├── prometheus/ │ │ └── prometheus.yml │ ├── realms/ │ │ ├── acme-api.yaml │ │ ├── acme-apps.yaml │ │ ├── acme-auth0.yaml │ │ ├── acme-client-examples.yaml │ │ ├── acme-demo.yaml │ │ ├── acme-internal.yaml │ │ ├── acme-ldap.yaml │ │ ├── acme-offline-test.yaml │ │ ├── acme-ops.yaml │ │ ├── acme-passwordless.yaml │ │ ├── acme-saml.yaml │ │ ├── acme-stepup.yaml │ │ ├── company-apps.yaml │ │ ├── company-users.yaml │ │ ├── master.yaml │ │ ├── other/ │ │ │ ├── acme-internal-custom.yaml │ │ │ ├── acme-saml.yaml │ │ │ ├── acme-user-profile.yaml │ │ │ ├── acme-vci.yaml │ │ │ ├── acme-workshop-clients.yaml │ │ │ ├── acme-workshop-idp.yaml │ │ │ └── acme-workshop.yaml │ │ └── workshop.yaml │ └── tls/ │ └── .gitkeep ├── deployments/ │ └── local/ │ ├── cluster/ │ │ ├── apache/ │ │ │ ├── docker-compose-apache.yml │ │ │ └── id.acme.test.conf │ │ ├── caddy/ │ │ │ ├── caddy.json │ │ │ └── docker-compose-caddy.yml │ │ ├── cli/ │ │ │ ├── 0001-onstart-init.cli │ │ │ ├── 0010-add-jmx-user.sh │ │ │ ├── 0100-onstart-setup-remote-caches.cli │ │ │ ├── 0200-onstart-setup-jgroups-encryption.cli │ │ │ └── 0300-onstart-setup-ispn-jdbc-store.cli │ │ ├── docker-compose.yml │ │ ├── haproxy/ │ │ │ ├── Dockerfile │ │ │ ├── docker-compose-haproxy.yml │ │ │ └── haproxy.cfg │ │ ├── haproxy-database-ispn/ │ │ │ ├── cli/ │ │ │ │ ├── 0010-add-jmx-user.sh │ │ │ │ └── 0300-onstart-setup-ispn-jdbc-store.cli │ │ │ └── docker-compose-haproxy-jdbc-store.yml │ │ ├── haproxy-encrypted-ispn/ │ │ │ ├── cli/ │ │ │ │ └── 0200-onstart-setup-jgroups-encryption.cli │ │ │ ├── docker-compose-enc-haproxy.yml │ │ │ ├── ispn/ │ │ │ │ └── jgroups.p12 │ │ │ └── jgroups-keystore.sh │ │ ├── haproxy-external-ispn/ │ │ │ ├── cli/ │ │ │ │ ├── 0100-onstart-setup-hotrod-caches.cli │ │ │ │ └── 0100-onstart-setup-remote-caches.cli │ │ │ ├── docker-compose-haproxy-ispn-hotrod.yml │ │ │ ├── docker-compose-haproxy-ispn-remote.yml │ │ │ ├── haproxy-external-ispn.env │ │ │ ├── ispn/ │ │ │ │ ├── Dockerfile │ │ │ │ ├── cacerts │ │ │ │ ├── conf/ │ │ │ │ │ ├── infinispan-keycloak.xml │ │ │ │ │ └── users.properties │ │ │ │ ├── ispn-server.jks │ │ │ │ └── ispn-truststore.jks │ │ │ └── readme.md │ │ ├── nginx/ │ │ │ ├── docker-compose-nginx.yml │ │ │ └── nginx.conf │ │ └── readme.md │ ├── clusterx/ │ │ ├── docker-compose.yml │ │ ├── haproxy/ │ │ │ ├── Dockerfile │ │ │ ├── acme.test+1.p12 │ │ │ ├── docker-compose-haproxy.yml │ │ │ └── haproxy.cfg │ │ ├── haproxy-database-ispn/ │ │ │ ├── Dockerfile │ │ │ ├── acme.test+1.p12 │ │ │ ├── cache-ispn-database.xml │ │ │ ├── docker-compose.yml │ │ │ ├── haproxy.cfg │ │ │ └── readme.md │ │ ├── haproxy-external-ispn/ │ │ │ ├── cache-ispn-remote.xml │ │ │ ├── docker-compose-haproxy-ispn-remote.yml │ │ │ ├── haproxy-external-ispn.env │ │ │ ├── haproxy.cfg │ │ │ └── ispn/ │ │ │ ├── Dockerfile │ │ │ ├── cacerts │ │ │ ├── conf/ │ │ │ │ ├── infinispan-keycloak.xml │ │ │ │ └── users.properties │ │ │ ├── ispn-server.jks │ │ │ └── ispn-truststore.jks │ │ ├── haproxy-external-ispn-database/ │ │ │ ├── cache-ispn-remote.xml │ │ │ ├── docker-compose-haproxy-ispn-remote-database.yml │ │ │ ├── haproxy-external-ispn.env │ │ │ ├── haproxy.cfg │ │ │ └── ispn/ │ │ │ ├── Dockerfile │ │ │ ├── cacerts │ │ │ ├── conf/ │ │ │ │ ├── infinispan-keycloak-database.xml │ │ │ │ └── users.properties │ │ │ ├── ispn-server.jks │ │ │ └── ispn-truststore.jks │ │ ├── keycloakx/ │ │ │ ├── Dockerfile │ │ │ ├── cache-custom-jgroups-tcp.xml │ │ │ ├── cache-custom-jgroups.xml │ │ │ ├── cache-custom.xml │ │ │ ├── jgroups-jdbcping-enc.xml │ │ │ ├── jgroups-multicast-diag.xml │ │ │ ├── jgroups-multicast-enc.xml │ │ │ └── jgroups.p12 │ │ ├── nginx/ │ │ │ ├── docker-compose-nginx.yml │ │ │ └── nginx.conf │ │ └── readme.md │ ├── dev/ │ │ ├── docker-compose-ci-github.yml │ │ ├── docker-compose-grafana.yml │ │ ├── docker-compose-graylog.yml │ │ ├── docker-compose-keycloak.yml │ │ ├── docker-compose-keycloakx.yml │ │ ├── docker-compose-mssql.yml │ │ ├── docker-compose-mysql.yml │ │ ├── docker-compose-nats.yml │ │ ├── docker-compose-opa.yml │ │ ├── docker-compose-openldap.yml │ │ ├── docker-compose-oracle.yml │ │ ├── docker-compose-postgres.yml │ │ ├── docker-compose-prometheus.yml │ │ ├── docker-compose-provisioning.yml │ │ ├── docker-compose-simplesaml.yml │ │ ├── docker-compose-tls.yml │ │ ├── docker-compose-tracing-tls.yml │ │ ├── docker-compose-tracing.yml │ │ ├── docker-compose.yml │ │ ├── graylog/ │ │ │ ├── Dockerfile │ │ │ ├── cli/ │ │ │ │ └── 0020-onstart-setup-graylog-logging.cli │ │ │ ├── contentpacks/ │ │ │ │ └── iam-keycloak-content-pack-v1.json │ │ │ └── modules/ │ │ │ └── logstash-gelf-1.14.1/ │ │ │ └── biz/ │ │ │ └── paluch/ │ │ │ └── logging/ │ │ │ └── main/ │ │ │ └── module.xml │ │ ├── keycloak/ │ │ │ └── Dockerfile │ │ ├── keycloak-common.env │ │ ├── keycloak-db.env │ │ ├── keycloak-ext/ │ │ │ └── readme.md │ │ ├── keycloak-http.env │ │ ├── keycloak-openldap.env │ │ ├── keycloak-provisioning.env │ │ ├── keycloak-tls.env │ │ ├── keycloak-tracing.env │ │ ├── keycloakx/ │ │ │ ├── Dockerfile │ │ │ ├── Dockerfile-ci │ │ │ └── health_check.sh │ │ ├── mysql/ │ │ │ └── Dockerfile │ │ ├── nats/ │ │ │ ├── readme.md │ │ │ └── server.conf │ │ ├── oracle/ │ │ │ └── Dockerfile │ │ ├── otel-collector/ │ │ │ └── Dockerfile │ │ ├── postgresql/ │ │ │ └── Dockerfile │ │ ├── simplesaml/ │ │ │ └── idp/ │ │ │ ├── Dockerfile │ │ │ └── authsources.php │ │ └── sqlserver/ │ │ ├── Dockerfile │ │ ├── db-init.sh │ │ ├── db-init.sql │ │ ├── docker-entrypoint.sh │ │ └── mssql.conf │ └── standalone/ │ ├── docker-compose.yml │ ├── keycloak/ │ │ ├── Dockerfile │ │ └── conf/ │ │ ├── keycloak.conf │ │ └── quarkus.properties │ ├── proxy/ │ │ └── nginx.conf │ ├── readme.md │ └── up.sh ├── keycloak/ │ ├── cli/ │ │ ├── 0001-onstart-init.cli │ │ ├── 0010-register-smallrye-extensions.cli │ │ ├── 0020-onstart-setup-graylog-logging.cli │ │ └── 0100-onstart-deploy-extensions.sh │ ├── clisnippets/ │ │ ├── http-client-config.md │ │ ├── json-logging.md │ │ ├── map-keycloak-endpoint-to-custom-endpint.md │ │ ├── offline-sessions-lazy-loading.md │ │ ├── undertow-access.md │ │ └── undertow-request-logging.md │ ├── config/ │ │ ├── jmxremote.password │ │ ├── openid-config.json │ │ └── quarkus.properties │ ├── docker/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── docker/ │ │ ├── keycloak/ │ │ │ ├── Dockerfile.alpine-slim │ │ │ ├── Dockerfile.ci.plain │ │ │ ├── Dockerfile.plain │ │ │ └── custom-docker-entrypoint.sh │ │ └── keycloakx/ │ │ ├── Dockerfile.ci.plain │ │ └── Dockerfile.plain │ ├── e2e-tests/ │ │ ├── .gitignore │ │ ├── cypress/ │ │ │ ├── e2e/ │ │ │ │ └── login/ │ │ │ │ └── login.cy.ts │ │ │ ├── fixtures/ │ │ │ │ ├── messages.json │ │ │ │ └── users.json │ │ │ ├── plugins/ │ │ │ │ └── index.ts │ │ │ ├── support/ │ │ │ │ ├── commands.ts │ │ │ │ └── e2e.ts │ │ │ ├── tsconfig.json │ │ │ └── utils/ │ │ │ └── keycloakUtils.ts │ │ ├── cypress.config.ts │ │ ├── package.json │ │ └── readme.md │ ├── extensions/ │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ └── com/ │ │ │ │ └── github/ │ │ │ │ └── thomasdarimont/ │ │ │ │ └── keycloak/ │ │ │ │ └── custom/ │ │ │ │ ├── account/ │ │ │ │ │ ├── AccountActivity.java │ │ │ │ │ ├── AccountChange.java │ │ │ │ │ ├── AccountDeletion.java │ │ │ │ │ ├── AccountEmail.java │ │ │ │ │ ├── AccountPostLoginAction.java │ │ │ │ │ ├── MfaChange.java │ │ │ │ │ ├── RequestAccountDeletionActionToken.java │ │ │ │ │ ├── RequestAccountDeletionActionTokenHandler.java │ │ │ │ │ └── console/ │ │ │ │ │ └── AcmeAccountConsoleFactory.java │ │ │ │ ├── admin/ │ │ │ │ │ └── ui/ │ │ │ │ │ └── example/ │ │ │ │ │ └── ExampleUiPageProvider.java │ │ │ │ ├── audit/ │ │ │ │ │ └── AcmeAuditListener.java │ │ │ │ ├── auth/ │ │ │ │ │ ├── authzen/ │ │ │ │ │ │ ├── AuthZen.java │ │ │ │ │ │ └── AuthzenClient.java │ │ │ │ │ ├── checkaccess/ │ │ │ │ │ │ └── CheckAccessAuthenticator.java │ │ │ │ │ ├── confirmcookie/ │ │ │ │ │ │ └── ConfirmCookieAuthenticator.java │ │ │ │ │ ├── customcookie/ │ │ │ │ │ │ └── CustomCookieAuthenticator.java │ │ │ │ │ ├── debug/ │ │ │ │ │ │ └── DebugAuthenticator.java │ │ │ │ │ ├── demo/ │ │ │ │ │ │ └── SkippableRequiredAction.java │ │ │ │ │ ├── dynamicidp/ │ │ │ │ │ │ └── DynamicIdpAuthenticator.java │ │ │ │ │ ├── hello/ │ │ │ │ │ │ └── HelloAuthenticator.java │ │ │ │ │ ├── idpselection/ │ │ │ │ │ │ └── AcmeDynamicIdpLookupUsernameForm.java │ │ │ │ │ ├── magiclink/ │ │ │ │ │ │ └── MagicLinkAuthenticator.java │ │ │ │ │ ├── mfa/ │ │ │ │ │ │ ├── MfaInfo.java │ │ │ │ │ │ ├── emailcode/ │ │ │ │ │ │ │ ├── EmailCodeAuthenticatorForm.java │ │ │ │ │ │ │ ├── EmailCodeCredentialModel.java │ │ │ │ │ │ │ ├── EmailCodeCredentialProvider.java │ │ │ │ │ │ │ └── RegisterEmailCodeRequiredAction.java │ │ │ │ │ │ ├── otp/ │ │ │ │ │ │ │ └── AcmeOTPFormAuthenticator.java │ │ │ │ │ │ ├── setup/ │ │ │ │ │ │ │ └── SelectMfaMethodAuthenticator.java │ │ │ │ │ │ └── sms/ │ │ │ │ │ │ ├── PhoneNumberUtils.java │ │ │ │ │ │ ├── SmsAuthenticator.java │ │ │ │ │ │ ├── SmsCodeSender.java │ │ │ │ │ │ ├── client/ │ │ │ │ │ │ │ ├── SmsClient.java │ │ │ │ │ │ │ ├── SmsClientFactory.java │ │ │ │ │ │ │ └── mock/ │ │ │ │ │ │ │ └── MockSmsClient.java │ │ │ │ │ │ ├── credentials/ │ │ │ │ │ │ │ ├── SmsCredentialModel.java │ │ │ │ │ │ │ └── SmsCredentialProvider.java │ │ │ │ │ │ └── updatephone/ │ │ │ │ │ │ └── UpdatePhoneNumberRequiredAction.java │ │ │ │ │ ├── net/ │ │ │ │ │ │ └── NetworkAuthenticator.java │ │ │ │ │ ├── opa/ │ │ │ │ │ │ ├── OpaAccessResponse.java │ │ │ │ │ │ ├── OpaAuthenticator.java │ │ │ │ │ │ ├── OpaCheckAccessAction.java │ │ │ │ │ │ └── OpaClient.java │ │ │ │ │ ├── passwordform/ │ │ │ │ │ │ └── FederationAwarePasswordForm.java │ │ │ │ │ ├── trusteddevice/ │ │ │ │ │ │ ├── TrustedDeviceCookie.java │ │ │ │ │ │ ├── TrustedDeviceName.java │ │ │ │ │ │ ├── TrustedDeviceToken.java │ │ │ │ │ │ ├── action/ │ │ │ │ │ │ │ ├── ManageTrustedDeviceAction.java │ │ │ │ │ │ │ └── TrustedDeviceInfo.java │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ └── TrustedDeviceAuthenticator.java │ │ │ │ │ │ ├── credentials/ │ │ │ │ │ │ │ ├── TrustedDeviceCredentialInput.java │ │ │ │ │ │ │ ├── TrustedDeviceCredentialModel.java │ │ │ │ │ │ │ └── TrustedDeviceCredentialProvider.java │ │ │ │ │ │ └── support/ │ │ │ │ │ │ └── UserAgentParser.java │ │ │ │ │ ├── userpasswordform/ │ │ │ │ │ │ └── AcmeCaptchaUsernamePasswordForm.java │ │ │ │ │ └── verifyemailcode/ │ │ │ │ │ └── VerifyEmailCodeAction.java │ │ │ │ ├── authz/ │ │ │ │ │ ├── filter/ │ │ │ │ │ │ └── AcmeAccessFilter.java │ │ │ │ │ └── policies/ │ │ │ │ │ └── AcmeImpersonationPolicyProvider.java │ │ │ │ ├── config/ │ │ │ │ │ ├── ClientConfig.java │ │ │ │ │ ├── ConfigAccessor.java │ │ │ │ │ ├── MapConfig.java │ │ │ │ │ └── RealmConfig.java │ │ │ │ ├── consent/ │ │ │ │ │ ├── ConsentSelectionAction.java │ │ │ │ │ ├── ScopeBean.java │ │ │ │ │ ├── ScopeField.java │ │ │ │ │ └── ScopeFieldBean.java │ │ │ │ ├── context/ │ │ │ │ │ └── ContextSelectionAction.java │ │ │ │ ├── email/ │ │ │ │ │ └── AcmeEmailSenderProvider.java │ │ │ │ ├── endpoints/ │ │ │ │ │ ├── CorsUtils.java │ │ │ │ │ ├── CustomAdminResourceProvider.java │ │ │ │ │ ├── CustomResource.java │ │ │ │ │ ├── CustomResourceProvider.java │ │ │ │ │ ├── account/ │ │ │ │ │ │ └── AcmeAccountResource.java │ │ │ │ │ ├── admin/ │ │ │ │ │ │ ├── AdminSettingsResource.java │ │ │ │ │ │ ├── CustomAdminResource.java │ │ │ │ │ │ └── UserProvisioningResource.java │ │ │ │ │ ├── applications/ │ │ │ │ │ │ └── ApplicationsInfoResource.java │ │ │ │ │ ├── branding/ │ │ │ │ │ │ └── BrandingResource.java │ │ │ │ │ ├── credentials/ │ │ │ │ │ │ └── UserCredentialsInfoResource.java │ │ │ │ │ ├── demo/ │ │ │ │ │ │ └── DemosResource.java │ │ │ │ │ ├── idp/ │ │ │ │ │ │ └── IdpApplications.java │ │ │ │ │ ├── migration/ │ │ │ │ │ │ ├── TokenMigrationResource.java │ │ │ │ │ │ └── UserImportMigrationResource.java │ │ │ │ │ ├── offline/ │ │ │ │ │ │ ├── OfflineSessionPropagationResource.java │ │ │ │ │ │ ├── SessionPropagationActionToken.java │ │ │ │ │ │ └── SessionPropagationActionTokenHandler.java │ │ │ │ │ ├── profile/ │ │ │ │ │ │ ├── ProfileData.java │ │ │ │ │ │ └── UserProfileResource.java │ │ │ │ │ └── settings/ │ │ │ │ │ └── UserSettingsResource.java │ │ │ │ ├── eventpublishing/ │ │ │ │ │ ├── AcmeEventPublisherEventListener.java │ │ │ │ │ ├── EventPublisher.java │ │ │ │ │ ├── NatsEventPublisher.java │ │ │ │ │ └── NoopPublisher.java │ │ │ │ ├── health/ │ │ │ │ │ ├── CustomHealthChecks.java │ │ │ │ │ └── CustomReadinessCheck.java │ │ │ │ ├── idp/ │ │ │ │ │ ├── azure/ │ │ │ │ │ │ ├── CustomAzureADGroupMapper.java │ │ │ │ │ │ └── CustomEntraIdProfileMapper.java │ │ │ │ │ ├── brokering/ │ │ │ │ │ │ └── RestrictBrokeredUserMapper.java │ │ │ │ │ ├── linking/ │ │ │ │ │ │ └── AcmeIdpLinkAction.java │ │ │ │ │ ├── oidc/ │ │ │ │ │ │ └── AcmeOidcIdentityProvider.java │ │ │ │ │ └── social/ │ │ │ │ │ └── linkedin/ │ │ │ │ │ └── LinkedInUserProfileImportIdpMapper.java │ │ │ │ ├── infinispan/ │ │ │ │ │ └── CustomInfinispanUserSessionProviderFactory.java │ │ │ │ ├── jpa/ │ │ │ │ │ └── CustomQuarkusJpaConnectionProviderFactory.java │ │ │ │ ├── metrics/ │ │ │ │ │ ├── KeycloakMetric.java │ │ │ │ │ ├── KeycloakMetricAccessor.java │ │ │ │ │ ├── KeycloakMetricStore.java │ │ │ │ │ ├── KeycloakMetrics.java │ │ │ │ │ ├── RealmMetricUpdater.java │ │ │ │ │ ├── RealmMetricsUpdater.java │ │ │ │ │ └── events/ │ │ │ │ │ ├── MetricEventListenerProvider.java │ │ │ │ │ └── MetricEventRecorder.java │ │ │ │ ├── migration/ │ │ │ │ │ └── acmecred/ │ │ │ │ │ ├── AcmeCredentialModel.java │ │ │ │ │ ├── AcmeCredentialProvider.java │ │ │ │ │ └── AcmePasswordValidator.java │ │ │ │ ├── oauth/ │ │ │ │ │ ├── client/ │ │ │ │ │ │ └── OauthClientCredentialsTokenManager.java │ │ │ │ │ └── tokenexchange/ │ │ │ │ │ ├── ApiKeyTokenExchangeProvider.java │ │ │ │ │ ├── CustomTokenExchangeProvider.java │ │ │ │ │ └── CustomV2TokenExchangeProvider.java │ │ │ │ ├── oidc/ │ │ │ │ │ ├── ageinfo/ │ │ │ │ │ │ └── AgeInfoMapper.java │ │ │ │ │ ├── authzenclaims/ │ │ │ │ │ │ └── AuthzenClaimMapper.java │ │ │ │ │ ├── opaclaims/ │ │ │ │ │ │ └── OpaClaimMapper.java │ │ │ │ │ ├── remoteclaims/ │ │ │ │ │ │ └── RemoteOidcMapper.java │ │ │ │ │ ├── scopes/ │ │ │ │ │ │ └── OnlyGrantedScopesMapper.java │ │ │ │ │ ├── userdata/ │ │ │ │ │ │ └── AcmeUserInfoMapper.java │ │ │ │ │ └── wellknown/ │ │ │ │ │ └── AcmeOidcWellKnownProvider.java │ │ │ │ ├── profile/ │ │ │ │ │ ├── AcmeUserAttributes.java │ │ │ │ │ ├── emailupdate/ │ │ │ │ │ │ └── UpdateEmailRequiredAction.java │ │ │ │ │ └── phonenumber/ │ │ │ │ │ └── AcmePhoneValidator.java │ │ │ │ ├── registration/ │ │ │ │ │ ├── actiontokens/ │ │ │ │ │ │ └── AcmeExecuteActionsActionTokenHandler.java │ │ │ │ │ └── formaction/ │ │ │ │ │ ├── CustomRegistrationUserCreation.java │ │ │ │ │ └── WelcomeEmailFormAction.java │ │ │ │ ├── saml/ │ │ │ │ │ ├── AcmeSamlAuthenticationPreprocessor.java │ │ │ │ │ ├── brokering/ │ │ │ │ │ │ └── AcmeSamlRoleImporter.java │ │ │ │ │ └── rolelist/ │ │ │ │ │ └── AcmeSamlRoleListMapper.java │ │ │ │ ├── scheduling/ │ │ │ │ │ ├── ScheduledTaskProvider.java │ │ │ │ │ ├── ScheduledTaskProviderFactory.java │ │ │ │ │ ├── ScheduledTaskSpi.java │ │ │ │ │ └── tasks/ │ │ │ │ │ └── AcmeScheduledTaskProvider.java │ │ │ │ ├── security/ │ │ │ │ │ ├── filter/ │ │ │ │ │ │ └── IpAccessFilter.java │ │ │ │ │ └── friendlycaptcha/ │ │ │ │ │ ├── FriendlyCaptcha.java │ │ │ │ │ ├── FriendlyCaptchaClient.java │ │ │ │ │ ├── FriendlyCaptchaConfig.java │ │ │ │ │ └── FriendlyCaptchaFormAction.java │ │ │ │ ├── support/ │ │ │ │ │ ├── AuthUtils.java │ │ │ │ │ ├── ConfigUtils.java │ │ │ │ │ ├── CookieHelper.java │ │ │ │ │ ├── CookieUtils.java │ │ │ │ │ ├── CredentialUtils.java │ │ │ │ │ ├── LocaleUtils.java │ │ │ │ │ ├── RealmUtils.java │ │ │ │ │ ├── RequiredActionUtils.java │ │ │ │ │ ├── ScopeUtils.java │ │ │ │ │ ├── TokenUtils.java │ │ │ │ │ ├── UserSessionUtils.java │ │ │ │ │ └── UserUtils.java │ │ │ │ ├── terms/ │ │ │ │ │ └── AcmeTermsAndConditionsAction.java │ │ │ │ ├── themes/ │ │ │ │ │ └── login/ │ │ │ │ │ ├── AcmeFreeMarkerLoginFormsProvider.java │ │ │ │ │ ├── AcmeLoginBean.java │ │ │ │ │ └── AcmeUrlBean.java │ │ │ │ └── userstorage/ │ │ │ │ ├── adhoc/ │ │ │ │ │ └── AdhocUserStorageProvider.java │ │ │ │ ├── ldap/ │ │ │ │ │ ├── AcmeLDAPStorageProvider.java │ │ │ │ │ └── AcmeReadonlyLDAPUserModelDelegate.java │ │ │ │ └── remote/ │ │ │ │ ├── AcmeUserAdapter.java │ │ │ │ ├── AcmeUserStorageProvider.java │ │ │ │ └── accountclient/ │ │ │ │ ├── AccountClientOptions.java │ │ │ │ ├── AcmeAccountClient.java │ │ │ │ ├── AcmeUser.java │ │ │ │ ├── SimpleAcmeAccountClient.java │ │ │ │ ├── UserSearchInput.java │ │ │ │ ├── UserSearchOutput.java │ │ │ │ ├── VerifyCredentialsInput.java │ │ │ │ └── VerifyCredentialsOutput.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── keycloak-scripts.json │ │ │ ├── ignore_default-persistence.xml │ │ │ ├── my-script-authenticator.js │ │ │ └── my-script-mapper.js │ │ └── test/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── github/ │ │ │ └── thomasdarimont/ │ │ │ └── keycloak/ │ │ │ └── custom/ │ │ │ ├── BoostrapTest.java │ │ │ ├── KeycloakEnvironment.java │ │ │ ├── KeycloakIntegrationTest.java │ │ │ └── KeycloakTestSupport.java │ │ └── resources/ │ │ ├── log4j.properties │ │ └── testcontainers.properties │ ├── http-tests/ │ │ ├── advanced_oauth_par.http │ │ ├── advanced_oauth_resources.http │ │ ├── custom-token-migration.http │ │ ├── custom_token_exchange.http │ │ ├── dynamic-client-registration.http │ │ ├── example-requests.http │ │ ├── grant_type_client_credentials-requests.http │ │ ├── grant_type_password-requests.http │ │ ├── grant_type_refreshtoken-requests.http │ │ ├── http-client.env.json │ │ ├── implicit-flow-request.http │ │ ├── keycloak-lightweight-token-requests.http │ │ ├── oidc-endpoint-requests.http │ │ ├── token_exchange.http │ │ ├── token_exchange_api_gateway.http │ │ ├── token_exchange_fed-identity-chaining.http │ │ └── token_exchange_v2.http │ ├── misc/ │ │ ├── custom-keycloak-server/ │ │ │ ├── pom.xml │ │ │ ├── readme.md │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── copy-to-keycloak/ │ │ │ │ └── conf/ │ │ │ │ └── quarkus.properties │ │ │ ├── java/ │ │ │ │ └── demo/ │ │ │ │ └── events/ │ │ │ │ └── MyEventListener.java │ │ │ └── resources/ │ │ │ ├── META-INF/ │ │ │ │ ├── keycloak-themes.json │ │ │ │ └── keycloak.conf │ │ │ └── theme/ │ │ │ └── custom/ │ │ │ └── login/ │ │ │ ├── messages/ │ │ │ │ └── messages_en.properties │ │ │ ├── resources/ │ │ │ │ ├── css/ │ │ │ │ │ └── custom-login.css │ │ │ │ └── js/ │ │ │ │ └── custom-login.js │ │ │ └── theme.properties │ │ └── snippets/ │ │ ├── create-keycloak-config-cli-client.txt │ │ ├── jgroups-debugging.txt │ │ ├── jmx-config-keycloakx.md │ │ ├── jmx-config-wildfly.md │ │ ├── jvm-settings.txt │ │ ├── keycloakx-cli.md │ │ ├── metrics-examples.txt │ │ └── overlay-keycloak-endpoint-undertow.txt │ ├── patches/ │ │ ├── keycloak-model-infinispan-patch/ │ │ │ ├── pom.xml │ │ │ ├── readme.md │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── keycloak/ │ │ │ ├── models/ │ │ │ │ └── sessions/ │ │ │ │ └── infinispan/ │ │ │ │ └── CacheDecorators.java │ │ │ └── patch/ │ │ │ └── infinispan/ │ │ │ └── keymappers/ │ │ │ └── CustomDefaultTwoWayKey2StringMapper.java │ │ ├── wildfly-clustering-infinispan-extension-patch/ │ │ │ ├── pom.xml │ │ │ ├── readme.md │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── jboss/ │ │ │ └── as/ │ │ │ └── clustering/ │ │ │ └── infinispan/ │ │ │ └── subsystem/ │ │ │ ├── InfinispanSubsystemXMLReader.java │ │ │ ├── LocalDescriptions.properties │ │ │ ├── RemoteStoreResourceDefinition.java │ │ │ ├── RemoteStoreServiceConfigurator.java │ │ │ ├── XMLAttribute.java │ │ │ └── remote/ │ │ │ └── RemoteCacheContainerResourceDefinition.java │ │ ├── wildfly-clustering-infinispan-extension-patch-25.0.x/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── org/ │ │ │ └── jboss/ │ │ │ └── as/ │ │ │ └── clustering/ │ │ │ └── infinispan/ │ │ │ └── subsystem/ │ │ │ ├── InfinispanSubsystemXMLReader.java │ │ │ ├── LocalDescriptions.properties │ │ │ ├── RemoteStoreResourceDefinition.java │ │ │ ├── RemoteStoreServiceConfigurator.java │ │ │ └── remote/ │ │ │ └── RemoteCacheContainerResourceDefinition.java │ │ └── wildfly-clustering-infinispan-extension-patch-26.0.x/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── org/ │ │ └── jboss/ │ │ └── as/ │ │ └── clustering/ │ │ └── infinispan/ │ │ └── subsystem/ │ │ ├── InfinispanSubsystemXMLReader.java │ │ ├── LocalDescriptions.properties │ │ ├── RemoteStoreResourceDefinition.java │ │ └── RemoteStoreServiceConfigurator.java │ └── themes/ │ ├── acme-account.v2/ │ │ └── account/ │ │ ├── messages/ │ │ │ ├── messages_de.properties │ │ │ └── messages_en.properties │ │ ├── resources/ │ │ │ ├── content.json │ │ │ └── css/ │ │ │ └── styles.css │ │ └── theme.properties │ ├── admin-custom/ │ │ └── admin/ │ │ ├── admin-settings.ftl │ │ ├── resources/ │ │ │ └── js/ │ │ │ └── admin-settings.js │ │ └── theme.properties │ ├── apps/ │ │ └── login/ │ │ ├── login.ftl │ │ ├── messages/ │ │ │ ├── messages_de.properties │ │ │ └── messages_en.properties │ │ ├── register.ftl │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── custom-login.css │ │ │ └── js/ │ │ │ └── custom-login.js │ │ ├── terms.ftl │ │ └── theme.properties │ ├── custom/ │ │ └── login/ │ │ ├── messages/ │ │ │ └── messages_en.properties │ │ ├── register.ftl │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── custom-login.css │ │ │ └── js/ │ │ │ └── custom-login.js │ │ └── theme.properties │ ├── internal/ │ │ ├── account/ │ │ │ ├── account.ftl │ │ │ ├── messages/ │ │ │ │ ├── messages_de.properties │ │ │ │ └── messages_en.properties │ │ │ └── theme.properties │ │ ├── email/ │ │ │ ├── html/ │ │ │ │ ├── acme-account-blocked.ftl │ │ │ │ ├── acme-account-deletion-requested.ftl │ │ │ │ ├── acme-account-updated.ftl │ │ │ │ ├── acme-email-verification-with-code.ftl │ │ │ │ ├── acme-magic-link.ftl │ │ │ │ ├── acme-mfa-added.ftl │ │ │ │ ├── acme-mfa-removed.ftl │ │ │ │ ├── acme-passkey-added.ftl │ │ │ │ ├── acme-passkey-removed.ftl │ │ │ │ ├── acme-trusted-device-added.ftl │ │ │ │ ├── acme-trusted-device-removed.ftl │ │ │ │ ├── acme-welcome.ftl │ │ │ │ ├── code-email.ftl │ │ │ │ └── template.ftl │ │ │ ├── messages/ │ │ │ │ ├── messages_de.properties │ │ │ │ └── messages_en.properties │ │ │ ├── text/ │ │ │ │ ├── acme-account-blocked.ftl │ │ │ │ ├── acme-account-deletion-requested.ftl │ │ │ │ ├── acme-account-updated.ftl │ │ │ │ ├── acme-email-verification-with-code.ftl │ │ │ │ ├── acme-magic-link.ftl │ │ │ │ ├── acme-mfa-added.ftl │ │ │ │ ├── acme-mfa-removed.ftl │ │ │ │ ├── acme-passkey-added.ftl │ │ │ │ ├── acme-passkey-removed.ftl │ │ │ │ ├── acme-trusted-device-added.ftl │ │ │ │ ├── acme-trusted-device-removed.ftl │ │ │ │ ├── acme-welcome.ftl │ │ │ │ ├── code-email.ftl │ │ │ │ └── template.ftl │ │ │ └── theme.properties │ │ └── login/ │ │ ├── email-code-form.ftl │ │ ├── login-confirm-cookie-form.ftl │ │ ├── login-magic-link.ftl │ │ ├── login-otp.ftl │ │ ├── login-password.ftl │ │ ├── login-select-mfa-method.ftl │ │ ├── login-skippable-action.ftl │ │ ├── login-sms.ftl │ │ ├── login-username.ftl │ │ ├── manage-trusted-device-form.ftl │ │ ├── messages/ │ │ │ ├── messages_de.properties │ │ │ └── messages_en.properties │ │ ├── register.ftl │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── custom-login.css │ │ │ └── js/ │ │ │ └── custom-login.js │ │ ├── select-consent-form.ftl │ │ ├── theme.properties │ │ ├── update-email-form.ftl │ │ ├── update-phone-number-form.ftl │ │ ├── verify-email-form.ftl │ │ └── verify-phone-number-form.ftl │ ├── internal-modern/ │ │ ├── account/ │ │ │ └── theme.properties │ │ ├── email/ │ │ │ ├── messages/ │ │ │ │ ├── messages_de.properties │ │ │ │ └── messages_en.properties │ │ │ └── theme.properties │ │ └── login/ │ │ ├── context-selection.ftl │ │ ├── login-applications.ftl │ │ ├── login-idp-selection.ftl │ │ ├── login-password.ftl │ │ ├── login-username.ftl │ │ ├── login-verify-email.ftl │ │ ├── login.ftl │ │ ├── messages/ │ │ │ ├── messages_de.properties │ │ │ └── messages_en.properties │ │ ├── register.ftl │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── custom-modern-login.css │ │ │ └── js/ │ │ │ └── custom-modern-login.js │ │ ├── select-authenticator.ftl │ │ ├── template.ftl │ │ └── theme.properties │ ├── minimal/ │ │ └── login/ │ │ ├── resources/ │ │ │ ├── css/ │ │ │ │ └── custom-login.css │ │ │ └── js/ │ │ │ └── custom-login.js │ │ └── theme.properties │ └── minimal-branding/ │ └── login/ │ ├── customizations.ftl │ ├── info.ftl │ ├── login-update-password.ftl │ ├── resources/ │ │ ├── css/ │ │ │ └── custom-login.css │ │ └── js/ │ │ └── custom-login.js │ ├── template.ftl │ └── theme.properties ├── keycloak.env ├── maven-settings.xml ├── pom.xml ├── readme.md ├── start.java ├── stop.java └── tools/ ├── kcadm/ │ └── readme.md ├── postman/ │ ├── acme.postman_collection.json │ ├── acme.postman_environment_http.json │ ├── acme.postman_environment_https.json │ ├── acme.postman_globals.json │ └── readme.md ├── session-generator/ │ ├── .gitignore │ ├── .mvn/ │ │ └── wrapper/ │ │ ├── MavenWrapperDownloader.java │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── github/ │ │ │ └── thomasdarimont/ │ │ │ └── keycloak/ │ │ │ └── tools/ │ │ │ └── sessiongenerator/ │ │ │ └── SessionGeneratorApplication.java │ │ └── resources/ │ │ └── application.properties │ └── test/ │ └── java/ │ └── com/ │ └── github/ │ └── thomasdarimont/ │ └── keycloak/ │ └── tools/ │ └── sessiongenerator/ │ └── SessionGeneratorApplicationTests.java └── tcpdump/ ├── Dockerfile └── readme.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: ACME Keycloak Build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Configure JDK 21 uses: actions/setup-java@v1 with: java-version: 21 - name: Cache Maven packages uses: actions/cache@v1 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: Maven - verify run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn verify --file pom.xml --settings maven-settings.xml - name: Maven - integration-test run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -Pwith-integration-tests test --file pom.xml --settings maven-settings.xml - name: Maven - build image run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -DskipTests io.fabric8:docker-maven-plugin:build --file pom.xml --settings maven-settings.xml ================================================ FILE: .github/workflows/e2e-tests.yml ================================================ name: Acme e2e Test CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: e2e-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Configure JDK 21 uses: actions/setup-java@v1 with: java-version: 21 - name: Configure Node.js '18.x' uses: actions/setup-node@v1 with: node-version: '18.x' - name: Prepare artifacts (e.g. extensions) for docker-compose stack run: mvn --batch-mode -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -DskipTests verify --file pom.xml --settings maven-settings.xml -pl keycloak/extensions,keycloak/docker - name: Build the docker-compose stack run: | USER="$(id -u)" GROUP="" export USER export GROUP echo "Running as ${USER}:${GROUP}" java bin/envcheck.java touch deployments/local/dev/keycloakx/acme.test+1.pem java start.java --http --extensions=jar --keycloak=keycloakx --ci=github --detach - name: Show current containers run: | docker ps -a # - name: Check keycloak is reachable # run: docker run --network container:dev_acme-keycloak_1 --rm appropriate/curl -s --retry 15 --max-time 180 --retry-connrefused http://localhost:8080/auth/realms/acme-internal - name: Sleep uses: jakejarvis/wait-action@master with: time: '70s' - name: Check docker-compose stack if: ${{ always() }} run: | docker ps -a docker inspect dev-acme-keycloak-1 docker logs --details dev-acme-keycloak-1 - name: Run cypress tests working-directory: ./keycloak/e2e-tests # https://docs.cypress.io/guides/references/configuration#Timeouts run: | yarn install docker run --network container:dev-acme-keycloak-1 --rm -v "${PWD}":/e2e -w /e2e --entrypoint=cypress cypress/included:10.8.0 run --config pageLoadTimeout=70000,defaultCommandTimeout=10000,watchForFileChanges=false --env keycloak_host=http://localhost:8080 - name: Archive testrun video if: ${{ always() }} uses: actions/upload-artifact@v2 with: name: testrun-video path: ./keycloak/e2e-tests/cypress/videos/ retention-days: 1 - name: Shutdown the docker-compose stack if: ${{ always() }} run: java stop.java --skip=grafana ================================================ FILE: .gitignore ================================================ !testrun/.gitkeep testrun/ !run/.gitkeep run/ deployments/local/dev/run/ deployments/local/cluster/haproxy-external-ispn/ispn/data/ispn-1/ deployments/local/cluster/haproxy-external-ispn/ispn/data/ispn-2/ deployments/local/clusterx/haproxy-external-ispn/ispn/data/ispn-1/ deployments/local/clusterx/haproxy-external-ispn/ispn/data/ispn-2/ !deployments/local/cluster/run/.gitkeep deployments/local/cluster/run/ *.pem *-key.pem local.env module-thorntail.xml !scratch/.gitkeep scratch/ *.jfr !keycloak/imex/.gitkeep keycloak/imex/* config/stage/dev/tls/*.pem config/stage/dev/tls/*.p12 config/stage/dev/tls/*.crt config/stage/dev/tls/*.key # Exclude cypress resources node_modules src/test/e2e/cypress/reports src/test/e2e/cypress/videos src/test/e2e/cypress/screenshots # Created by https://www.gitignore.io/api/osx,java,maven,gradle,eclipse,intellij+all,visualstudiocode # Edit at https://www.gitignore.io/?templates=osx,java,maven,gradle,eclipse,intellij+all,visualstudiocode ### Eclipse ### .metadata tmp/ *.tmp *.bak *.swp *~.nib local.properties .settings/ .loadpath .recommenders # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # PyDev specific (Python IDE for Eclipse) *.pydevproject # CDT-specific (C/C++ Development Tooling) .cproject # CDT- autotools .autotools # Java annotation processor (APT) .factorypath # PDT-specific (PHP Development Tools) .buildpath # sbteclipse plugin .target # Tern plugin .tern-project # TeXlipse plugin .texlipse # STS (Spring Tool Suite) .springBeans # Code Recommenders .recommenders/ # Annotation Processing .apt_generated/ # Scala IDE specific (Scala & Java development for Eclipse) .cache-main .scala_dependencies .worksheet ### Eclipse Patch ### # Eclipse Core .project # JDT-specific (Eclipse Java Development Tools) .classpath # Annotation Processing .apt_generated .sts4-cache/ ### Intellij+all ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/modules.xml # .idea/*.iml # .idea/modules # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### Intellij+all Patch ### # Ignores the whole .idea folder and all .iml files # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 .idea/ # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 *.iml modules.xml .idea/misc.xml *.ipr # Sonarlint plugin .idea/sonarlint ### Java ### # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar !bin/opentelemetry-javaagent-*.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* ### Maven ### target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties .mvn/wrapper/maven-wrapper.jar ### OSX ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ### VisualStudioCode Patch ### # Ignore all local history of files .history ### Gradle ### .gradle build/ # Ignore Gradle GUI config gradle-app.setting # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar # Cache of project .gradletasknamecache # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties ### Gradle Patch ### **/build/ # End of https://www.gitignore.io/api/osx,java,maven,gradle,eclipse,intellij+all,visualstudiocode # Created by https://www.toptal.com/developers/gitignore/api/node # Edit at https://www.toptal.com/developers/gitignore?templates=node ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test .env*.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Storybook build outputs .out .storybook-out storybook-static # rollup.js default build output dist/ # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # Temporary folders tmp/ temp/ # End of https://www.toptal.com/developers/gitignore/api/node /apps/offline-session-client/data/ /deployments/local/cluster/haproxy-external-ispn/ispn/data/sessions/ ================================================ FILE: .run/Acme Backend API Quarkus.run.xml ================================================ true ================================================ FILE: .run/Keycloak Remote.run.xml ================================================ ================================================ FILE: .run/OfflineSessionClient (logout).run.xml ================================================ ================================================ FILE: .run/OfflineSessionClient.run.xml ================================================ ================================================ FILE: .run/acme-webapp-saml-node-express.run.xml ================================================ ================================================ FILE: apps/acme-greetme/index.html ================================================ App: ClientId

ClientId:

================================================ FILE: apps/acme-webapp-saml-node-express/package.json ================================================ { "name": "acme-webapp-saml-node-express", "version": "1.0.0", "main": "src/index.js", "author": "Thomas Darimont", "license": "MIT", "dependencies": { "@node-saml/passport-saml": "^4.0.2", "body-parser": "^1.20.2", "ejs": "^3.1.8", "es6-promisify": "^7.0.0", "express": "^4.18.2", "express-session": "^1.17.3", "https": "^1.0.0", "passport": "^0.6.0", "spdy": "^4.0.2", "stoppable": "^1.1.0", "winston": "^3.8.2" }, "type": "module", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js" }, "devDependencies": { "nodemon": "^2.0.12" } } ================================================ FILE: apps/acme-webapp-saml-node-express/src/config.js ================================================ import fs from "fs"; const IDP_ISSUER = process.env.IDP_ISSUER || "https://id.acme.test:8443/auth/realms/acme-internal"; const PORT = process.env.PORT || 4723; const HOSTNAME = process.env.HOSTNAME || "apps.acme.test" + (PORT === 443 ? "" : ":"+ PORT); const SP_ISSUER = process.env.SP_ISSUER || "acme-webapp-saml-node-express"; const TLS_CERT_FILE = process.env.TLS_CERT_FILE || '../../config/stage/dev/tls/acme.test+1.pem'; const TLS_KEY_FILE = process.env.TLS_KEY_FILE || '../../config/stage/dev/tls/acme.test+1-key.pem'; const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; const LOG_FORMAT = process.env.LOG_FORMAT || 'json'; // plain / json // see https://github.com/RisingStack/kubernetes-graceful-shutdown-example/blob/master/src/index.js const READINESS_PROBE_DELAY = process.env.READINESS_PROBE_DELAY || 1000; // 2 * 2 * 1000; // failureThreshold: 2, periodSeconds: 2 (4s) const SESSION_SECRET = process.env.SECRET || 'keyboard cat'; // Private Key generated in SAML client in Keycloak let SAML_SP_KEY = process.env.SAML_SP_KEY || fs.readFileSync("sp.key.pem", "utf-8") // realm certificate used to sign saml requests from Keycloak let SAML_IDP_CERT = process.env.SAML_IDP_CERT; export default { IDP_ISSUER, SP_ISSUER, HOSTNAME, PORT, SAML_SP_KEY, SAML_IDP_CERT, TLS_CERT_FILE, TLS_KEY_FILE, LOG_LEVEL, LOG_FORMAT, READINESS_PROBE_DELAY, SESSION_SECRET }; ================================================ FILE: apps/acme-webapp-saml-node-express/src/express.js ================================================ import express from "express"; import session from "express-session"; import passport from "passport"; import {Strategy as SamlStrategy} from "@node-saml/passport-saml"; import {default as bodyParser} from "body-parser"; function createExpressApp(config, LOG) { LOG.info("Create express app"); const app = express(); app.use(bodyParser.urlencoded({extended: true})); configureSession(app, config); configureSaml(app, config, LOG); configureTemplateEngine(app, config); configureRoutes(app, config); return app; } function configureSession(app, config) { app.use(session({ secret: config.SESSION_SECRET, resave: false, saveUninitialized: true, cookie: {secure: true} })); } let samlStrategy; function configureSaml(app, config, LOG) { app.use(passport.initialize()); app.use(passport.session()); passport.serializeUser(function (user, done) { done(null, user); }); passport.deserializeUser(function (user, done) { done(null, user); }); let idpSamlMetadataUrl = config.IDP_ISSUER + "/protocol/saml/descriptor"; LOG.info("Fetching SAML metadata from IdP: " + idpSamlMetadataUrl); fetch(idpSamlMetadataUrl).then(response => response.text()).then(samlMetadata => { LOG.info("Successfully fetched SAML metadata from IdP"); // LOG.info("##### SAML Metadata: \n" + samlMetadata) // poor man's IdP metadata parsing let idpCert = samlMetadata.match(/(.*)<\/ds:X509Certificate>/)[1]; samlStrategy = new SamlStrategy( // See Config parameter details: https://www.npmjs.com/package/passport-saml // See also https://github.com/node-saml/passport-saml { entryPoint: config.IDP_ISSUER + "/protocol/saml", issuer: config.SP_ISSUER, host: config.HOSTNAME, protocol: "https://", signatureAlgorithm: "sha256", privateKey: config.SAML_SP_KEY, // cert: config.SAML_IDP_CERT, cert: idpCert, passReqToCallback: true, logoutUrl: config.IDP_ISSUER + "/protocol/saml", identifierFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", }, // Sign-in Verify function (request, profile, done) { // profile contains user profile data sent from server let user = { username: profile["nameID"], firstname: profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"], lastname: profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"], email: profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"], // e.g. if you added a Group claim group: profile["http://schemas.xmlsoap.org/claims/Group"], nameID: profile.nameID, nameIDFormat: profile.nameIDFormat, }; return done(null, user); }, // Sign-out Verify function (request, profile, done) { // profile contains user profile data sent from server let user = { username: profile["nameID"], firstname: profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"], lastname: profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"], email: profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"], // e.g. if you added a Group claim group: profile["http://schemas.xmlsoap.org/claims/Group"], nameID: profile.nameID, nameIDFormat: profile.nameIDFormat, }; return done(null, user); } ); passport.use(samlStrategy); }).catch((error) => { console.error('Could not fetch Saml Metadata from IdP', error); }); } function configureTemplateEngine(app, config) { // set the view engine to ejs app.set('view engine', 'ejs'); } function configureRoutes(app, config) { let ensureAuthenticated = function (req, res, next) { if (!req.isAuthenticated()) { // let redirectTo = `${req.protocol}://${req.get('host')}${req.originalUrl}`; // res.session.redirectToUrl = req.originalUrl; res.redirect('/login?target=' + encodeURIComponent(req.originalUrl)) return; } return next(); } app.get('/login', function (req, res, next) { // try to extract desired target location in app let additionalParams = null; if (req.query.target) { let decodedTarget = decodeURIComponent(req.query.target); if (decodedTarget.startsWith("/")) { additionalParams = { RelayState: encodeURIComponent(decodedTarget) }; } } passport.authenticate('saml', { failureRedirect: '/', failureFlash: true, additionalParams: additionalParams }, null)(req, res, next); }, function (req, res) { res.redirect('/app'); } ); app.post('/saml', passport.authenticate('saml', { failureRedirect: '/error', failureFlash: true }, null), (req, res) => { // success redirection to index return res.redirect('/'); } ); app.post('/saml/consume', passport.authenticate('saml', { failureRedirect: '/error', failureFlash: true }), (req, res) => { if (req.body.RelayState) { let decodedTargetUri = decodeURIComponent(req.body.RelayState); if (decodedTargetUri.startsWith("/")) { return res.redirect(decodedTargetUri); } } // success redirection to /app return res.redirect('/app'); } ); app.get('/logout', ensureAuthenticated, (req, res, next) => { if (req.user != null) { return samlStrategy.logout(req, (err, uri) => { return req.logout(err => { if (err) { LOG.warn("Could not logout: " + err); return next(err); } req.session.destroy(); res.redirect(uri); }); }); } return res.redirect('/'); }); app.get('/error', function (req, res) { res.render('pages/error'); } ); app.get('/', function (req, res) { res.render('pages/index'); } ); app.get('/app', ensureAuthenticated, function (req, res) { let user = req.user; res.render('pages/app', { user }); } ); app.get('/page1', ensureAuthenticated, function (req, res) { let user = req.user; res.render('pages/page1', { user }); } ); } export default createExpressApp; ================================================ FILE: apps/acme-webapp-saml-node-express/src/index.js ================================================ 'use strict' import config from './config.js'; import initLogging from './logging.js'; import createExpressApp from './express.js'; import createServer from "./server.js"; const LOG = initLogging(config); const app = createExpressApp(config, LOG); createServer(app, config, LOG); ================================================ FILE: apps/acme-webapp-saml-node-express/src/logging.js ================================================ import winston from "winston"; function initLogging(config) { const loggingFormat = winston.format.combine( winston.format.timestamp(), 'json' === config.LOG_FORMAT ? winston.format.json() : winston.format.simple() ); return winston.createLogger({ level: config.LOG_LEVEL, format: loggingFormat, defaultMeta: {service: 'acme-webapp-saml-node-express'}, transports: [ new winston.transports.Console(), // // - Write all logs with level `error` and below to `error.log` // - Write all logs with level `info` and below to `combined.log` // // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], }); } export default initLogging; ================================================ FILE: apps/acme-webapp-saml-node-express/src/server.js ================================================ import fs from "fs"; import stoppable from "stoppable"; import {promisify} from "es6-promisify"; import spdy from "spdy"; function createServer(app, config, LOG) { LOG.info("Create server"); const httpsServer = spdy.createServer({ key: fs.readFileSync(config.TLS_KEY_FILE), cert: fs.readFileSync(config.TLS_CERT_FILE), }, app); // for Graceful shutdown see https://github.com/RisingStack/kubernetes-graceful-shutdown-example configureGracefulShutdown(httpsServer, config, LOG); // Start server httpsServer.listen(config.PORT, () => { LOG.info(`Listening on HTTPS port ${config.PORT}`); LOG.info(`Using frontend URL ${config.FRONTEND_URL}`); }); } function configureGracefulShutdown(httpsServer, config, LOG) { // Keep-alive connections doesn't let the server to close in time // Destroy extension helps to force close connections // Because we wait READINESS_PROBE_DELAY, we expect that all requests are fulfilled // https://en.wikipedia.org/wiki/HTTP_persistent_connection stoppable(httpsServer); const serverDestroy = promisify(httpsServer.stop.bind(httpsServer)); // Graceful stop async function gracefulStop() { LOG.info('Server is shutting down...') try { await serverDestroy(); // close server first (ongoing requests) LOG.info('Successful graceful shutdown'); process.exit(0); // exit with ok code } catch (err) { LOG.error('Error happened during graceful shutdown', err) process.exit(1) // exit with not ok code } } // Support graceful shutdown // do not accept more request and release resources process.on('SIGTERM', () => { LOG.info('Got SIGTERM. Graceful shutdown start'); // Wait a little bit to give enough time for Kubernetes readiness probe to fail (we don't want more traffic) // Don't worry livenessProbe won't kill it until (failureThreshold: 3) => 30s // http://www.bite-code.com/2015/07/27/implementing-graceful-shutdown-for-docker-containers-in-go-part-2/ setTimeout(gracefulStop, config.READINESS_PROBE_DELAY); }); } export default createServer; ================================================ FILE: apps/acme-webapp-saml-node-express/views/pages/app.ejs ================================================ <%- include('../partials/head'); %>
<%- include('../partials/header'); %>

App

Welcome User: <%= user.username %>

  • Firstname: <%= user.firstname %>
  • Lastname: <%= user.lastname %>
  • E-Mail: <%= user.email %>
<%- include('../partials/footer'); %>
================================================ FILE: apps/acme-webapp-saml-node-express/views/pages/error.ejs ================================================ <%- include('../partials/head'); %>
<%- include('../partials/header'); %>

An error occurred!

<%- include('../partials/footer'); %>
================================================ FILE: apps/acme-webapp-saml-node-express/views/pages/index.ejs ================================================ <%- include('../partials/head'); %>
<%- include('../partials/header'); %>
Welcome!
<%- include('../partials/footer'); %>
================================================ FILE: apps/acme-webapp-saml-node-express/views/pages/page1.ejs ================================================ <%- include('../partials/head'); %>
<%- include('../partials/header'); %>

Page 1

Welcome User: <%= user.username %>

  • Firstname: <%= user.firstname %>
  • Lastname: <%= user.lastname %>
  • E-Mail: <%= user.email %>
<%- include('../partials/footer'); %>
================================================ FILE: apps/acme-webapp-saml-node-express/views/partials/footer.ejs ================================================

© Copyright 2022 Thomas Darimont

================================================ FILE: apps/acme-webapp-saml-node-express/views/partials/head.ejs ================================================ Acme Webapp with SAML and NodeJS Express ================================================ FILE: apps/acme-webapp-saml-node-express/views/partials/header.ejs ================================================ ================================================ FILE: apps/backend-api-dnc/api/.dockerignore ================================================ **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/.idea **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: apps/backend-api-dnc/api/.gitignore ================================================ # Visual Studio bin/ obj/ .vs/ *.user ================================================ FILE: apps/backend-api-dnc/api/Controllers/UsersController.cs ================================================ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Api.Controllers; [ApiController] [Route("/api/users")] public class UsersController { private readonly ILogger _logger; private readonly IHttpContextAccessor _accessor; public UsersController(ILogger logger, IHttpContextAccessor accessor) { _logger = logger; _accessor = accessor; } [Authorize] [HttpGet] [Route("me")] public object Me() { _logger.LogInformation("### Accessing {}", _accessor.HttpContext?.Request.Path.Value); // var username = _accessor.HttpContext?.User.FindFirst("preferred_username")?.Value; var username = _accessor.HttpContext?.User?.Identity?.Name; var data = new Dictionary { { "message", "Hello " + username }, { "backend", "AspNetCore" }, { "datetime", DateTime.Now } }; return data; } } ================================================ FILE: apps/backend-api-dnc/api/Dockerfile ================================================ FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["api/api.csproj", "api/"] RUN dotnet restore "api/api.csproj" COPY . . WORKDIR "/src/api" RUN dotnet build "api.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "api.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "api.dll"] ================================================ FILE: apps/backend-api-dnc/api/JwtBearerOptions.cs ================================================ namespace Api; /// /// Options for JWT Bearer authentication. /// public class JwtBearerOptions { /// /// Gets or sets the authority. /// /// /// The authority. /// public string Authority { get; set; } = String.Empty; /// /// Gets or sets the audience. /// /// /// The audience. /// public string Audience { get; set; } = String.Empty; } ================================================ FILE: apps/backend-api-dnc/api/Program.cs ================================================ using Microsoft.AspNetCore.Authentication.JwtBearer; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddLogging(config => { config.AddDebug(); config.AddConsole(); //etc }); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle // builder.Services.AddEndpointsApiExplorer(); // builder.Services.AddSwaggerGen(); builder.Services.AddCors(options => options.AddDefaultPolicy(b => { b.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); })); builder.Services.AddSingleton(); var jwtOptions = builder.Configuration.GetSection("JwtBearer").Get(); builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = jwtOptions.Authority; options.Audience = jwtOptions.Audience; options.RequireHttpsMetadata = false; options.TokenValidationParameters.NameClaimType = "preferred_username"; options.TokenValidationParameters.RoleClaimType = "role"; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { // app.UseSwagger(); // app.UseSwaggerUI(); } app.UseCors(); app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run(); ================================================ FILE: apps/backend-api-dnc/api/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:43687", "sslPort": 44332 } }, "profiles": { "api": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "https://localhost:7229;http://localhost:5178", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_Kestrel__Certificates__Default__Path": "../../../config/stage/dev/tls/acme.test+1.pem", "ASPNETCORE_Kestrel__Certificates__Default__KeyPath": "../../../config/stage/dev/tls/acme.test+1-key.pem" }, "workingDirectory": "." }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": false, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: apps/backend-api-dnc/api/api.csproj ================================================ net6.0 enable enable Linux ================================================ FILE: apps/backend-api-dnc/api/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "JwtBearer": { "Authority": "https://id.acme.test:8443/auth/realms/acme-internal", "Audience": "account" }, "Kestrel": { "HttpsInlineCertAndKeyFile": { "Url": "https://apps.acme.test:7229", "Certificate": { "Path": "../../../config/stage/dev/tls/acme.test+1.pem", "KeyPath": "../../../config/stage/dev/tls/acme.test+1-key.pem", "Password": "" } } } } ================================================ FILE: apps/backend-api-dnc/api/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "Kestrel": { "HttpsInlineCertAndKeyFile": { "Url": "https://apps.acme.test:7229", "Certificate": { "Path": "../../../config/stage/dev/tls/acme.test+1.pem", "KeyPath": "../../../config/stage/dev/tls/acme.test+1-key.pem", "Password": "" } } } } ================================================ FILE: apps/backend-api-dnc/backend-api-dnc.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api", "api\api.csproj", "{DF1D196F-6305-4ADD-A02E-84CAF13F59C7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF1D196F-6305-4ADD-A02E-84CAF13F59C7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal ================================================ FILE: apps/backend-api-micronaut/.gitignore ================================================ Thumbs.db .DS_Store .gradle build/ target/ out/ .idea *.iml *.ipr *.iws .project .settings .classpath .factorypath ================================================ FILE: apps/backend-api-micronaut/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.net.Authenticator; import java.net.PasswordAuthentication; import java.net.URL; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: apps/backend-api-micronaut/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: apps/backend-api-micronaut/README.md ================================================ ## Micronaut 3.3.4 Documentation - [User Guide](https://docs.micronaut.io/3.3.4/guide/index.html) - [API Reference](https://docs.micronaut.io/3.3.4/api/index.html) - [Configuration Reference](https://docs.micronaut.io/3.3.4/guide/configurationreference.html) - [Micronaut Guides](https://guides.micronaut.io/index.html) --- ## Feature http-client documentation - [Micronaut HTTP Client documentation](https://docs.micronaut.io/latest/guide/index.html#httpClient) ## Feature security-jwt documentation - [Micronaut Security JWT documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html) ================================================ FILE: apps/backend-api-micronaut/micronaut-cli.yml ================================================ applicationType: default defaultPackage: com.acme.backend.micronaut testFramework: junit sourceLanguage: java buildTool: maven features: [annotation-api, app-name, http-client, jackson-databind, java, java-application, junit, logback, maven, netty-server, readme, security-annotations, security-jwt, shade, yaml] ================================================ FILE: apps/backend-api-micronaut/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/backend-api-micronaut/mvnw.bat ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: apps/backend-api-micronaut/pom.xml ================================================ 4.0.0 com.acme.backend.micronaut backend-api-micronaut 0.1 ${packaging} io.micronaut micronaut-parent 3.4.0 jar 11 11 ${project.parent.version} com.acme.backend.micronaut.Application netty central https://repo.maven.apache.org/maven2 io.micronaut micronaut-inject compile io.micronaut micronaut-validation compile org.junit.jupiter junit-jupiter-api test org.junit.jupiter junit-jupiter-engine test io.micronaut.test micronaut-test-junit5 test io.micronaut micronaut-http-client compile io.micronaut micronaut-http-server-netty compile io.micronaut micronaut-jackson-databind compile io.micronaut micronaut-runtime compile io.micronaut.security micronaut-security-jwt compile jakarta.annotation jakarta.annotation-api compile ch.qos.logback logback-classic runtime io.micronaut.build micronaut-maven-plugin org.apache.maven.plugins maven-compiler-plugin io.micronaut micronaut-http-validation ${micronaut.version} io.micronaut.security micronaut-security-annotations ${micronaut.security.version} -Amicronaut.processing.group=com.acme.backend.micronaut -Amicronaut.processing.module=backend-api-micronaut ================================================ FILE: apps/backend-api-micronaut/src/main/java/com/acme/backend/micronaut/Application.java ================================================ package com.acme.backend.micronaut; import io.micronaut.runtime.Micronaut; public class Application { public static void main(String[] args) { Micronaut.run(Application.class, args); } } ================================================ FILE: apps/backend-api-micronaut/src/main/java/com/acme/backend/micronaut/api/UsersResource.java ================================================ package com.acme.backend.micronaut.api; import io.micronaut.http.HttpRequest; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.security.annotation.Secured; import io.micronaut.security.authentication.Authentication; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Instant; import java.util.HashMap; import java.util.Map; import static io.micronaut.security.rules.SecurityRule.IS_AUTHENTICATED; @Secured(IS_AUTHENTICATED) @Controller("/api/users") class UsersResource { private static final Logger log = LoggerFactory.getLogger(UsersResource.class); @Get("/me") public Object me(HttpRequest request, Authentication authentication) { log.info("### Accessing {}", request.getUri()); Object username = authentication.getName(); Map data = new HashMap<>(); data.put("message", "Hello " + username); data.put("backend", "Micronaut"); data.put("datetime", Instant.now()); return data; } } ================================================ FILE: apps/backend-api-micronaut/src/main/resources/application.yml ================================================ micronaut: application: name: backendApiMicronaut ssl: enabled: true keyStore: path: file:config/stage/dev/tls/acme.test+1.p12 # (1) password: changeit # (2) type: PKCS12 security: authentication: bearer token: name-key: "preferred_username" jwt: signatures: jwks: keycloak: url: "${micronaut.security.token.jwt.claims-validators.issuer}/protocol/openid-connect/certs" claims-validators: issuer: "https://id.acme.test:8443/auth/realms/acme-internal" expiration: true subject-not-null: true # audience: "" server: ssl: port: 4953 cors: enabled: true configurations: web: allowedOrigins: - https://apps.acme.test:4443 netty: default: allocator: max-order: 3 ================================================ FILE: apps/backend-api-micronaut/src/main/resources/logback.xml ================================================ true %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n ================================================ FILE: apps/backend-api-node-express/package.json ================================================ { "name": "backend-api-node-express", "version": "1.0.0", "main": "src/index.js", "author": "Thomas Darimont", "license": "MIT", "dependencies": { "cors": "^2.8.5", "es6-promisify": "^7.0.0", "express": "^4.17.1", "express-jwt": "^6.1.0", "https": "^1.0.0", "jwks-rsa": "^2.0.4", "spdy": "^4.0.2", "stoppable": "^1.1.0", "winston": "^3.3.3" }, "type": "module", "scripts": { "start": "node src/index.js", "dev": "nodemon src/index.js" }, "devDependencies": { "nodemon": "^2.0.12" } } ================================================ FILE: apps/backend-api-node-express/readme.md ================================================ Acme Backend API Node Express --- # Setup Add rootCA for self-signed certificates - required for fetching public keys from JWKS endpoint in Keycloak. ``` export NODE_EXTRA_CA_CERTS=$(mkcert -CAROOT)/rootCA.pem ``` # Build ``` yarn install ``` # Run ``` yarn run start ``` ================================================ FILE: apps/backend-api-node-express/src/api.js ================================================ /** * Initializes the API endpoints * @param app * @param LOG */ function createApiEndpoints(app, config, LOG) { LOG.info('Create API endpoints'); // API routes can then access JWT claims in the request object via request.user app.get('/api/users/me', (req, res) => { let username = req.user.preferred_username; LOG.info(`### Accessing ${req.path}`); const data = { datetime: new Date().toISOString(), message: `Hello ${username}`, backend: 'NodeJS Express', }; res.status(200).send(JSON.stringify(data)); }); } export default createApiEndpoints; ================================================ FILE: apps/backend-api-node-express/src/config.js ================================================ const ISSUER = process.env.ISSUER || "https://id.acme.test:8443/auth/realms/acme-internal"; const PORT = process.env.PORT || 4743; const CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || 'https://apps.acme.test:4443'; // * or https://domain1:4443,https://domain2:4443 const CORS_ALLOWED_METHODS = process.env.CORS_ALLOWED_METHODS || 'GET'; // or GET,POST,PUT const TLS_CERT = process.env.TLS_CERT || '../../config/stage/dev/tls/acme.test+1.pem'; const TLS_KEY = process.env.TLS_KEY || '../../config/stage/dev/tls/acme.test+1-key.pem'; const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; const LOG_FORMAT = process.env.LOG_FORMAT || 'json'; // plain / json // see https://github.com/RisingStack/kubernetes-graceful-shutdown-example/blob/master/src/index.js const READINESS_PROBE_DELAY = process.env.READINESS_PROBE_DELAY || 1000; // 2 * 2 * 1000; // failureThreshold: 2, periodSeconds: 2 (4s) export default { ISSUER, PORT, CORS_ALLOWED_METHODS, CORS_ALLOWED_ORIGINS, TLS_CERT, TLS_KEY, LOG_LEVEL, LOG_FORMAT, READINESS_PROBE_DELAY, }; ================================================ FILE: apps/backend-api-node-express/src/express.js ================================================ import express from "express"; import cors from "cors"; import jwksRsa from "jwks-rsa"; import jwt from "express-jwt"; function createExpressApp(config, LOG) { LOG.info("Create express app"); const app = express(); configureCors(app, config, LOG); configureJwtAuthorization(app, config, LOG); return app; } function configureCors(app, config, LOG) { LOG.info("Configure CORS"); const corsOptions = { origin: config.CORS_ALLOWED_ORIGINS.split(","), methods: config.CORS_ALLOWED_METHODS.split(","), optionsSuccessStatus: 200 // For legacy browser support }; app.use(cors(corsOptions)); } function configureJwtAuthorization(app, config, LOG) { LOG.info("Configure JWT Authorization"); // JWT Bearer Authorization let jwtOptions = { // Dynamically provide a signing key based on the kid in the header and the signing keys provided by the JWKS endpoint. secret: jwksRsa.expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: `${config.ISSUER}/protocol/openid-connect/certs`, handleSigningKeyError: (err, cb) => { if (err instanceof jwksRsa.SigningKeyNotFoundError) { return cb(new Error('Could not fetch certs from JWKS endpoint.')); } return cb(err); } }), // Validate the audience. // audience: 'urn:my-resource-server', // Validate the issuer. issuer: config.ISSUER, algorithms: ['RS256'] }; app.use('/api/*', jwt(jwtOptions)); } export default createExpressApp; ================================================ FILE: apps/backend-api-node-express/src/index.js ================================================ 'use strict' import config from './config.js'; import initLogging from './logging.js'; import createExpressApp from './express.js'; import createApiEndpoints from './api.js'; import createServer from "./server.js"; const LOG = initLogging(config); const app = createExpressApp(config, LOG); createApiEndpoints(app, config, LOG); createServer(app, config, LOG); ================================================ FILE: apps/backend-api-node-express/src/logging.js ================================================ import winston from "winston"; function initLogging(config) { const loggingFormat = winston.format.combine( winston.format.timestamp(), 'json' === config.LOG_FORMAT ? winston.format.json() : winston.format.simple() ); return winston.createLogger({ level: config.LOG_LEVEL, format: loggingFormat, defaultMeta: {service: 'acme-backend-api'}, transports: [ new winston.transports.Console(), // // - Write all logs with level `error` and below to `error.log` // - Write all logs with level `info` and below to `combined.log` // // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], }); } export default initLogging; ================================================ FILE: apps/backend-api-node-express/src/server.js ================================================ import fs from "fs"; import stoppable from "stoppable"; import {promisify} from "es6-promisify"; import spdy from "spdy"; function createServer(app, config, LOG) { LOG.info("Create server"); const httpsServer = spdy.createServer({ key: fs.readFileSync(config.TLS_KEY), cert: fs.readFileSync(config.TLS_CERT), }, app); // for Graceful shutdown see https://github.com/RisingStack/kubernetes-graceful-shutdown-example configureGracefulShutdown(httpsServer, config, LOG); // Start server httpsServer.listen(config.PORT, () => { LOG.info(`Listening on HTTPS port ${config.PORT}`); }); } function configureGracefulShutdown(httpsServer, config, LOG) { // Keep-alive connections doesn't let the server to close in time // Destroy extension helps to force close connections // Because we wait READINESS_PROBE_DELAY, we expect that all requests are fulfilled // https://en.wikipedia.org/wiki/HTTP_persistent_connection stoppable(httpsServer); const serverDestroy = promisify(httpsServer.stop.bind(httpsServer)); // Graceful stop async function gracefulStop() { LOG.info('Server is shutting down...') try { await serverDestroy(); // close server first (ongoing requests) LOG.info('Successful graceful shutdown'); process.exit(0); // exit with ok code } catch (err) { LOG.error('Error happened during graceful shutdown', err) process.exit(1) // exit with not ok code } } // Support graceful shutdown // do not accept more request and release resources process.on('SIGTERM', () => { LOG.info('Got SIGTERM. Graceful shutdown start'); // Wait a little bit to give enough time for Kubernetes readiness probe to fail (we don't want more traffic) // Don't worry livenessProbe won't kill it until (failureThreshold: 3) => 30s // http://www.bite-code.com/2015/07/27/implementing-graceful-shutdown-for-docker-containers-in-go-part-2/ setTimeout(gracefulStop, config.READINESS_PROBE_DELAY); }); } export default createServer; ================================================ FILE: apps/backend-api-quarkus/.dockerignore ================================================ * !target/*-runner !target/*-runner.jar !target/lib/* !target/quarkus-app/* ================================================ FILE: apps/backend-api-quarkus/.gitignore ================================================ #Maven target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup release.properties # Eclipse .project .classpath .settings/ bin/ # IntelliJ .idea *.ipr *.iml *.iws # NetBeans nb-configuration.xml # Visual Studio Code .vscode .factorypath # OSX .DS_Store # Vim *.swp *.swo # patch *.orig *.rej # Local environment .env ================================================ FILE: apps/backend-api-quarkus/README.md ================================================ backend-api-quarkus project --- Simple backend API example that can be access with an Access-Token from the oidc-js-spa application. This project uses Quarkus, the Supersonic Subatomic Java Framework. If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . ## Running the application in dev mode You can run your application in dev mode that enables live coding using: ```shell script mvn compile quarkus:dev ``` > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. ## Packaging and running the application The application can be packaged using: ```shell script mvn package ``` It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. If you want to build an _über-jar_, execute the following command: ```shell script mvn package -Dquarkus.package.type=uber-jar ``` The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. ## Creating a native executable You can create a native executable using: ```shell script mvn package -Pnative ``` Or, if you don't have GraalVM installed, you can run the native executable build in a container using: ```shell script mvn package -Pnative -Dquarkus.native.container-build=true ``` You can then execute your native executable with: `./target/backend-api-quarkus-1.0-SNAPSHOT-runner` If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling.html . ## Related guides - RESTEasy JAX-RS ([guide](https://quarkus.io/guides/rest-json)): REST endpoint framework implementing JAX-RS and more ## Provided examples ### RESTEasy JAX-RS example REST is easy peasy with this Hello World RESTEasy resource. [Related guide section...](https://quarkus.io/guides/getting-started#the-jax-rs-resources) ================================================ FILE: apps/backend-api-quarkus/pom.xml ================================================ 4.0.0 com.github.thomasdarimont.apps backend-api-quarkus 1.0-SNAPSHOT Acme Backend API Quarkus 3.11.0 17 UTF-8 UTF-8 quarkus-bom io.quarkus.platform 3.8.3 true 3.1.2 ${quarkus.platform.group-id} ${quarkus.platform.artifact-id} ${quarkus.platform.version} pom import io.quarkus quarkus-resteasy io.quarkus quarkus-resteasy-jackson io.quarkus quarkus-smallrye-jwt io.quarkus quarkus-arc io.quarkus quarkus-junit5 test io.rest-assured rest-assured test ${quarkus.platform.group-id} quarkus-maven-plugin ${quarkus.platform.version} true build generate-code generate-code-tests maven-compiler-plugin ${compiler-plugin.version} -parameters maven-surefire-plugin ${surefire-plugin.version} org.jboss.logmanager.LogManager ${maven.home} maven-failsafe-plugin ${surefire-plugin.version} integration-test verify ${project.build.directory}/${project.build.finalName}-runner org.jboss.logmanager.LogManager ${maven.home} native native false native ================================================ FILE: apps/backend-api-quarkus/src/main/docker/Dockerfile.jvm ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode # # Before building the container image run: # # ./mvnw package # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.jvm -t quarkus/quarkus242test-jvm . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/quarkus242test-jvm # # If you want to include the debug port into your docker image # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 # # Then run the container using : # # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus242test-jvm # ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # Install java and the run-java script # Also set up permissions for user `1001` RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ && microdnf update \ && microdnf clean all \ && mkdir /deployments \ && chown 1001 /deployments \ && chmod "g+rwX" /deployments \ && chown 1001:root /deployments \ && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ && chown 1001 /deployments/run-java.sh \ && chmod 540 /deployments/run-java.sh \ && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" # We make four distinct layers so if there are application changes the library layers can be re-used COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/ COPY --chown=1001 target/quarkus-app/*.jar /deployments/ COPY --chown=1001 target/quarkus-app/app/ /deployments/app/ COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/ EXPOSE 8080 USER 1001 ENTRYPOINT [ "/deployments/run-java.sh" ] ================================================ FILE: apps/backend-api-quarkus/src/main/docker/Dockerfile.legacy-jar ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode # # Before building the container image run: # # ./mvnw package -Dquarkus.package.type=legacy-jar # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/quarkus242test-legacy-jar . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/quarkus242test-legacy-jar # # If you want to include the debug port into your docker image # you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 # # Then run the container using : # # docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" quarkus/quarkus242test-legacy-jar # ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 ARG JAVA_PACKAGE=java-11-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' # Install java and the run-java script # Also set up permissions for user `1001` RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \ && microdnf update \ && microdnf clean all \ && mkdir /deployments \ && chown 1001 /deployments \ && chmod "g+rwX" /deployments \ && chown 1001:root /deployments \ && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \ && chown 1001 /deployments/run-java.sh \ && chmod 540 /deployments/run-java.sh \ && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security # Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size. ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" COPY target/lib/* /deployments/lib/ COPY target/*-runner.jar /deployments/app.jar EXPOSE 8080 USER 1001 ENTRYPOINT [ "/deployments/run-java.sh" ] ================================================ FILE: apps/backend-api-quarkus/src/main/docker/Dockerfile.native ================================================ #### # This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode # # Before building the container image run: # # ./mvnw package -Pnative # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.native -t quarkus/quarkus242test . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/quarkus242test # ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ && chown 1001:root /work COPY --chown=1001:root target/*-runner /work/application EXPOSE 8080 USER 1001 CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] ================================================ FILE: apps/backend-api-quarkus/src/main/docker/Dockerfile.native-distroless ================================================ #### # This Dockerfile is used in order to build a distroless container that runs the Quarkus application in native (no JVM) mode # # Before building the container image run: # # ./mvnw package -Pnative # # Then, build the image with: # # docker build -f src/main/docker/Dockerfile.native-distroless -t quarkus/quarkus242test . # # Then run the container using: # # docker run -i --rm -p 8080:8080 quarkus/quarkus242test # ### FROM quay.io/quarkus/quarkus-distroless-image:1.0 COPY target/*-runner /application EXPOSE 8080 USER nonroot CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] ================================================ FILE: apps/backend-api-quarkus/src/main/java/com/acme/backend/quarkus/users/UsersResource.java ================================================ package com.acme.backend.quarkus.users; import io.quarkus.security.Authenticated; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import java.time.Instant; import java.util.HashMap; import java.util.Map; @Path("/api/users") @Produces(MediaType.APPLICATION_JSON) @Authenticated public class UsersResource { @Inject Logger log; @Inject JsonWebToken jwt; @Context SecurityContext securityContext; @Context UriInfo uriInfo; @Context HttpRequest request; @GET @Path("/me") public Object me() { log.infof("### Accessing %s", uriInfo.getPath()); // Note in order to have role information in the token, you need to add the microprofile-jwt scope // to the token to populate the groups claim with the realm roles. // securityContext.isUserInRole("admin"); //Object username = jwt.getClaim("preferred_username"); String username = securityContext.getUserPrincipal().getName(); Map data = new HashMap<>(); data.put("message", "Hello " + username); data.put("backend", "Quarkus"); data.put("datetime", Instant.now()); return data; } @GET @RolesAllowed("iam") // require 'iam' present in groups claim list @Path("/claims") public Object claims( @QueryParam("issuer") String issuer, @QueryParam("clientId") String clientId, @QueryParam("userId") String userId, @QueryParam("username") String username ) { log.infof("### Generating dynamic claims for user. issuer=%s client_id=%s user_id=%s username=%s", issuer, clientId, userId, username ); var acmeData = new HashMap(); acmeData.put("hello", "world"); return Map.of("acme", acmeData); } } ================================================ FILE: apps/backend-api-quarkus/src/main/resources/META-INF/resources/index.html ================================================ backend-api-quarkus - 1.0-SNAPSHOT

Congratulations, you have created a new Quarkus cloud application.

What is this page?

This page is served by Quarkus. The source is in src/main/resources/META-INF/resources/index.html.

What are your next steps?

If not already done, run the application in dev mode using: ./mvnw compile quarkus:dev.

  • Your static assets are located in src/main/resources/META-INF/resources.
  • Configure your application in src/main/resources/application.properties.
  • Quarkus now ships with a Dev UI (available in dev mode only)
  • Play with the getting started example code located in src/main/java:

RESTEasy JAX-RS example

REST is easy peasy with this Hello World RESTEasy resource.

@Path: /hello

Related guide section...

Application

  • GroupId: com.github.thomasdarimont.apps
  • ArtifactId: backend-api-quarkus
  • Version: 1.0-SNAPSHOT
  • Quarkus Version: 1.13.7.Final

Do you like Quarkus?

  • Go give it a star on GitHub.

Selected extensions guides

================================================ FILE: apps/backend-api-quarkus/src/main/resources/application.properties ================================================ quarkus.http.port=4500 # Allows access via host.docker.internal from container quarkus.http.host=0.0.0.0 quarkus.http.ssl-port=4543 quarkus.http.ssl.certificate.files=../../config/stage/dev/tls/acme.test+1.pem quarkus.http.ssl.certificate.key-files=../../config/stage/dev/tls/acme.test+1-key.pem quarkus.http.cors=true quarkus.http.cors.origins=https://apps.acme.test:4443 quarkus.http.cors.headers=accept, origin, authorization, content-type, x-requested-with quarkus.http.cors.methods=GET,POST,OPTIONS quarkus.log.category."io.smallrye.jwt.auth.principal".level=DEBUG # Note: If you need to fetch the certificate info from an https JWKS uri, then # you need to import the certificate of the acme.issuer.uri into your JVM truststore, e.g: # ~/.sdkman/candidates/java/11.0.11.hs-adpt/bin/keytool -import -noprompt -cacerts -storepass changeit -file config/stage/dev/tls/acme.test+1.pem # see https://quarkus.io/guides/security-jwt mp.jwt.verify.publickey.location=${acme.issuer.uri}/protocol/openid-connect/certs mp.jwt.verify.publickey.algorithm=RS256 mp.jwt.verify.issuer=${acme.issuer.uri} acme.issuer.uri=https://id.acme.test:8443/auth/realms/acme-internal # see https://stackoverflow.com/questions/63347673/quarkus-native-image-load-a-pkcs12-file-at-runtime quarkus.native.enable-all-security-services=true quarkus.package.type=fast-jar ================================================ FILE: apps/backend-api-rust-actix/.gitignore ================================================ /target ================================================ FILE: apps/backend-api-rust-actix/Cargo.toml ================================================ [package] name = "backend-api-rust-actix" version = "0.1.0" edition = "2021" authors = ["Thomas Darimont "] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] actix-web = { version = "4.3.1", features = ["openssl"] } actix-cors = "0.6.4" actix-4-jwt-auth = "0.4.2" # TODO update to 1.0.0 openssl = "0.10.48" serde = { version = "1.0.159", features = ["derive"] } serde_json = { version = "1.0.95" } chrono = "0.4.24" env_logger = "0.10.0" [dev-dependencies] # cargo +nightly watch --quiet --clear --exec run cargo-watch = "8.4.0" ================================================ FILE: apps/backend-api-rust-actix/rustfmt.toml ================================================ max_width = 120 ================================================ FILE: apps/backend-api-rust-actix/rustup-toolchain.toml ================================================ [toolchain] channel = "nightly" ================================================ FILE: apps/backend-api-rust-actix/src/api/me_info.rs ================================================ use crate::middleware::jwt_auth::FoundClaims; use actix_4_jwt_auth::AuthenticatedUser; use actix_web::{get, HttpResponse}; use chrono::Utc; use serde::Serialize; #[derive(Serialize)] struct MeInfo { pub message: String, pub backend: String, pub datetime: String, } #[derive(Serialize)] struct ErrorInfo { pub code: String, } #[get("/api/users/me")] pub async fn handle_me_info(user: AuthenticatedUser) -> HttpResponse { if !user.claims.has_scope("email") { return HttpResponse::Forbidden().json(ErrorInfo { code: "invalid_scope".into(), }); } let username = user.claims.preferred_username.unwrap_or("anonymous".into()); let obj = MeInfo { message: format!("Hello, {}!", username), backend: "rust-actix".into(), datetime: Utc::now().to_string(), }; HttpResponse::Ok().json(obj) } ================================================ FILE: apps/backend-api-rust-actix/src/api/mod.rs ================================================ pub mod me_info; ================================================ FILE: apps/backend-api-rust-actix/src/config.rs ================================================ use std::env; pub struct Config { pub server_bind_addr: String, pub cert_location: String, pub key_location: String, pub oidc_issuer: String, pub allowed_cors_origin: String, pub log_level_default: String, } impl Config { pub fn from_environment_with_defaults() -> Self { Self { server_bind_addr: env::var("SERVER_BIND_ADDRESS").unwrap_or("127.0.0.1:4863".into()), cert_location: env::var("CERT_LOCATION").unwrap_or("../../config/stage/dev/tls/acme.test+1.pem".into()), key_location: env::var("KEY_LOCATION").unwrap_or("../../config/stage/dev/tls/acme.test+1-key.pem".into()), oidc_issuer: env::var("OIDC_ISSUER") .unwrap_or("https://id.acme.test:8443/auth/realms/acme-internal".into()), allowed_cors_origin: env::var("ALLOWED_CORS_ORIGIN").unwrap_or("https://apps.acme.test:4443".into()), log_level_default: env::var("LOG_LEVEL_DEFAULT").unwrap_or("info".into()), } } } ================================================ FILE: apps/backend-api-rust-actix/src/main.rs ================================================ #![feature(decl_macro)] use actix_web::middleware::Logger; use actix_web::{App, HttpServer}; mod api; mod config; mod middleware; #[actix_web::main] async fn main() -> std::io::Result<()> { let config = config::Config::from_environment_with_defaults(); env_logger::init_from_env(env_logger::Env::new().default_filter_or(&config.log_level_default)); let ssl_acceptor_builder = middleware::ssl::create_ssl_acceptor_builder(&config.cert_location, &config.key_location); let oidc_jwt_validator = middleware::jwt_auth::create_oidc_jwt_validator(config.oidc_issuer).await; HttpServer::new(move || { let cors = middleware::cors::create_cors_config(config.allowed_cors_origin.clone()); App::new() // see https://actix.rs/actix-web/actix_web/middleware/struct.Logger.html .wrap(Logger::new("%a \"%r\" %s %b \"%{Referer}i\" %T")) .wrap(cors) .app_data(oidc_jwt_validator.clone()) .service(api::me_info::handle_me_info) }) .bind_openssl(config.server_bind_addr, ssl_acceptor_builder)? .run() .await } ================================================ FILE: apps/backend-api-rust-actix/src/middleware/cors.rs ================================================ use actix_cors::Cors; use actix_web::http::header; pub fn create_cors_config(allowed_origin: String) -> Cors { Cors::default() .allowed_origin_fn(move |header, _request| { return header.as_bytes().ends_with(allowed_origin.as_bytes()); }) .allowed_methods(vec!["GET", "POST"]) .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT]) .allowed_header(header::CONTENT_TYPE) .supports_credentials() .max_age(3600) } ================================================ FILE: apps/backend-api-rust-actix/src/middleware/jwt_auth.rs ================================================ use actix_4_jwt_auth::{OIDCValidator, OIDCValidatorConfig}; use actix_web::rt::task; use serde::Deserialize; use serde_json::Value; use std::collections::BTreeMap as Map; #[derive(Debug, Deserialize)] pub struct FoundClaims { pub iat: usize, pub exp: usize, pub iss: String, pub sub: String, pub scope: String, pub preferred_username: Option, #[serde(flatten)] pub other: Map, } impl FoundClaims { pub fn has_scope(&self, scope: &str) -> bool { self.scope.split_ascii_whitespace().any(|s| s == scope) } } pub async fn create_oidc_jwt_validator(issuer: String) -> OIDCValidatorConfig { task::spawn_blocking(move || { let validator = OIDCValidator::new_from_issuer(issuer.clone()).unwrap(); OIDCValidatorConfig { issuer, validator } }) .await .unwrap() } ================================================ FILE: apps/backend-api-rust-actix/src/middleware/mod.rs ================================================ pub mod cors; pub mod jwt_auth; pub mod ssl; ================================================ FILE: apps/backend-api-rust-actix/src/middleware/ssl.rs ================================================ use openssl::ssl::{SslAcceptor, SslAcceptorBuilder, SslFiletype, SslMethod}; pub fn create_ssl_acceptor_builder(cert_location: &str, key_location: &str) -> SslAcceptorBuilder { let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); builder.set_private_key_file(key_location, SslFiletype::PEM).unwrap(); builder.set_certificate_chain_file(cert_location).unwrap(); builder } ================================================ FILE: apps/backend-api-rust-rocket/.gitignore ================================================ /target .idea/ .DS_Store ================================================ FILE: apps/backend-api-rust-rocket/.run/Run backend-api-rust-rocket.run.xml ================================================ ================================================ FILE: apps/backend-api-rust-rocket/Cargo.toml ================================================ [package] name = "backend-api-rust-rocket" version = "0.1.0" authors = ["Thomas Darimont "] edition = "2021" [dependencies] serde = { version ="1.0.157", features = ["derive"]} lazy_static = "1.4.0" log = "0.4.17" env_logger = "0.10.0" chrono = "0.4.24" jsonwebtoken = "8.3.0" rocket = { version = "0.5.0-rc.2", features = ["tls", "json", "serde_json"] } reqwest = { version = "0.11.14", features = ["blocking", "json"] } [dev-dependencies] cargo-watch = "8.4.0" ================================================ FILE: apps/backend-api-rust-rocket/README.md ================================================ # Backend API with JWK authentication based on Rocket (Rust) ## Features - Validate JWT issued by Keycloak - Validate JWT with JWK from JWKS endpoint - Periodically fetch a JWKS Keyset from Keycloak - Extract claims from JWT ## Run ``` ROCKET_PROFILE=debug cargo run ``` Browse to: https://127.0.0.1:4853 This example is inspired by [maylukas/rust_jwk_example](https://github.com/maylukas/rust_jwk_example) ================================================ FILE: apps/backend-api-rust-rocket/Rocket.toml ================================================ # see https://rocket.rs/v0.5-rc/guide/configuration/#overview [debug] address = "127.0.0.1" port = 4853 workers = 2 keep_alive = 5 log_level = "normal" limits = { forms = 32768 } [debug.tls] certs = "../../config/stage/dev/tls/acme.test+1.pem" key = "../../config/stage/dev/tls/acme.test+1-key.pem" ================================================ FILE: apps/backend-api-rust-rocket/rustfmt.toml ================================================ edition = "2021" ================================================ FILE: apps/backend-api-rust-rocket/rustup-toolchain.toml ================================================ [toolchain] channel = "nightly" ================================================ FILE: apps/backend-api-rust-rocket/src/domain/mod.rs ================================================ pub mod user; ================================================ FILE: apps/backend-api-rust-rocket/src/domain/user.rs ================================================ pub struct User { pub uid: String, pub username: String, pub email: String, // TODO add other claims // TODO add access token } ================================================ FILE: apps/backend-api-rust-rocket/src/main.rs ================================================ //#![feature(proc_macro_hygiene, decl_macro)] #[macro_use] extern crate rocket; use rocket::routes; use std::error::Error; use crate::domain::user::User; use crate::middleware::auth::jwt::JwtAuth; use chrono::Utc; use rocket::serde::json::Json; use rocket::tokio::task::spawn_blocking; use serde::Deserialize; use serde::Serialize; use crate::middleware::logging; pub mod domain; pub mod middleware; pub mod support; #[derive(Serialize, Deserialize)] pub struct MeInfo { pub message: String, pub backend: String, pub datetime: String, } #[options("/api/users/me")] fn options_me_info() {} #[get("/api/users/me")] fn get_me_info(user: User) -> Json { log::info!("Handle user info request. username={}", &user.username); let info = MeInfo { datetime: Utc::now().to_string(), message: format!("Hello, {}!", user.username), backend: String::from("rust-rocket"), }; Json(info) } #[rocket::main] async fn main() -> Result<(), Box> { logging::init_logging(); let auth = spawn_blocking(JwtAuth::new).await?; let _ = rocket::build() .attach(middleware::cors::Cors) .mount("/", routes![get_me_info, options_me_info]) .manage(auth) .launch() .await?; Ok(()) } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/auth.rs ================================================ use crate::middleware::auth::jwt::{fetch_jwks_keys, Claims, JwkKeys, JwtVerifier}; use crate::support::scheduling::use_repeating_job; use jsonwebtoken::TokenData; use std::sync::{Arc, Mutex}; use std::time::Duration; use log; type CleanupFn = Box; pub struct JwtAuth { verifier: Arc>, cleanup: Mutex, } impl Drop for JwtAuth { fn drop(&mut self) { // Stop the update thread when the updater is destructed let cleanup_fn = self.cleanup.lock().unwrap(); cleanup_fn(); } } impl JwtAuth { pub fn new() -> JwtAuth { let jwk_key_result = fetch_jwks_keys(); let jwk_keys: JwkKeys = match jwk_key_result { Ok(keys) => keys, Err(_) => { panic!("Unable to fetch jwt keys! Cannot verify user tokens! Shutting down...") } }; let verifier = Arc::new(Mutex::new(JwtVerifier::new(jwk_keys.keys))); let mut instance = JwtAuth { verifier, cleanup: Mutex::new(Box::new(|| {})), }; instance.start_key_update(); instance } pub fn verify(&self, token: &str) -> Option> { let verifier = self.verifier.lock().unwrap(); verifier.verify(token) } fn start_key_update(&mut self) { let verifier_ref = Arc::clone(&self.verifier); let stop = use_repeating_job(move || match fetch_jwks_keys() { Ok(jwk_keys) => { let mut verifier = verifier_ref.lock().unwrap(); verifier.set_keys(jwk_keys.keys); log::info!("Updated JWK keys. Next refresh will be in {:?}", jwk_keys.validity); jwk_keys.validity } Err(_) => Duration::from_secs(10), }); let mut cleanup = self.cleanup.lock().unwrap(); *cleanup = stop; } } impl Default for JwtAuth { fn default() -> Self { Self::new() } } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/claims.rs ================================================ use rocket::serde::json::serde_json; use std::collections::HashMap; pub type Claims = HashMap; ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/config.rs ================================================ use crate::middleware; #[derive(Debug)] pub struct JwtConfig { pub jwk_url: String, pub audience: String, // TODO ADD support for multiple issuers via HashSet pub issuer: String, } pub fn get_config() -> JwtConfig { JwtConfig { jwk_url: middleware::expect_env_var( "JWK_URL", "https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/certs", ), audience: middleware::expect_env_var("JWK_AUDIENCE", "app-minispa"), issuer: middleware::expect_env_var( "JWK_ISSUER", "https://id.acme.test:8443/auth/realms/acme-internal", ), } } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/get_max_age.rs ================================================ use reqwest::blocking::Response; use reqwest::header::HeaderValue; use std::time::Duration; pub enum MaxAgeParseError { NoMaxAgeSpecified, NoCacheControlHeader, MaxAgeValueEmpty, NonNumericMaxAge, } // Determines the max age of an HTTP response pub fn get_max_age(response: &Response) -> Result { let headers = response.headers(); let header = headers.get("Cache-Control"); match header { Some(header_value) => parse_cache_control_header(header_value), None => Err(MaxAgeParseError::NoCacheControlHeader), } } fn parse_max_age_value(cache_control_value: &str) -> Result { let tokens: Vec<&str> = cache_control_value.split(',').collect(); for token in tokens { let key_value: Vec<&str> = token.split('=').map(|s| s.trim()).collect(); let key = key_value.first().unwrap(); let val = key_value.get(1); if String::from("max-age").eq(&key.to_lowercase()) { match val { Some(value) => { return Ok(Duration::from_secs( value .parse() .map_err(|_| MaxAgeParseError::NonNumericMaxAge)?, )) } None => return Err(MaxAgeParseError::MaxAgeValueEmpty), } } } Err(MaxAgeParseError::NoMaxAgeSpecified) } fn parse_cache_control_header(header_value: &HeaderValue) -> Result { match header_value.to_str() { Ok(string_value) => parse_max_age_value(string_value), Err(_) => Err(MaxAgeParseError::NoCacheControlHeader), } } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/jwks.rs ================================================ use crate::middleware::auth::jwt; use crate::middleware::auth::jwt::get_max_age::get_max_age; use crate::middleware::auth::jwt::JwtConfig; use serde::Deserialize; use std::error::Error; use std::time::Duration; #[derive(Debug, Deserialize)] struct KeyResponse { keys: Vec, } #[derive(Debug, Deserialize, Eq, PartialEq)] pub struct JwkKey { pub e: String, pub alg: String, pub kty: String, pub kid: String, pub n: String, } #[derive(Debug, Deserialize, Eq, PartialEq)] pub struct JwkKeys { pub keys: Vec, pub validity: Duration, } // TODO make JWKS fetch FALLBACK_TIMEOUT configurable const FALLBACK_TIMEOUT: Duration = Duration::from_secs(300); pub fn fetch_keys_for_config(config: &JwtConfig) -> Result> { log::info!("Fetching JWKS Keys from URL={}", &config.jwk_url); let http_response = reqwest::blocking::get::<>(&config.jwk_url).unwrap(); let max_age = get_max_age(&http_response).unwrap_or(FALLBACK_TIMEOUT); let result = Ok(http_response.json::().unwrap()); result.map(|res| JwkKeys { keys: res.keys, validity: max_age, }) } pub fn fetch_jwks_keys() -> Result> { return fetch_keys_for_config(&jwt::get_config()); } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/mod.rs ================================================ mod auth; mod claims; mod config; mod jwks; mod get_max_age; mod verifier; pub use auth::*; pub use claims::*; pub use config::*; pub use jwks::*; pub use verifier::*; ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt/verifier.rs ================================================ use crate::middleware::auth::jwt; use crate::middleware::auth::jwt::claims::Claims; use crate::middleware::auth::jwt::{JwkKey, JwtConfig}; use jsonwebtoken::decode_header; use jsonwebtoken::TokenData; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use std::collections::HashMap; use std::str::FromStr; enum VerificationError { InvalidSignature, UnknownKeyAlgorithm, } #[derive(Debug)] pub struct JwtVerifier { keys: HashMap, config: JwtConfig, } fn keys_to_map(keys: Vec) -> HashMap { let mut keys_as_map = HashMap::new(); for key in keys { keys_as_map.insert(String::clone(&key.kid), key); } keys_as_map } impl JwtVerifier { pub fn new(keys: Vec) -> JwtVerifier { JwtVerifier { keys: keys_to_map(keys), config: jwt::get_config(), } } pub fn verify(&self, token: &str) -> Option> { let token_kid = match decode_header(token).map(|header| header.kid) { Ok(Some(header)) => header, _ => return None, }; let jwk_key = match self.get_key(token_kid) { Some(key) => key, _ => return None, }; match self.decode_token_with_key(jwk_key, token) { Ok(token_data) => Some(token_data), _ => None, } } pub fn set_keys(&mut self, keys: Vec) { self.keys = keys_to_map(keys); } fn get_key(&self, key_id: String) -> Option<&JwkKey> { self.keys.get(&key_id) } fn decode_token_with_key( &self, key: &JwkKey, token: &str, ) -> Result, VerificationError> { // TODO ensure that "none" algorithm cannot be used! let algorithm = match Algorithm::from_str(&key.alg) { Ok(alg) => alg, Err(_error) => return Err(VerificationError::UnknownKeyAlgorithm), }; let mut validation = Validation::new(algorithm); // TODO make audience validation configurable (enable / disable) // TODO make allowed audience configurable // validation.set_audience(&[&self.middleware.audience]); // TODO adapt to support multiple issuers let mut issuers = std::collections::HashSet::new(); issuers.insert(self.config.issuer.clone()); validation.iss = Some(issuers); let key = DecodingKey::from_rsa_components(&key.n, &key.e).unwrap(); decode::(token, &key, &validation) .map_err(|_| VerificationError::InvalidSignature) } } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/jwt_auth_request_guard.rs ================================================ use crate::domain::user::User; use crate::middleware::auth::jwt::JwtAuth; use rocket::http::Status; use rocket::outcome::Outcome; use rocket::request; use rocket::request::FromRequest; use rocket::Request; use rocket::State; #[derive(Debug)] pub enum AuthError { InvalidJwt, NoAuthorizationHeader, MultipleKeysProvided, NoJwkVerifier, } fn get_token_from_header(header: &str) -> Option { let prefix_len = "Bearer ".len(); match header.len() { l if l < prefix_len => None, _ => Some(header[prefix_len..].to_string()), } } fn verify_token(token: &str, auth: &JwtAuth) -> request::Outcome { let verified_token = auth.verify(token); // TODO externalize claims to JWT User conversion let maybe_user = verified_token.map(|token| User { // TODO use more idiomatic value conversion here uid: token .claims .get("sub") .unwrap() .as_str() .unwrap() .to_string(), username: token .claims .get("preferred_username") .unwrap() .as_str() .unwrap() .to_string(), email: token .claims .get("email") .unwrap() .as_str() .unwrap() .to_string(), }); match maybe_user { Some(user) => Outcome::Success(user), None => Outcome::Failure((Status::BadRequest, AuthError::InvalidJwt)), } } fn parse_and_verify_auth_header(header: &str, auth: &JwtAuth) -> request::Outcome { let maybe_token = get_token_from_header(header); match maybe_token { Some(token) => verify_token(&token, auth), None => Outcome::Failure((Status::Unauthorized, AuthError::InvalidJwt)), } } #[rocket::async_trait] impl<'r> FromRequest<'r> for User { type Error = AuthError; async fn from_request(request: &'r Request<'_>) -> request::Outcome { let auth_headers: Vec<_> = request.headers().get("Authorization").collect(); let configured_auth = request.guard::<&'r State>(); match configured_auth.await { Outcome::Success(auth) => match auth_headers.len() { 0 => Outcome::Failure((Status::Unauthorized, AuthError::NoAuthorizationHeader)), 1 => parse_and_verify_auth_header(auth_headers[0], auth), _ => Outcome::Failure((Status::BadRequest, AuthError::MultipleKeysProvided)), }, _ => Outcome::Failure((Status::InternalServerError, AuthError::NoJwkVerifier)), } } } #[cfg(test)] mod describe { #[test] fn test_extract_token() { let token = super::get_token_from_header("Bearer token_string"); assert_eq!(Some("token_string".to_string()), token) } #[test] fn test_extract_token_too_short() { assert_eq!(None, super::get_token_from_header(&"Bear".to_string())); assert_eq!(None, super::get_token_from_header(&"Bearer".to_string())) } } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/auth/mod.rs ================================================ pub mod jwt; mod jwt_auth_request_guard; pub use jwt_auth_request_guard::*; ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/cors/cors.rs ================================================ use rocket::fairing::Fairing; use rocket::fairing::Info; use rocket::fairing::Kind; use rocket::http::Header; use rocket::Request; use rocket::Response; pub struct Cors; #[rocket::async_trait] impl Fairing for Cors { fn info(&self) -> Info { Info { name: "Attaching CORS headers to responses", kind: Kind::Response, } } async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) { // TODO make cors configurable response.set_header(Header::new("Access-Control-Allow-Origin", "*")); response.set_header(Header::new( "Access-Control-Allow-Methods", "POST, GET, PATCH, OPTIONS", )); response.set_header(Header::new("Access-Control-Allow-Headers", "*")); response.set_header(Header::new("Access-Control-Allow-Credentials", "true")); } } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/cors/mod.rs ================================================ pub mod cors; pub use cors::*; ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/logging/logging.rs ================================================ use chrono::Local; use env_logger::Builder; use log; use log::LevelFilter; use std::io::Write; pub fn init_logging() { Builder::new() .format(|buf, record| { writeln!(buf, "{} [{}] - {}", Local::now().format("%Y-%m-%dT%H:%M:%S"), record.level(), record.args() ) }) .filter(None, LevelFilter::Info) .init(); } ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/logging/mod.rs ================================================ pub mod logging; pub use logging::*; ================================================ FILE: apps/backend-api-rust-rocket/src/middleware/mod.rs ================================================ use std::env; pub mod auth; pub mod cors; pub mod logging; #[cfg(debug_assertions)] pub fn expect_env_var(name: &str, default: &str) -> String { env::var(name).unwrap_or(String::from(default)) } #[cfg(not(debug_assertions))] pub fn expect_env_var(name: &str, _default: &str) -> String { return env::var(name).expect(&format!( "Environment variable {name} is not defined", name = name )); } ================================================ FILE: apps/backend-api-rust-rocket/src/support/mod.rs ================================================ pub mod scheduling; ================================================ FILE: apps/backend-api-rust-rocket/src/support/scheduling/mod.rs ================================================ pub mod use_repeating_job; pub use use_repeating_job::*; ================================================ FILE: apps/backend-api-rust-rocket/src/support/scheduling/use_repeating_job.rs ================================================ use std::sync::mpsc::{self, TryRecvError}; use std::thread; use std::time::Duration; type Delay = Duration; type Cancel = Box; // Runs a given closure as a repeating job until the cancel callback is invoked. // The jobs are run with a delay returned by the closure execution. pub fn use_repeating_job(job: F) -> Cancel where F: Fn() -> Delay, F: Send + 'static, { let (shutdown_tx, shutdown_rx) = mpsc::channel(); thread::spawn(move || loop { let delay = job(); thread::sleep(delay); if let Ok(_) | Err(TryRecvError::Disconnected) = shutdown_rx.try_recv() { break; } }); Box::new(move || { println!("Stopping..."); let _ = shutdown_tx.send("stop"); }) } ================================================ FILE: apps/backend-api-rust-rocket/tests/fetch_keys.rs ================================================ use jwk_example::fetch_keys_for_config; use jwk_example::JwkConfiguration; use jwk_example::JwkKey; fn assert_is_valid_key(key: &JwkKey) { assert!(key.kid.len() > 0); assert!(key.n.len() > 0); assert!(key.e.len() > 0); assert!(key.kty.len() > 0); assert!(key.alg.len() > 0); } #[test] fn test_fetch_keys() { let config = JwkConfiguration { jwk_url: String::from("https://www.googleapis.com/service_accounts/v1/jwt/securetoken@system.gserviceaccount.com"), audience: String::from("tracking-app-dev-271418"), issuer: String::from("https://securetoken.google.com/tracking-app-dev-271418") }; let result = fetch_keys_for_config(&config).expect("Did not fetch keys"); assert_eq!(2, result.keys.len()); assert_is_valid_key(result.keys.get(0).expect("")); assert_is_valid_key(result.keys.get(1).expect("")); } ================================================ FILE: apps/backend-api-springboot/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/backend-api-springboot/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: apps/backend-api-springboot/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: apps/backend-api-springboot/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/backend-api-springboot/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: apps/backend-api-springboot/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.18 com.example backend-api-springboot 0.0.1-SNAPSHOT backend-api-springboot backend-api-springboot 21 1.18.38 org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/BackendApiSpringbootApp.java ================================================ package com.acme.backend.springboot.users; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication @ConfigurationPropertiesScan public class BackendApiSpringbootApp { public static void main(String[] args) { SpringApplication.run(BackendApiSpringbootApp.class, args); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/AcmeServiceProperties.java ================================================ package com.acme.backend.springboot.users.config; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Getter @Setter @Component @ConfigurationProperties(prefix = "acme") public class AcmeServiceProperties { private KeycloakJwtProperties jwt = new KeycloakJwtProperties(); /** * Specifies JWT client ID, issuer URI and allowed audiences * for validation */ @Getter @Setter public static class KeycloakJwtProperties { private String clientId; private String issuerUri; private List allowedAudiences; } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/JwtSecurityConfig.java ================================================ package com.acme.backend.springboot.users.config; import com.acme.backend.springboot.users.support.keycloak.KeycloakGrantedAuthoritiesConverter; import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; /** * Configures JWT handling (decoder and validator) */ @Configuration class JwtSecurityConfig { /** * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint) * * @param validators validators for the given key * @param properties key properties (provides JWK location) * @return the decoder bean */ @Bean JwtDecoder jwtDecoder(List> validators, OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder // .withJwkSetUri(properties.getJwt().getJwkSetUri()) // .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256))) // .build(); jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return jwtDecoder; } /** * Configures the token validator. Specifies two additional validation constraints: *

* * Timestamp on the token is still valid * * The issuer is the expected entity * * @param properties JWT resource specification * @return token validator */ @Bean OAuth2TokenValidator defaultTokenValidator(OAuth2ResourceServerProperties properties) { List> validators = new ArrayList<>(); validators.add(new JwtTimestampValidator()); validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri())); return new DelegatingOAuth2TokenValidator<>(validators); } @Bean KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter> authoritiesConverter) { return new KeycloakJwtAuthenticationConverter(authoritiesConverter); } @Bean Converter> keycloakGrantedAuthoritiesConverter(GrantedAuthoritiesMapper authoritiesMapper, AcmeServiceProperties acmeServiceProperties) { String clientId = acmeServiceProperties.getJwt().getClientId(); return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/MethodSecurityConfig.java ================================================ package com.acme.backend.springboot.users.config; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; /** * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method. */ @Configuration @RequiredArgsConstructor @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true) class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { private final ApplicationContext applicationContext; private final PermissionEvaluator permissionEvaluator; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); expressionHandler.setApplicationContext(applicationContext); expressionHandler.setPermissionEvaluator(permissionEvaluator); return expressionHandler; } @Bean GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper; } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java ================================================ package com.acme.backend.springboot.users.config; import com.acme.backend.springboot.users.support.access.AccessController; import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.CorsConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; /** * Configuration applied on all web endpoints defined for this * application. Any configuration on specific resources is applied * in addition to these global rules. */ @Configuration @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; /** * Configures basic security handler per HTTP session. *

*

    *
  • Stateless session (no session kept server-side)
  • *
  • CORS set up
  • *
  • Require the role "ACCESS" for all api paths
  • *
  • JWT converted into Spring token
  • *
* * @param http security configuration * @throws Exception any error */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.sessionManagement(smc -> { smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS); }); http.cors(this::configureCors); http.authorizeRequests(arc -> { // declarative route configuration // .mvcMatchers("/api").hasAuthority("ROLE_ACCESS") arc.mvcMatchers("/api/**").access("@accessController.checkAccess()"); // add additional routes arc.anyRequest().fullyAuthenticated(); // }); http.oauth2ResourceServer(arsc -> { arsc.jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter); }); return http.build(); } @Bean AccessController accessController() { return new AccessController(); } /** * Configures CORS to allow requests from localhost:30000 * * @param cors mutable cors configuration */ protected void configureCors(CorsConfigurer cors) { UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues(); corsConfiguration.addAllowedOrigin("https://apps.acme.test:4443"); List.of("GET", "POST", "PUT", "DELETE").forEach(corsConfiguration::addAllowedMethod); defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/api/**", corsConfiguration); cors.configurationSource(req -> { CorsConfiguration config = new CorsConfiguration(); config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req)); // check if request Header "origin" is in white-list -> dynamically generate cors config return config; }); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java ================================================ package com.acme.backend.springboot.users.support.access; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; /** * Example for generic custom access checks on request level. */ @Slf4j public class AccessController { public boolean checkAccess() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); log.info("Check access for username={} path={}", auth.getName(), requestAttributes.getRequest().getRequestURI()); return true; } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakAudienceValidator.java ================================================ package com.acme.backend.springboot.users.support.keycloak; import lombok.RequiredArgsConstructor; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; /** * Example class for custom audience (aud) or authorized party (azp) claim validations. */ @Component @RequiredArgsConstructor class KeycloakAudienceValidator implements OAuth2TokenValidator { private final OAuth2Error ERROR_INVALID_AUDIENCE = new OAuth2Error("invalid_token", "Invalid audience", null); @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { // String authorizedParty = jwt.getClaimAsString("azp"); // // if (!keycloakDataServiceProperties.getJwt().getAllowedAudiences().contains(authorizedParty)) { // return OAuth2TokenValidatorResult.failure(ERROR_INVALID_AUDIENCE); // } return OAuth2TokenValidatorResult.success(); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakGrantedAuthoritiesConverter.java ================================================ package com.acme.backend.springboot.users.support.keycloak; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Allows to extract granted authorities from a given JWT. The authorities * are determined by combining the realm (overarching) and client (application-specific) * roles, and normalizing them (configure them to the default format). */ public class KeycloakGrantedAuthoritiesConverter implements Converter> { private static final Converter> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter(); private final String clientId; private final GrantedAuthoritiesMapper authoritiesMapper; public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) { this.clientId = clientId; this.authoritiesMapper = authoritiesMapper; } @Override public Collection convert(Jwt jwt) { Collection authorities = mapKeycloakRolesToAuthorities( // getRealmRolesFrom(jwt), // getClientRolesFrom(jwt, clientId) // ); Collection scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt); if(!CollectionUtils.isEmpty(scopeAuthorities)) { authorities.addAll(scopeAuthorities); } return authorities; } protected Collection mapKeycloakRolesToAuthorities(Set realmRoles, Set clientRoles) { List combinedAuthorities = new ArrayList<>(); combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()))); combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()))); return combinedAuthorities; } protected Set getRealmRolesFrom(Jwt jwt) { Map realmAccess = jwt.getClaimAsMap("realm_access"); if (CollectionUtils.isEmpty(realmAccess)) { return Collections.emptySet(); } @SuppressWarnings("unchecked") Collection realmRoles = (Collection) realmAccess.get("roles"); if (CollectionUtils.isEmpty(realmRoles)) { return Collections.emptySet(); } return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); } protected Set getClientRolesFrom(Jwt jwt, String clientId) { Map resourceAccess = jwt.getClaimAsMap("resource_access"); if (CollectionUtils.isEmpty(resourceAccess)) { return Collections.emptySet(); } @SuppressWarnings("unchecked") Map> clientAccess = (Map>) resourceAccess.get(clientId); if (CollectionUtils.isEmpty(clientAccess)) { return Collections.emptySet(); } List clientRoles = clientAccess.get("roles"); if (CollectionUtils.isEmpty(clientRoles)) { return Collections.emptySet(); } return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); } private String normalizeRole(String role) { return role.replace('-', '_'); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakJwtAuthenticationConverter.java ================================================ package com.acme.backend.springboot.users.support.keycloak; import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import java.util.Collection; /** * Converts a JWT into a Spring authentication token (by extracting * the username and roles from the claims of the token, delegating * to the {@link KeycloakGrantedAuthoritiesConverter}) */ @RequiredArgsConstructor public class KeycloakJwtAuthenticationConverter implements Converter { private Converter> grantedAuthoritiesConverter; public KeycloakJwtAuthenticationConverter(Converter> grantedAuthoritiesConverter) { this.grantedAuthoritiesConverter = grantedAuthoritiesConverter; } @Override public JwtAuthenticationToken convert(Jwt jwt) { Collection authorities = grantedAuthoritiesConverter.convert(jwt); String username = getUsernameFrom(jwt); return new JwtAuthenticationToken(jwt, authorities, username); } protected String getUsernameFrom(Jwt jwt) { if (jwt.hasClaim("preferred_username")) { return jwt.getClaimAsString("preferred_username"); } return jwt.getSubject(); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/permissions/DefaultPermissionEvaluator.java ================================================ package com.acme.backend.springboot.users.support.permissions; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.io.Serializable; /** * Custom {@link PermissionEvaluator} for method level permission checks. * * @see com.acme.backend.springboot.users.config.MethodSecurityConfig */ @Slf4j @Component @RequiredArgsConstructor class DefaultPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { log.info("check permission user={} target={} permission={}", auth.getName(), targetDomainObject, permission); // TODO implement sophisticated permission check here return true; } @Override public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) { DomainObjectReference dor = new DomainObjectReference(targetType, targetId.toString()); return hasPermission(auth, dor, permission); } } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/support/permissions/DomainObjectReference.java ================================================ package com.acme.backend.springboot.users.support.permissions; import lombok.Data; /** * Defines a single domain object by a type and name to look up */ @Data public class DomainObjectReference { private final String type; private final String id; } ================================================ FILE: apps/backend-api-springboot/src/main/java/com/acme/backend/springboot/users/web/UsersController.java ================================================ package com.acme.backend.springboot.users.web; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.ServletWebRequest; import java.time.Instant; import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/users") class UsersController { @GetMapping("/me") public Object me(ServletWebRequest request, Authentication authentication) { log.info("### Accessing {}", request.getRequest().getRequestURI()); Object username = authentication.getName(); Map data = new HashMap<>(); data.put("message", "Hello " + username); data.put("backend", "Spring Boot"); data.put("datetime", Instant.now()); return data; } } ================================================ FILE: apps/backend-api-springboot/src/main/resources/application.yml ================================================ spring: jackson: serialization: write-dates-as-timestamps: false deserialization: # deals with single and multi-valued JWT claims accept-single-value-as-array: true security: oauth2: resourceserver: jwt: issuer-uri: ${acme.jwt.issuerUri} jwk-set-uri: ${acme.jwt.issuerUri}/protocol/openid-connect/certs # Use mock-service jwks-endpoint to obtain public key for testing # jwk-set-uri: http://localhost:9999/jwks acme: jwt: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal server: port: 4643 ssl: enabled: true key-store: ../../config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 error: include-stacktrace: never include-exception: false include-message: never ================================================ FILE: apps/backend-api-springboot/src/test/java/com/acme/backend/springboot/users/BackendApiSpringbootAppTests.java ================================================ package com.acme.backend.springboot.users; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class BackendApiSpringbootAppTests { @Test void contextLoads() { } } ================================================ FILE: apps/backend-api-springboot-reactive/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/backend-api-springboot-reactive/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar ================================================ FILE: apps/backend-api-springboot-reactive/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /usr/local/etc/mavenrc ] ; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`\\unset -f command; \\command -v java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" else jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/backend-api-springboot-reactive/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: apps/backend-api-springboot-reactive/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.14 com.example backend-api-springboot-reactive 0.0.1-SNAPSHOT backend-api-springboot-reactive backend-api-springboot-reactive 17 1.18.38 org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test io.projectreactor reactor-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/BackendApiSpringbootReactiveApp.java ================================================ package com.acme.backend.springreactive; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BackendApiSpringbootReactiveApp { public static void main(String[] args) { SpringApplication.run(BackendApiSpringbootReactiveApp.class, args); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/AcmeServiceProperties.java ================================================ package com.acme.backend.springreactive.config; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Getter @Setter @Component @ConfigurationProperties(prefix = "acme") public class AcmeServiceProperties { private KeycloakJwtProperties jwt = new KeycloakJwtProperties(); /** * Specifies JWT client ID, issuer URI and allowed audiences * for validation */ @Getter @Setter public static class KeycloakJwtProperties { private String clientId; private String issuerUri; private List allowedAudiences; } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/JwtSecurityConfig.java ================================================ package com.acme.backend.springreactive.config; import com.acme.backend.springreactive.support.keycloak.KeycloakGrantedAuthoritiesConverter; import com.acme.backend.springreactive.support.keycloak.KeycloakJwtAuthenticationConverter; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; /** * Configures JWT handling (decoder and validator) */ @Configuration class JwtSecurityConfig { /** * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint) * * @param validators validators for the given key * @param properties key properties (provides JWK location) * @return the decoder bean */ @Bean ReactiveJwtDecoder jwtDecoder(List> validators, OAuth2ResourceServerProperties properties) { NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder .withJwkSetUri(properties.getJwt().getJwkSetUri()) // .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256))) .build(); jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return jwtDecoder; } /** * Configures the token validator. Specifies two additional validation constraints: *

* * Timestamp on the token is still valid * * The issuer is the expected entity * * @param properties JWT resource specification * @return token validator */ @Bean OAuth2TokenValidator defaultTokenValidator(OAuth2ResourceServerProperties properties) { List> validators = new ArrayList<>(); validators.add(new JwtTimestampValidator()); validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri())); return new DelegatingOAuth2TokenValidator<>(validators); } @Bean KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter> authoritiesConverter) { return new KeycloakJwtAuthenticationConverter(authoritiesConverter); } @Bean Converter> keycloakGrantedAuthoritiesConverter(GrantedAuthoritiesMapper authoritiesMapper, AcmeServiceProperties acmeServiceProperties) { String clientId = acmeServiceProperties.getJwt().getClientId(); return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/MethodSecurityConfig.java ================================================ package com.acme.backend.springreactive.config; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; /** * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method. */ @Configuration @RequiredArgsConstructor @EnableReactiveMethodSecurity class MethodSecurityConfig { @Bean GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper; } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/WebFluxConfig.java ================================================ package com.acme.backend.springreactive.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.config.CorsRegistry; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.WebFluxConfigurer; @Configuration @EnableWebFlux class WebFluxConfig implements WebFluxConfigurer { @Override public void addCorsMappings(CorsRegistry corsRegistry) { corsRegistry.addMapping("/api/**") .allowedOrigins("https://apps.acme.test:4443") .allowedMethods("GET", "POST", "PUT", "DELETE") .maxAge(3600); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/WebFluxRoutes.java ================================================ package com.acme.backend.springreactive.config; import com.acme.backend.springreactive.users.UserHandlers; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; @Configuration class WebFluxRoutes { @Bean public RouterFunction route(UserHandlers userHandlers) { return RouterFunctions.route( // GET("/api/users/me").and(accept(MediaType.APPLICATION_JSON)), userHandlers::me) // ; } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/config/WebSecurityConfig.java ================================================ package com.acme.backend.springreactive.config; import com.acme.backend.springreactive.support.keycloak.KeycloakJwtAuthenticationConverter; import lombok.RequiredArgsConstructor; import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest; import org.springframework.boot.autoconfigure.security.reactive.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; /** * Configuration applied on all web endpoints defined for this * application. Any configuration on specific resources is applied * in addition to these global rules. */ @EnableWebFluxSecurity @EnableReactiveMethodSecurity @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; /** * Configures basic security handler per HTTP session. *

*

    *
  • JWT converted into Spring token
  • *
* * @param http security configuration */ @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http // csrf disabled for testing .csrf() // .disable() // .authorizeExchange() // // CORS requests .pathMatchers(HttpMethod.OPTIONS, "/api/**") // .permitAll() // .matchers(PathRequest.toStaticResources().atCommonLocations()) // .permitAll() // .matchers(EndpointRequest.to("health")) // .permitAll() // .matchers(EndpointRequest.to("info")) // .permitAll().matchers(EndpointRequest.toAnyEndpoint()) // .permitAll() // .anyExchange() // .authenticated() // .and() // // Enable OAuth2 Resource Server Support .oauth2ResourceServer() // // Enable custom JWT handling .jwt().jwtAuthenticationConverter(keycloakJwtAuthenticationConverter) // ; return http.build(); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/support/keycloak/KeycloakAudienceValidator.java ================================================ package com.acme.backend.springreactive.support.keycloak; import lombok.RequiredArgsConstructor; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; /** * Example class for custom audience (aud) or authorized party (azp) claim validations. */ @Component @RequiredArgsConstructor class KeycloakAudienceValidator implements OAuth2TokenValidator { private final OAuth2Error ERROR_INVALID_AUDIENCE = new OAuth2Error("invalid_token", "Invalid audience", null); @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { // String authorizedParty = jwt.getClaimAsString("azp"); // // if (!keycloakDataServiceProperties.getJwt().getAllowedAudiences().contains(authorizedParty)) { // return OAuth2TokenValidatorResult.failure(ERROR_INVALID_AUDIENCE); // } return OAuth2TokenValidatorResult.success(); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/support/keycloak/KeycloakGrantedAuthoritiesConverter.java ================================================ package com.acme.backend.springreactive.support.keycloak; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Allows to extract granted authorities from a given JWT. The authorities * are determined by combining the realm (overarching) and client (application-specific) * roles, and normalizing them (configure them to the default format). */ public class KeycloakGrantedAuthoritiesConverter implements Converter> { private static final Converter> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter(); private final String clientId; private final GrantedAuthoritiesMapper authoritiesMapper; public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) { this.clientId = clientId; this.authoritiesMapper = authoritiesMapper; } @Override public Collection convert(Jwt jwt) { Collection authorities = mapKeycloakRolesToAuthorities( // getRealmRolesFrom(jwt), // getClientRolesFrom(jwt, clientId) // ); Collection scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt); if(!CollectionUtils.isEmpty(scopeAuthorities)) { authorities.addAll(scopeAuthorities); } return authorities; } protected Collection mapKeycloakRolesToAuthorities(Set realmRoles, Set clientRoles) { List combinedAuthorities = new ArrayList<>(); combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()))); combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()))); return combinedAuthorities; } protected Set getRealmRolesFrom(Jwt jwt) { Map realmAccess = jwt.getClaimAsMap("realm_access"); if (CollectionUtils.isEmpty(realmAccess)) { return Collections.emptySet(); } @SuppressWarnings("unchecked") Collection realmRoles = (Collection) realmAccess.get("roles"); if (CollectionUtils.isEmpty(realmRoles)) { return Collections.emptySet(); } return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); } protected Set getClientRolesFrom(Jwt jwt, String clientId) { Map resourceAccess = jwt.getClaimAsMap("resource_access"); if (CollectionUtils.isEmpty(resourceAccess)) { return Collections.emptySet(); } @SuppressWarnings("unchecked") Map> clientAccess = (Map>) resourceAccess.get(clientId); if (CollectionUtils.isEmpty(clientAccess)) { return Collections.emptySet(); } List clientRoles = clientAccess.get("roles"); if (CollectionUtils.isEmpty(clientRoles)) { return Collections.emptySet(); } return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); } private String normalizeRole(String role) { return role.replace('-', '_'); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/support/keycloak/KeycloakJwtAuthenticationConverter.java ================================================ package com.acme.backend.springreactive.support.keycloak; import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import reactor.core.publisher.Mono; import java.util.Collection; /** * Converts a JWT into a Spring authentication token (by extracting * the username and roles from the claims of the token, delegating * to the {@link KeycloakGrantedAuthoritiesConverter}) */ @RequiredArgsConstructor public class KeycloakJwtAuthenticationConverter implements Converter> { private Converter> grantedAuthoritiesConverter; public KeycloakJwtAuthenticationConverter(Converter> grantedAuthoritiesConverter) { this.grantedAuthoritiesConverter = grantedAuthoritiesConverter; } @Override public Mono convert(Jwt jwt) { Collection authorities = grantedAuthoritiesConverter.convert(jwt); String username = getUsernameFrom(jwt); return Mono.just(new JwtAuthenticationToken(jwt, authorities, username)); } protected String getUsernameFrom(Jwt jwt) { if (jwt.hasClaim("preferred_username")) { return jwt.getClaimAsString("preferred_username"); } return jwt.getSubject(); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/java/com/acme/backend/springreactive/users/UserHandlers.java ================================================ package com.acme.backend.springreactive.users; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; import java.time.Instant; import java.util.HashMap; @Slf4j @Component public class UserHandlers { public Mono me(ServerRequest request) { log.info("### Accessing {}", request.uri()); return request.principal().flatMap(auth -> { var username = auth.getName(); var data = new HashMap(); data.put("message", "Hello " + username); data.put("backend", "Spring Boot Reactive"); data.put("datetime", Instant.now()); return ServerResponse.ok().bodyValue(data); }); } } ================================================ FILE: apps/backend-api-springboot-reactive/src/main/resources/application.yml ================================================ spring: jackson: serialization: write-dates-as-timestamps: false deserialization: # deals with single and multi-valued JWT claims accept-single-value-as-array: true security: oauth2: resourceserver: jwt: issuer-uri: ${acme.jwt.issuerUri} jwk-set-uri: ${acme.jwt.issuerUri}/protocol/openid-connect/certs # Use mock-service jwks-endpoint to obtain public key for testing # jwk-set-uri: http://localhost:9999/jwks acme: jwt: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal server: port: 4943 ssl: enabled: true key-store: ../../config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 ================================================ FILE: apps/backend-api-springboot-reactive/src/test/java/com/acme/backend/springreactive/BackendApiSpringbootReactiveAppTests.java ================================================ package com.acme.backend.springreactive; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class BackendApiSpringbootReactiveAppTests { @Test void contextLoads() { } } ================================================ FILE: apps/backend-api-springboot3/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/backend-api-springboot3/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: apps/backend-api-springboot3/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: apps/backend-api-springboot3/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/backend-api-springboot3/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: apps/backend-api-springboot3/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.4.7 com.example backend-api-springboot3 0.0.1-SNAPSHOT backend-api-springboot3 backend-api-springboot3 17 org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin spring-milestones Spring Milestones https://repo.spring.io/milestone false spring-milestones Spring Milestones https://repo.spring.io/milestone false ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/BackendApiSpringboot3App.java ================================================ package com.acme.backend.springboot.users; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication @ConfigurationPropertiesScan public class BackendApiSpringboot3App { public static void main(String[] args) { SpringApplication.run(BackendApiSpringboot3App.class, args); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/AcmeServiceProperties.java ================================================ package com.acme.backend.springboot.users.config; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Getter @Setter @Component @ConfigurationProperties(prefix = "acme") public class AcmeServiceProperties { private KeycloakJwtProperties jwt = new KeycloakJwtProperties(); /** * Specifies JWT client ID, issuer URI and allowed audiences * for validation */ @Getter @Setter public static class KeycloakJwtProperties { private String clientId; private String issuerUri; private List allowedAudiences; } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/JwtSecurityConfig.java ================================================ package com.acme.backend.springboot.users.config; import com.acme.backend.springboot.users.support.keycloak.KeycloakGrantedAuthoritiesConverter; import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.JwtTimestampValidator; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; /** * Configures JWT handling (decoder and validator) */ @Configuration class JwtSecurityConfig { /** * Configures a decoder with the specified validators (validation key fetched from JWKS endpoint) * * @param validators validators for the given key * @param properties key properties (provides JWK location) * @return the decoder bean */ @Bean JwtDecoder jwtDecoder(List> validators, OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder // .withJwkSetUri(properties.getJwt().getJwkSetUri()) // .jwsAlgorithms(algs -> algs.addAll(Set.of(SignatureAlgorithm.RS256, SignatureAlgorithm.ES256))) // .build(); jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return jwtDecoder; } /** * Configures the token validator. Specifies two additional validation constraints: *

* * Timestamp on the token is still valid * * The issuer is the expected entity * * @param properties JWT resource specification * @return token validator */ @Bean OAuth2TokenValidator defaultTokenValidator(OAuth2ResourceServerProperties properties) { List> validators = new ArrayList<>(); validators.add(new JwtTimestampValidator()); validators.add(new JwtIssuerValidator(properties.getJwt().getIssuerUri())); return new DelegatingOAuth2TokenValidator<>(validators); } @Bean KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter(Converter> authoritiesConverter) { return new KeycloakJwtAuthenticationConverter(authoritiesConverter); } @Bean Converter> keycloakGrantedAuthoritiesConverter(GrantedAuthoritiesMapper authoritiesMapper, AcmeServiceProperties acmeServiceProperties) { String clientId = acmeServiceProperties.getJwt().getClientId(); return new KeycloakGrantedAuthoritiesConverter(clientId, authoritiesMapper); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/MethodSecurityConfig.java ================================================ package com.acme.backend.springboot.users.config; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; /** * Enables security annotations via like {@link org.springframework.security.access.prepost.PreAuthorize} and * {@link org.springframework.security.access.prepost.PostAuthorize} annotations per-method. */ @Configuration @RequiredArgsConstructor @EnableMethodSecurity class MethodSecurityConfig { private final ApplicationContext applicationContext; private final PermissionEvaluator permissionEvaluator; @Bean MethodSecurityExpressionHandler customMethodSecurityExpressionHandler() { var expressionHandler = new DefaultMethodSecurityExpressionHandler(); expressionHandler.setApplicationContext(applicationContext); expressionHandler.setPermissionEvaluator(permissionEvaluator); return expressionHandler; } @Bean GrantedAuthoritiesMapper keycloakAuthoritiesMapper() { var mapper = new SimpleAuthorityMapper(); mapper.setConvertToUpperCase(true); return mapper; } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/config/WebSecurityConfig.java ================================================ package com.acme.backend.springboot.users.config; import com.acme.backend.springboot.users.support.access.AccessController; import com.acme.backend.springboot.users.support.keycloak.KeycloakJwtAuthenticationConverter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.CorsConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; /** * Configuration applied on all web endpoints defined for this * application. Any configuration on specific resources is applied * in addition to these global rules. */ @Configuration @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakJwtAuthenticationConverter keycloakJwtAuthenticationConverter; /** * Configures basic security handler per HTTP session. *

*

    *
  • Stateless session (no session kept server-side)
  • *
  • CORS set up
  • *
  • Require the role "ACCESS" for all api paths
  • *
  • JWT converted into Spring token
  • *
* * @param http security configuration * @throws Exception any error */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.sessionManagement(smc -> { smc.sessionCreationPolicy(SessionCreationPolicy.STATELESS); }); http.cors(this::configureCors); http.authorizeHttpRequests(ahrc -> { // declarative route configuration // .mvcMatchers("/api").hasAuthority("ROLE_ACCESS") ahrc.requestMatchers("/api/**").access(AccessController::checkAccess); // add additional routes ahrc.anyRequest().fullyAuthenticated(); // }); http.oauth2ResourceServer(arsc -> { arsc.jwt(jc -> { jc.jwtAuthenticationConverter(keycloakJwtAuthenticationConverter); }); }); return http.build(); } @Bean AccessController accessController() { return new AccessController(); } /** * Configures CORS to allow requests from localhost:30000 * * @param cors mutable cors configuration */ protected void configureCors(CorsConfigurer cors) { UrlBasedCorsConfigurationSource defaultUrlBasedCorsConfigSource = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues(); corsConfiguration.addAllowedOrigin("https://apps.acme.test:4443"); List.of("GET", "POST", "PUT", "DELETE").forEach(corsConfiguration::addAllowedMethod); defaultUrlBasedCorsConfigSource.registerCorsConfiguration("/api/**", corsConfiguration); cors.configurationSource(req -> { CorsConfiguration config = new CorsConfiguration(); config = config.combine(defaultUrlBasedCorsConfigSource.getCorsConfiguration(req)); // check if request Header "origin" is in white-list -> dynamically generate cors config return config; }); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/access/AccessController.java ================================================ package com.acme.backend.springboot.users.support.access; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.core.Authentication; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import java.util.function.Supplier; /** * Example for generic custom access checks on request level. */ @Slf4j public class AccessController { private static final AuthorizationDecision GRANTED = new AuthorizationDecision(true); private static final AuthorizationDecision DENIED = new AuthorizationDecision(false); public static AuthorizationDecision checkAccess(Supplier authentication, RequestAuthorizationContext requestContext) { var auth = authentication.get(); log.info("Check access for username={} path={}", auth.getName(), requestContext.getRequest().getRequestURI()); return GRANTED; } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakAudienceValidator.java ================================================ package com.acme.backend.springboot.users.support.keycloak; import lombok.RequiredArgsConstructor; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; /** * Example class for custom audience (aud) or authorized party (azp) claim validations. */ @Component @RequiredArgsConstructor class KeycloakAudienceValidator implements OAuth2TokenValidator { private final OAuth2Error ERROR_INVALID_AUDIENCE = new OAuth2Error("invalid_token", "Invalid audience", null); @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { // String authorizedParty = jwt.getClaimAsString("azp"); // // if (!keycloakDataServiceProperties.getJwt().getAllowedAudiences().contains(authorizedParty)) { // return OAuth2TokenValidatorResult.failure(ERROR_INVALID_AUDIENCE); // } return OAuth2TokenValidatorResult.success(); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakGrantedAuthoritiesConverter.java ================================================ package com.acme.backend.springboot.users.support.keycloak; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * Allows to extract granted authorities from a given JWT. The authorities * are determined by combining the realm (overarching) and client (application-specific) * roles, and normalizing them (configure them to the default format). */ public class KeycloakGrantedAuthoritiesConverter implements Converter> { private static final Converter> JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER = new JwtGrantedAuthoritiesConverter(); private final String clientId; private final GrantedAuthoritiesMapper authoritiesMapper; public KeycloakGrantedAuthoritiesConverter(String clientId, GrantedAuthoritiesMapper authoritiesMapper) { this.clientId = clientId; this.authoritiesMapper = authoritiesMapper; } @Override public Collection convert(Jwt jwt) { Collection authorities = mapKeycloakRolesToAuthorities( // getRealmRolesFrom(jwt), // getClientRolesFrom(jwt, clientId) // ); Collection scopeAuthorities = JWT_SCOPE_GRANTED_AUTHORITIES_CONVERTER.convert(jwt); if(!CollectionUtils.isEmpty(scopeAuthorities)) { authorities.addAll(scopeAuthorities); } return authorities; } protected Collection mapKeycloakRolesToAuthorities(Set realmRoles, Set clientRoles) { List combinedAuthorities = new ArrayList<>(); combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(realmRoles.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()))); combinedAuthorities.addAll(authoritiesMapper.mapAuthorities(clientRoles.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()))); return combinedAuthorities; } protected Set getRealmRolesFrom(Jwt jwt) { Map realmAccess = jwt.getClaimAsMap("realm_access"); if (CollectionUtils.isEmpty(realmAccess)) { return Collections.emptySet(); } @SuppressWarnings("unchecked") Collection realmRoles = (Collection) realmAccess.get("roles"); if (CollectionUtils.isEmpty(realmRoles)) { return Collections.emptySet(); } return realmRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); } protected Set getClientRolesFrom(Jwt jwt, String clientId) { Map resourceAccess = jwt.getClaimAsMap("resource_access"); if (CollectionUtils.isEmpty(resourceAccess)) { return Collections.emptySet(); } @SuppressWarnings("unchecked") Map> clientAccess = (Map>) resourceAccess.get(clientId); if (CollectionUtils.isEmpty(clientAccess)) { return Collections.emptySet(); } List clientRoles = clientAccess.get("roles"); if (CollectionUtils.isEmpty(clientRoles)) { return Collections.emptySet(); } return clientRoles.stream().map(this::normalizeRole).collect(Collectors.toSet()); } private String normalizeRole(String role) { return role.replace('-', '_'); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/keycloak/KeycloakJwtAuthenticationConverter.java ================================================ package com.acme.backend.springboot.users.support.keycloak; import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import java.util.Collection; /** * Converts a JWT into a Spring authentication token (by extracting * the username and roles from the claims of the token, delegating * to the {@link KeycloakGrantedAuthoritiesConverter}) */ @RequiredArgsConstructor public class KeycloakJwtAuthenticationConverter implements Converter { private Converter> grantedAuthoritiesConverter; public KeycloakJwtAuthenticationConverter(Converter> grantedAuthoritiesConverter) { this.grantedAuthoritiesConverter = grantedAuthoritiesConverter; } @Override public JwtAuthenticationToken convert(Jwt jwt) { Collection authorities = grantedAuthoritiesConverter.convert(jwt); String username = getUsernameFrom(jwt); return new JwtAuthenticationToken(jwt, authorities, username); } protected String getUsernameFrom(Jwt jwt) { if (jwt.hasClaim("preferred_username")) { return jwt.getClaimAsString("preferred_username"); } return jwt.getSubject(); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/permissions/DefaultPermissionEvaluator.java ================================================ package com.acme.backend.springboot.users.support.permissions; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.io.Serializable; /** * Custom {@link PermissionEvaluator} for method level permission checks. * * @see com.acme.backend.springboot.users.config.MethodSecurityConfig */ @Slf4j @Component @RequiredArgsConstructor class DefaultPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { log.info("check permission user={} target={} permission={}", auth.getName(), targetDomainObject, permission); // TODO implement sophisticated permission check here return true; } @Override public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) { DomainObjectReference dor = new DomainObjectReference(targetType, targetId.toString()); return hasPermission(auth, dor, permission); } } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/support/permissions/DomainObjectReference.java ================================================ package com.acme.backend.springboot.users.support.permissions; import lombok.Data; /** * Defines a single domain object by a type and name to look up */ @Data public class DomainObjectReference { private final String type; private final String id; } ================================================ FILE: apps/backend-api-springboot3/src/main/java/com/acme/backend/springboot/users/web/UsersController.java ================================================ package com.acme.backend.springboot.users.web; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.ServletWebRequest; import java.time.Instant; import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/users") class UsersController { @GetMapping("/me") public Object me(ServletWebRequest request, Authentication authentication) { log.info("### Accessing {}", request.getRequest().getRequestURI()); Object username = authentication.getName(); Map data = new HashMap<>(); data.put("message", "Hello " + username); data.put("backend", "Spring Boot 3"); data.put("datetime", Instant.now()); return data; } } ================================================ FILE: apps/backend-api-springboot3/src/main/resources/application.yml ================================================ spring: jackson: serialization: write-dates-as-timestamps: false deserialization: # deals with single and multi-valued JWT claims accept-single-value-as-array: true security: oauth2: resourceserver: jwt: issuer-uri: ${acme.jwt.issuerUri} jwk-set-uri: ${acme.jwt.issuerUri}/protocol/openid-connect/certs # Use mock-service jwks-endpoint to obtain public key for testing # jwk-set-uri: http://localhost:9999/jwks acme: jwt: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal server: port: 4623 ssl: enabled: true key-store: ../../config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 ================================================ FILE: apps/backend-api-springboot3/src/test/java/com/acme/backend/springboot/users/BackendApiSpringboot3AppTests.java ================================================ package com.acme.backend.springboot.users; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class BackendApiSpringboot3AppTests { @Test void contextLoads() { } } ================================================ FILE: apps/bff-springboot/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/bff-springboot/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar ================================================ FILE: apps/bff-springboot/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /usr/local/etc/mavenrc ] ; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`\\unset -f command; \\command -v java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" else jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/bff-springboot/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: apps/bff-springboot/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.14 com.github.thomasdarimont.keycloak bff-springboot 0.0.1-SNAPSHOT bff-springboot bff-springboot 17 1.18.38 org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity5 org.springframework.boot spring-boot-starter-data-redis org.springframework.session spring-session-data-redis io.lettuce lettuce-core org.springframework.boot spring-boot-devtools runtime true io.micrometer micrometer-registry-prometheus runtime org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/BffApp.java ================================================ package com.github.thomasdarimont.apps.bff; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BffApp { public static void main(String[] args) { SpringApplication.run(BffApp.class, args); } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/api/UsersResource.java ================================================ package com.github.thomasdarimont.apps.bff.api; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.LinkedHashMap; import java.util.Map; @RestController @RequestMapping("/api/users") class UsersResource { private final RestTemplate oauthRestTemplate; public UsersResource(@Qualifier("oauth") RestTemplate oauthRestTemplate) { this.oauthRestTemplate = oauthRestTemplate; } @GetMapping("/me") public ResponseEntity userInfo(Authentication auth) { var userInfo = getUserInfoFromAuthority(auth); // var userInfo = getUserInfoFromRemote(); return ResponseEntity.ok(userInfo); } private Map getUserInfoFromAuthority(Authentication auth) { return auth.getAuthorities().stream() // .filter(OidcUserAuthority.class::isInstance) // .map(authority -> (OidcUserAuthority) authority)// .map(OidcUserAuthority::getUserInfo) // .map(OidcUserInfo::getClaims) // .findFirst() // .orElseGet(() -> Map.of("error", "UserInfoMissing")); } private UserInfo getUserInfoFromRemote() { return oauthRestTemplate.getForObject("https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/userinfo", UserInfo.class); } static class UserInfo extends LinkedHashMap { } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/OAuth2RestTemplateConfig.java ================================================ package com.github.thomasdarimont.apps.bff.config; import com.github.thomasdarimont.apps.bff.oauth.TokenAccessor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.web.client.RestTemplate; @Configuration class OAuth2RestTemplateConfig { /** * Provides a {@link RestTemplate} that can obtain access tokes for the current user. * * @param tokenAccessor * @return */ @Bean @Qualifier("oauth") public RestTemplate oauthRestTemplate(TokenAccessor tokenAccessor) { var restTemplate = new RestTemplate(); restTemplate.getInterceptors().add((request, body, execution) -> { var accessToken = tokenAccessor.getAccessTokenForCurrentUser(); if (accessToken == null) { throw new OAuth2AuthenticationException("missing access token"); } var accessTokenValue = accessToken.getTokenValue(); request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + accessTokenValue); return execution.execute(request, body); }); return restTemplate; } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/SessionConfig.java ================================================ package com.github.thomasdarimont.apps.bff.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer; @Configuration @EnableRedisHttpSession public class SessionConfig extends AbstractHttpSessionApplicationInitializer { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/WebSecurityConfig.java ================================================ package com.github.thomasdarimont.apps.bff.config; import com.github.thomasdarimont.apps.bff.config.keycloak.KeycloakLogoutHandler; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import java.util.HashSet; @Configuration @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakLogoutHandler keycloakLogoutHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, AuthorizationRequestRepository authorizationRequestRepository) throws Exception { http.csrf().ignoringAntMatchers("/spa/**").csrfTokenRepository(new CookieCsrfTokenRepository()); // http.sessionManagement(sess -> { // sess.sessionAuthenticationStrategy() // }) http.authorizeRequests(arc -> { // declarative route configuration // add additional routes arc.antMatchers("/app/**", "/webjars/**", "/resources/**", "/css/**").permitAll(); arc.anyRequest().fullyAuthenticated(); }); // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow, // we explicitly enable the PKCE customization here. http.oauth2Client(o2cc -> { var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( // clientRegistrationRepository, // OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI // ); oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); o2cc.authorizationCodeGrant() // .authorizationRequestResolver(oauth2AuthRequestResolver) // .authorizationRequestRepository(authorizationRequestRepository); }); http.oauth2Login(o2lc -> { //o2lc.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper()); }); http.logout(lc -> { lc.addLogoutHandler(keycloakLogoutHandler); }); return http.build(); } @Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); } private GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { var mappedAuthorities = new HashSet(); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { var oidcUserAuthority = (OidcUserAuthority) authority; var userInfo = oidcUserAuthority.getUserInfo(); // TODO extract roles from userInfo response // List groupAuthorities = userInfo.getClaimAsStringList("groups").stream().map(g -> new SimpleGrantedAuthority("ROLE_" + g.toUpperCase())).collect(Collectors.toList()); // mappedAuthorities.addAll(groupAuthorities); } }); return mappedAuthorities; }; } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/config/keycloak/KeycloakLogoutHandler.java ================================================ package com.github.thomasdarimont.apps.bff.config.keycloak; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Component public class KeycloakLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { var principal = (DefaultOidcUser) auth.getPrincipal(); var idToken = principal.getIdToken(); log.info("Propagate logout to keycloak for user. userId={}", idToken.getSubject()); var issuerUri = idToken.getIssuer().toString(); var idTokenValue = idToken.getTokenValue(); var defaultRedirectUri = generateAppUri(request); var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri); try { response.sendRedirect(logoutUrl); } catch (IOException e) { e.printStackTrace(); } } private String generateAppUri(HttpServletRequest request) { var hostname = request.getServerName() + ":" + request.getServerPort(); var isStandardHttps = "https".equals(request.getScheme()) && request.getServerPort() == 443; var isStandardHttp = "http".equals(request.getScheme()) && request.getServerPort() == 80; if (isStandardHttps || isStandardHttp) { hostname = request.getServerName(); } return request.getScheme() + "://" + hostname + request.getContextPath(); } private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) { return issuerUri + "/protocol/openid-connect/logout?id_token_hint=" + idTokenValue + "&post_logout_redirect_uri=" + defaultRedirectUri; } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/oauth/TokenAccessor.java ================================================ package com.github.thomasdarimont.apps.bff.oauth; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.stereotype.Component; import java.time.Duration; import java.time.Instant; /** * Provides access to OAuth2 access- and refresh-tokens of an authenticated user. */ @Slf4j @Getter @Setter @Component @RequiredArgsConstructor public class TokenAccessor { private final OAuth2AuthorizedClientService authorizedClientService; private final TokenRefresher tokenRefresher; private Duration accessTokenExpiresSkew = Duration.ofSeconds(10); private boolean tokenRefreshEnabled = true; public OAuth2AccessToken getAccessTokenForCurrentUser() { return getAccessToken(SecurityContextHolder.getContext().getAuthentication()); } public OAuth2AccessToken getAccessToken(Authentication auth) { var client = getOAuth2AuthorizedClient(auth); if (client == null) { return null; } var accessToken = client.getAccessToken(); if (accessToken == null) { return null; } var accessTokenStillValid = isAccessTokenStillValid(accessToken); if (!accessTokenStillValid && tokenRefreshEnabled) { accessToken = tokenRefresher.refreshTokens(client); } return accessToken; } public OAuth2RefreshToken getRefreshToken(Authentication auth) { OAuth2AuthorizedClient client = getOAuth2AuthorizedClient(auth); if (client == null) { return null; } return client.getRefreshToken(); } private boolean isAccessTokenStillValid(OAuth2AccessToken accessToken) { var expiresAt = accessToken.getExpiresAt(); if (expiresAt == null) { return false; } var exp = expiresAt.minus(accessTokenExpiresSkew == null ? Duration.ofSeconds(0) : accessTokenExpiresSkew); var now = Instant.now(); return now.isBefore(exp); } private OAuth2AuthorizedClient getOAuth2AuthorizedClient(Authentication auth) { var authToken = (OAuth2AuthenticationToken) auth; var clientId = authToken.getAuthorizedClientRegistrationId(); var username = auth.getName(); return authorizedClientService.loadAuthorizedClient(clientId, username); } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/oauth/TokenIntrospector.java ================================================ package com.github.thomasdarimont.apps.bff.oauth; import com.fasterxml.jackson.annotation.JsonAnySetter; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; @Component @RequiredArgsConstructor public class TokenIntrospector { private final OAuth2AuthorizedClientService authorizedClientService; private final TokenAccessor tokenAccessor; public IntrospectionResult introspectToken(Authentication auth) { if (!(auth instanceof OAuth2AuthenticationToken)) { return null; } var authToken = (OAuth2AuthenticationToken) auth; var authorizedClient = authorizedClientService.loadAuthorizedClient( authToken.getAuthorizedClientRegistrationId(), auth.getName() ); if (authorizedClient == null) { return null; } var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", authorizedClient.getClientRegistration().getClientId()); requestBody.add("client_secret", authorizedClient.getClientRegistration().getClientSecret()); var accessToken = tokenAccessor.getAccessToken(auth); requestBody.add("token", accessToken.getTokenValue()); requestBody.add("token_type_hint", "access_token"); var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + "/protocol/openid-connect/token/introspect"; var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class); var responseData = responseEntity.getBody(); if (responseData == null || !responseData.isActive()) { return null; } return responseData; } @Data public static class IntrospectionResult { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/oauth/TokenRefresher.java ================================================ package com.github.thomasdarimont.apps.bff.oauth; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.text.ParseException; import java.util.HashMap; import java.util.Map; /** * Uses the current Oauth2 refresh token of the current user session to obtain new tokens. */ @Slf4j @Component @RequiredArgsConstructor public class TokenRefresher { private final OAuth2AuthorizedClientService authorizedClientService; public OAuth2AccessToken refreshTokens(OAuth2AuthorizedClient client) { var clientRegistration = client.getClientRegistration(); var refreshToken = client.getRefreshToken(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", clientRegistration.getClientId()); requestBody.add("client_secret", clientRegistration.getClientSecret()); requestBody.add("grant_type", "refresh_token"); requestBody.add("refresh_token", refreshToken.getTokenValue()); var rt = new RestTemplate(); var responseEntity = rt.postForEntity(clientRegistration.getProviderDetails().getTokenUri(), new HttpEntity<>(requestBody, headers), AccessTokenResponse.class); if (!responseEntity.getStatusCode().is2xxSuccessful()) { throw new OAuth2AuthenticationException("token refresh failed"); } var accessTokenResponse = responseEntity.getBody(); var newAccessTokenValue = accessTokenResponse.access_token; var newRefreshTokenValue = accessTokenResponse.refresh_token; JWTClaimsSet newAccessTokenClaimsSet; JWTClaimsSet newRefreshTokenClaimSet; try { var newAccessToken = JWTParser.parse(newAccessTokenValue); newAccessTokenClaimsSet = newAccessToken.getJWTClaimsSet(); } catch (ParseException e) { throw new OAuth2AuthenticationException("token refresh failed: could not parse access token"); } try { var newRefreshToken = JWTParser.parse(newRefreshTokenValue); newRefreshTokenClaimSet = newRefreshToken.getJWTClaimsSet(); } catch (ParseException e) { throw new OAuth2AuthenticationException("token refresh failed: could not parse refresh token"); } var accessTokenIat = newAccessTokenClaimsSet.getIssueTime().toInstant(); var accessTokenExp = newAccessTokenClaimsSet.getExpirationTime().toInstant(); var newOAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, newAccessTokenValue, accessTokenIat, accessTokenExp); var refreshTokenIat = newRefreshTokenClaimSet.getIssueTime().toInstant(); var refreshTokenExp = newRefreshTokenClaimSet.getExpirationTime().toInstant(); var newOAuth2RefreshToken = new OAuth2RefreshToken(newRefreshTokenValue, refreshTokenIat, refreshTokenExp); var newClient = new OAuth2AuthorizedClient(clientRegistration, client.getPrincipalName(), newOAuth2AccessToken, newOAuth2RefreshToken); authorizedClientService.saveAuthorizedClient(newClient, SecurityContextHolder.getContext().getAuthentication()); return newOAuth2AccessToken; } @Data static class AccessTokenResponse { final long createdAtSeconds = System.currentTimeMillis() / 1000; String access_token; String refresh_token; String error; int expires_in; Map metadata = new HashMap<>(); @JsonAnySetter public void setMetadata(String key, Object value) { metadata.put(key, value); } } } ================================================ FILE: apps/bff-springboot/src/main/java/com/github/thomasdarimont/apps/bff/web/UiResource.java ================================================ package com.github.thomasdarimont.apps.bff.web; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller @RequiredArgsConstructor class UiResource { @GetMapping("/") public String index(Model model) { model.addAttribute("appScript", "/app/app.js"); return "/app/index"; } } ================================================ FILE: apps/bff-springboot/src/main/resources/application.yml ================================================ server: port: 4693 ssl: enabled: true key-store: config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 servlet: context-path: /bff error: include-stacktrace: never logging: level: root: info org: springframework: web: info spring: thymeleaf: cache: false security: oauth2: client: provider: keycloak: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal user-name-attribute: preferred_username registration: keycloak: client-id: 'acme-bff-springboot' client-secret: 'secret' client-authentication-method: client_secret_post authorizationGrantType: authorization_code redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' scope: openid redis: client-name: "acme-bff" ================================================ FILE: apps/bff-springboot/src/main/resources/static/app/app.js ================================================ let spa = {}; function qs(selector) { return document.querySelector(selector); } function qsa(selector) { return [...document.querySelectorAll(selector)]; } function callApi(url, requestOptions, onError) { let csrfToken = qs("meta[name=_csrf]").content; let csrfTokenHeader = qs("meta[name=_csrf_header]").content; let requestData = { timeout: 2000, method: "GET", credentials: "include", headers: { "Accept": "application/json", 'Content-Type': 'application/json', [`${csrfTokenHeader}`]: csrfToken } , ...requestOptions } return fetch(url, requestData).catch(onError); } (async function onInit() { try { let userInfoResponse = await callApi("/bff/api/users/me", {}); if (userInfoResponse.ok) { let userInfo = await userInfoResponse.json(); console.log(userInfo); spa.userInfo = userInfo; } } catch { console.log("failed to fetch userinfo"); } if (spa.userInfo) { qs("#userInfo").innerText = JSON.stringify(spa.userInfo, null, " "); qs("#login").remove() } else { qs("#logout").remove() } }()); ================================================ FILE: apps/bff-springboot/src/main/resources/templates/app/index.html ================================================ SPA BFF Demo login Logout
Anonymous

================================================ FILE: apps/bff-springboot3/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/bff-springboot3/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar ================================================ FILE: apps/bff-springboot3/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /usr/local/etc/mavenrc ] ; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=$(java-config --jre-home) fi fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="$(which javac)" if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=$(which readlink) if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then javaHome="$(dirname "\"$javaExecutable\"")" javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi javaHome="$(dirname "\"$javaExecutable\"")" javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then # Remove \r in case we run on Windows within Git Bash # and check out the repository with auto CRLF management # enabled. Otherwise, we may read lines that are delimited with # \r\n and produce $'-Xarg\r' rather than -Xarg due to word # splitting rules. tr -s '\r\n' ' ' < "$1" fi } log() { if [ "$MVNW_VERBOSE" = true ]; then printf '%s\n' "$1" fi } BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR log "$MAVEN_PROJECTBASEDIR" ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" if [ -r "$wrapperJarPath" ]; then log "Found $wrapperJarPath" else log "Couldn't find $wrapperJarPath, downloading it ..." if [ -n "$MVNW_REPOURL" ]; then wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" else wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" fi while IFS="=" read -r key value; do # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) safeValue=$(echo "$value" | tr -d '\r') case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; esac done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" log "Downloading from: $wrapperUrl" if $cygwin; then wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") fi if command -v wget > /dev/null; then log "Found wget ... using wget" [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then log "Found curl ... using curl" [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" else curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" fi else log "Falling back to using Java to download" javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaSource=$(cygpath --path --windows "$javaSource") javaClass=$(cygpath --path --windows "$javaClass") fi if [ -e "$javaSource" ]; then if [ ! -e "$javaClass" ]; then log " - Compiling MavenWrapperDownloader.java ..." ("$JAVA_HOME/bin/javac" "$javaSource") fi if [ -e "$javaClass" ]; then log " - Running MavenWrapperDownloader.java ..." ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" fi fi fi fi ########################################################################################## # End of extension ########################################################################################## # If specified, validate the SHA-256 sum of the Maven wrapper jar file wrapperSha256Sum="" while IFS="=" read -r key value; do case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; esac done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" if [ -n "$wrapperSha256Sum" ]; then wrapperSha256Result=false if command -v sha256sum > /dev/null; then if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then wrapperSha256Result=true fi elif command -v shasum > /dev/null; then if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then wrapperSha256Result=true fi else echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." exit 1 fi if [ $wrapperSha256Result = false ]; then echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 exit 1 fi fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$JAVA_HOME" ] && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain # shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/bff-springboot3/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %WRAPPER_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file SET WRAPPER_SHA_256_SUM="" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B ) IF NOT %WRAPPER_SHA_256_SUM%=="" ( powershell -Command "&{"^ "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ " exit 1;"^ "}"^ "}" if ERRORLEVEL 1 goto error ) @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: apps/bff-springboot3/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.4.7 com.github.thomasdarimont.training bff-springboot3 0.0.1-SNAPSHOT bff-springboot3 bff-springboot3 17 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-oauth2-client org.thymeleaf.extras thymeleaf-extras-springsecurity6 org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-data-redis org.springframework.session spring-session-data-redis org.springframework.data spring-data-jdbc com.h2database h2 io.lettuce lettuce-core org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/Bff3App.java ================================================ package com.github.thomasdarimont.apps.bff3; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Bff3App { public static void main(String[] args) { SpringApplication.run(Bff3App.class, args); } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/api/UsersResource.java ================================================ package com.github.thomasdarimont.apps.bff3.api; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.LinkedHashMap; import java.util.Map; @RestController @RequestMapping("/api/users") class UsersResource { private final RestTemplate oauthRestTemplate; public UsersResource(@Qualifier("oauth") RestTemplate oauthRestTemplate) { this.oauthRestTemplate = oauthRestTemplate; } @GetMapping("/me") public ResponseEntity userInfo(Authentication auth) { // var userInfo = getUserInfoFromAuthority(auth); var userInfo = getUserInfoFromRemote(auth); return ResponseEntity.ok(userInfo); } private Map getUserInfoFromAuthority(Authentication auth) { return auth.getAuthorities().stream() // .filter(OidcUserAuthority.class::isInstance) // .map(authority -> (OidcUserAuthority) authority)// .map(OidcUserAuthority::getUserInfo) // .map(OidcUserInfo::getClaims) // .findFirst() // .orElseGet(() -> Map.of("error", "UserInfoMissing")); } private UserInfo getUserInfoFromRemote(Authentication auth) { var principal = (DefaultOidcUser) auth.getPrincipal(); var idToken = principal.getIdToken(); var issuerUri = idToken.getIssuer().toString(); return oauthRestTemplate.getForObject(issuerUri + "/protocol/openid-connect/userinfo", UserInfo.class); } static class UserInfo extends LinkedHashMap { } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/OAuth2RestTemplateConfig.java ================================================ package com.github.thomasdarimont.apps.bff3.config; import com.github.thomasdarimont.apps.bff3.oauth.TokenAccessor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.web.client.RestTemplate; @Configuration class OAuth2RestTemplateConfig { /** * Provides a {@link RestTemplate} that can obtain access tokes for the current user. * * @param tokenAccessor * @return */ @Bean @Qualifier("oauth") public RestTemplate oauthRestTemplate(TokenAccessor tokenAccessor) { var restTemplate = new RestTemplate(); restTemplate.getInterceptors().add((request, body, execution) -> { var accessToken = tokenAccessor.getAccessTokenForCurrentUser(); if (accessToken == null) { throw new OAuth2AuthenticationException("missing access token"); } var accessTokenValue = accessToken.getTokenValue(); request.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + accessTokenValue); return execution.execute(request, body); }); return restTemplate; } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/SessionConfig.java ================================================ package com.github.thomasdarimont.apps.bff3.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer; @Configuration @EnableRedisHttpSession class SessionConfig extends AbstractHttpSessionApplicationInitializer { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/WebSecurityConfig.java ================================================ package com.github.thomasdarimont.apps.bff3.config; import com.github.thomasdarimont.apps.bff3.config.keycloak.KeycloakLogoutHandler; import com.github.thomasdarimont.apps.bff3.support.HttpSessionOAuth2AuthorizedClientService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import java.util.HashSet; import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console; @Configuration @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakLogoutHandler keycloakLogoutHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http, // OAuth2AuthorizedClientService oAuth2AuthorizedClientService, // ClientRegistrationRepository clientRegistrationRepository, // AuthorizationRequestRepository authorizationRequestRepository // ) throws Exception { http.csrf(customizer -> { customizer.ignoringRequestMatchers("/spa/**") // .ignoringRequestMatchers(toH2Console()) // .csrfTokenRepository(new CookieCsrfTokenRepository()); }); // http.sessionManagement(sess -> { // sess.sessionAuthenticationStrategy() // }) http.authorizeHttpRequests(arc -> { // declarative route configuration // add additional routes arc.requestMatchers(toH2Console()).permitAll(); arc.requestMatchers("/app/**", "/webjars/**", "/resources/**", "/css/**").permitAll(); arc.anyRequest().fullyAuthenticated(); }); // for the sake of example disable frame protection http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow, // we explicitly enable the PKCE customization here. http.oauth2Client(o2cc -> { var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( // clientRegistrationRepository, // OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI // ); oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); o2cc.clientRegistrationRepository(clientRegistrationRepository); o2cc.authorizedClientService(oAuth2AuthorizedClientService); o2cc.authorizationCodeGrant(acgc -> { acgc.authorizationRequestResolver(oauth2AuthRequestResolver) // .authorizationRequestRepository(authorizationRequestRepository); }); }); http.oauth2Login(o2lc -> { //o2lc.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper()); }); http.logout(lc -> { lc.addLogoutHandler(keycloakLogoutHandler); }); return http.build(); } @Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); } @Bean public OAuth2AuthorizedClientRepository authorizedClientRepository() { return new HttpSessionOAuth2AuthorizedClientRepository(); } @Bean public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(JdbcOperations jdbcOps, ClientRegistrationRepository clientRegistrationRepository) { //var oauthAuthorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); // var oauthAuthorizedClientService = new JdbcOAuth2AuthorizedClientService(jdbcOps, clientRegistrationRepository); var oauthAuthorizedClientService = new HttpSessionOAuth2AuthorizedClientService(); return oauthAuthorizedClientService; } private GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { var mappedAuthorities = new HashSet(); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { var oidcUserAuthority = (OidcUserAuthority) authority; var userInfo = oidcUserAuthority.getUserInfo(); // TODO extract roles from userInfo response // List groupAuthorities = userInfo.getClaimAsStringList("groups").stream().map(g -> new SimpleGrantedAuthority("ROLE_" + g.toUpperCase())).collect(Collectors.toList()); // mappedAuthorities.addAll(groupAuthorities); } }); return mappedAuthorities; }; } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/config/keycloak/KeycloakLogoutHandler.java ================================================ package com.github.thomasdarimont.apps.bff3.config.keycloak; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.stereotype.Component; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Component public class KeycloakLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { var principal = (DefaultOidcUser) auth.getPrincipal(); var idToken = principal.getIdToken(); log.info("Propagate logout to keycloak for user. userId={}", idToken.getSubject()); var issuerUri = idToken.getIssuer().toString(); var idTokenValue = idToken.getTokenValue(); var defaultRedirectUri = generateAppUri(request); var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri); try { response.sendRedirect(logoutUrl); } catch (IOException e) { e.printStackTrace(); } } private String generateAppUri(HttpServletRequest request) { var hostname = request.getServerName() + ":" + request.getServerPort(); var isStandardHttps = "https".equals(request.getScheme()) && request.getServerPort() == 443; var isStandardHttp = "http".equals(request.getScheme()) && request.getServerPort() == 80; if (isStandardHttps || isStandardHttp) { hostname = request.getServerName(); } return request.getScheme() + "://" + hostname + request.getContextPath(); } private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) { return issuerUri + "/protocol/openid-connect/logout?id_token_hint=" + idTokenValue + "&post_logout_redirect_uri=" + defaultRedirectUri; } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/oauth/TokenAccessor.java ================================================ package com.github.thomasdarimont.apps.bff3.oauth; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.stereotype.Component; import java.time.Duration; import java.time.Instant; /** * Provides access to OAuth2 access- and refresh-tokens of an authenticated user. */ @Slf4j @Getter @Setter @Component public class TokenAccessor { private final OAuth2AuthorizedClientService authorizedClientService; private final TokenRefresher tokenRefresher; public TokenAccessor(OAuth2AuthorizedClientService authorizedClientService, TokenRefresher tokenRefresher) { this.authorizedClientService = authorizedClientService; this.tokenRefresher = tokenRefresher; } private Duration accessTokenExpiresSkew = Duration.ofSeconds(10); private boolean tokenRefreshEnabled = true; public OAuth2AccessToken getAccessTokenForCurrentUser() { return getAccessToken(SecurityContextHolder.getContext().getAuthentication()); } public OAuth2AccessToken getAccessToken(Authentication auth) { var client = getOAuth2AuthorizedClient(auth); if (client == null) { return null; } var accessToken = client.getAccessToken(); if (accessToken == null) { return null; } var accessTokenStillValid = isAccessTokenStillValid(accessToken); if (!accessTokenStillValid && tokenRefreshEnabled) { accessToken = tokenRefresher.refreshTokens(client); } return accessToken; } public OAuth2RefreshToken getRefreshToken(Authentication auth) { OAuth2AuthorizedClient client = getOAuth2AuthorizedClient(auth); if (client == null) { return null; } return client.getRefreshToken(); } private boolean isAccessTokenStillValid(OAuth2AccessToken accessToken) { var expiresAt = accessToken.getExpiresAt(); if (expiresAt == null) { return false; } var exp = expiresAt.minus(accessTokenExpiresSkew == null ? Duration.ofSeconds(0) : accessTokenExpiresSkew); var now = Instant.now(); return now.isBefore(exp); } private OAuth2AuthorizedClient getOAuth2AuthorizedClient(Authentication auth) { var authToken = (OAuth2AuthenticationToken) auth; var clientId = authToken.getAuthorizedClientRegistrationId(); var username = auth.getName(); return authorizedClientService.loadAuthorizedClient(clientId, username); } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/oauth/TokenIntrospector.java ================================================ package com.github.thomasdarimont.apps.bff3.oauth; import com.fasterxml.jackson.annotation.JsonAnySetter; import jakarta.servlet.http.HttpServletRequest; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; @Component @RequiredArgsConstructor public class TokenIntrospector { private final OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; private final TokenAccessor tokenAccessor; public IntrospectionResult introspectToken(Authentication auth, HttpServletRequest request) { if (!(auth instanceof OAuth2AuthenticationToken)) { return null; } var authToken = (OAuth2AuthenticationToken) auth; var authorizedClient = oAuth2AuthorizedClientRepository.loadAuthorizedClient( authToken.getAuthorizedClientRegistrationId(), // auth, // request ); if (authorizedClient == null) { return null; } var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", authorizedClient.getClientRegistration().getClientId()); requestBody.add("client_secret", authorizedClient.getClientRegistration().getClientSecret()); var accessToken = tokenAccessor.getAccessToken(auth); requestBody.add("token", accessToken.getTokenValue()); requestBody.add("token_type_hint", "access_token"); var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + "/protocol/openid-connect/token/introspect"; var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class); var responseData = responseEntity.getBody(); if (responseData == null || !responseData.isActive()) { return null; } return responseData; } @Data public static class IntrospectionResult { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/oauth/TokenRefresher.java ================================================ package com.github.thomasdarimont.apps.bff3.oauth; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.JWTParser; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.text.ParseException; import java.util.HashMap; import java.util.Map; /** * Uses the current Oauth2 refresh token of the current user session to obtain new tokens. */ @Slf4j @Component public class TokenRefresher { private final OAuth2AuthorizedClientService authorizedClientService; public TokenRefresher(OAuth2AuthorizedClientService authorizedClientService) { this.authorizedClientService = authorizedClientService; } public OAuth2AccessToken refreshTokens(OAuth2AuthorizedClient client) { var clientRegistration = client.getClientRegistration(); var refreshToken = client.getRefreshToken(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", clientRegistration.getClientId()); requestBody.add("client_secret", clientRegistration.getClientSecret()); requestBody.add("grant_type", "refresh_token"); requestBody.add("refresh_token", refreshToken.getTokenValue()); var rt = new RestTemplate(); var tokenUri = clientRegistration.getProviderDetails().getTokenUri(); var responseEntity = rt.postForEntity(tokenUri, new HttpEntity<>(requestBody, headers), AccessTokenResponse.class); if (!responseEntity.getStatusCode().is2xxSuccessful()) { throw new OAuth2AuthenticationException("token refresh failed"); } var accessTokenResponse = responseEntity.getBody(); var newAccessTokenValue = accessTokenResponse.access_token; var newRefreshTokenValue = accessTokenResponse.refresh_token; JWTClaimsSet newAccessTokenClaimsSet; JWTClaimsSet newRefreshTokenClaimSet; try { var newAccessToken = JWTParser.parse(newAccessTokenValue); newAccessTokenClaimsSet = newAccessToken.getJWTClaimsSet(); } catch (ParseException e) { throw new OAuth2AuthenticationException("token refresh failed: could not parse access token"); } try { var newRefreshToken = JWTParser.parse(newRefreshTokenValue); newRefreshTokenClaimSet = newRefreshToken.getJWTClaimsSet(); } catch (ParseException e) { throw new OAuth2AuthenticationException("token refresh failed: could not parse refresh token"); } var accessTokenIat = newAccessTokenClaimsSet.getIssueTime().toInstant(); var accessTokenExp = newAccessTokenClaimsSet.getExpirationTime().toInstant(); var newOAuth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, newAccessTokenValue, accessTokenIat, accessTokenExp); var refreshTokenIat = newRefreshTokenClaimSet.getIssueTime().toInstant(); var refreshTokenExp = newRefreshTokenClaimSet.getExpirationTime().toInstant(); var newOAuth2RefreshToken = new OAuth2RefreshToken(newRefreshTokenValue, refreshTokenIat, refreshTokenExp); var newClient = new OAuth2AuthorizedClient(clientRegistration, client.getPrincipalName(), newOAuth2AccessToken, newOAuth2RefreshToken); authorizedClientService.saveAuthorizedClient(newClient, SecurityContextHolder.getContext().getAuthentication()); return newOAuth2AccessToken; } @Data static class AccessTokenResponse { final long createdAtSeconds = System.currentTimeMillis() / 1000; String access_token; String refresh_token; String error; int expires_in; Map metadata = new HashMap<>(); @JsonAnySetter public void setMetadata(String key, Object value) { metadata.put(key, value); } } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/support/HttpServletRequestUtils.java ================================================ package com.github.thomasdarimont.apps.bff3.support; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.Optional; public class HttpServletRequestUtils { public static Optional getCurrentHttpServletRequest() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getRequest); } public static Optional getCurrentHttpSession(boolean create) { return getCurrentHttpServletRequest().map(req -> req.getSession(false)); } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/support/HttpSessionOAuth2AuthorizedClientService.java ================================================ package com.github.thomasdarimont.apps.bff3.support; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; @RequiredArgsConstructor public class HttpSessionOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService { @Override public T loadAuthorizedClient(String clientRegistrationId, String principalName) { return (T) HttpServletRequestUtils.getCurrentHttpSession(false) // .map(sess -> sess.getAttribute(clientRegistrationId)) // .orElse(null); } @Override public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) { HttpServletRequestUtils.getCurrentHttpSession(false) // .ifPresent(sess -> sess.setAttribute(authorizedClient.getClientRegistration().getRegistrationId(), authorizedClient)); } @Override public void removeAuthorizedClient(String clientRegistrationId, String principalName) { HttpServletRequestUtils.getCurrentHttpSession(false) // .ifPresent(sess -> sess.removeAttribute(clientRegistrationId)); } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/web/AuthResource.java ================================================ package com.github.thomasdarimont.apps.bff3.web; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.github.thomasdarimont.apps.bff3.oauth.TokenIntrospector; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/auth") @RequiredArgsConstructor class AuthResource { private final TokenIntrospector tokenIntrospector; @GetMapping("/check-session") public ResponseEntity checkSession(Authentication auth, HttpServletRequest request) throws ServletException { var introspectionResult = tokenIntrospector.introspectToken(auth, request); if (introspectionResult == null || !introspectionResult.isActive()) { // SecurityContextHolder.clearContext(); request.logout(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } return ResponseEntity.ok().build(); } @Data static class IntrospectionResponse { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/bff-springboot3/src/main/java/com/github/thomasdarimont/apps/bff3/web/UiResource.java ================================================ package com.github.thomasdarimont.apps.bff3.web; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller @RequiredArgsConstructor class UiResource { @GetMapping("/") public String index(Model model) { model.addAttribute("appScript", "/app/app.js"); return "/app/index"; } } ================================================ FILE: apps/bff-springboot3/src/main/resources/application.yml ================================================ server: port: 4693 ssl: enabled: true key-store: config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 servlet: context-path: /bff error: include-stacktrace: never logging: level: root: info org: springframework: web: info spring: thymeleaf: cache: false security: oauth2: client: provider: keycloak: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal user-name-attribute: preferred_username registration: keycloak: client-id: 'acme-bff-springboot' client-secret: 'secret' client-authentication-method: client_secret_post authorizationGrantType: authorization_code redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' scope: openid data: redis: client-name: "acme-bff" sql: init: schema-locations: classpath:org/springframework/security/oauth2/client/oauth2-client-schema.sql h2: console.enabled: true session: timeout: 30m ================================================ FILE: apps/bff-springboot3/src/main/resources/static/app/app.js ================================================ let spa = {}; function qs(selector) { return document.querySelector(selector); } function qsa(selector) { return [...document.querySelectorAll(selector)]; } function callApi(url, requestOptions, onError) { let csrfToken = qs("meta[name=_csrf]").content; let csrfTokenHeader = qs("meta[name=_csrf_header]").content; let requestData = { timeout: 2000, method: "GET", credentials: "include", headers: { "Accept": "application/json", 'Content-Type': 'application/json', [`${csrfTokenHeader}`]: csrfToken } , ...requestOptions } return fetch(url, requestData).catch(onError); } (async function onInit() { try { let userInfoResponse = await callApi("/bff/api/users/me", {}); if (userInfoResponse.ok) { let userInfo = await userInfoResponse.json(); console.log(userInfo); spa.userInfo = userInfo; } } catch { console.log("failed to fetch userinfo"); } if (spa.userInfo) { qs("#userInfo").innerText = JSON.stringify(spa.userInfo, null, " "); qs("#login").remove() } else { qs("#logout").remove() } }()); ================================================ FILE: apps/bff-springboot3/src/main/resources/templates/app/index.html ================================================ SPA BFF3 Demo login Logout
Anonymous

================================================ FILE: apps/frontend-webapp-springboot/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/frontend-webapp-springboot/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /usr/local/etc/mavenrc ] ; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`\\unset -f command; \\command -v java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" else jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/frontend-webapp-springboot/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: apps/frontend-webapp-springboot/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 2.7.14 com.github.thomasdarimont.keycloak frontend-webapp-springboot 0.0.1-SNAPSHOT frontend-webapp-springboot frontend-webapp-springboot 17 1.18.38 org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity5 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-webflux org.webjars webjars-locator-core org.webjars jquery 3.6.0 org.webjars bootstrap 5.1.3 org.webjars tempusdominus-bootstrap-4 5.39.0 org.webjars font-awesome 5.15.4 org.webjars momentjs 2.29.1 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/WebAppSpringBoot.java ================================================ package com.github.thomasdarimont.keycloak.webapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WebAppSpringBoot { public static void main(String[] args) { SpringApplication.run(WebAppSpringBoot.class, args); } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/KeycloakWebClientConfig.java ================================================ package com.github.thomasdarimont.keycloak.webapp.config; import com.github.thomasdarimont.keycloak.webapp.support.keycloakclient.KeycloakServiceException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @Configuration class KeycloakWebClientConfig { @Bean @Qualifier("keycloakWebClient") public WebClient keycloakWebClient(ClientRegistrationRepository clientRegistrations, OAuth2AuthorizedClientRepository authorizedClients) { var oauthExchangeFilterFunction = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients); oauthExchangeFilterFunction.setDefaultOAuth2AuthorizedClient(true); var clientRegistration = clientRegistrations.findByRegistrationId("keycloak"); oauthExchangeFilterFunction.setDefaultClientRegistrationId(clientRegistration.getRegistrationId()); return WebClient.builder() // .apply(oauthExchangeFilterFunction.oauth2Configuration()) // .defaultHeaders(headers -> { headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); }) // .filter(errorHandler()) // .baseUrl(clientRegistration.getProviderDetails().getIssuerUri()) // .build(); } public static ExchangeFilterFunction errorHandler() { return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { if (clientResponse.statusCode().is5xxServerError()) { return clientResponse.bodyToMono(String.class) // .flatMap(errorBody -> Mono.error(new KeycloakServiceException(errorBody))); } if (clientResponse.statusCode().is4xxClientError()) { return clientResponse.bodyToMono(String.class) // .flatMap(errorBody -> Mono.error(new KeycloakServiceException(errorBody))); } return Mono.just(clientResponse); }); } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/OidcUserServiceConfig.java ================================================ package com.github.thomasdarimont.keycloak.webapp.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; @Configuration class OidcUserServiceConfig { @Bean public OidcUserService keycloakUserService() { return new OidcUserService(); } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/WebSecurityConfig.java ================================================ package com.github.thomasdarimont.keycloak.webapp.config; import com.github.thomasdarimont.keycloak.webapp.support.security.KeycloakLogoutHandler; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.web.SecurityFilterChain; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashSet; @Configuration @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakLogoutHandler keycloakLogoutHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, AuthorizationRequestRepository authorizationRequestRepository) throws Exception { http.authorizeRequests(arc -> { // declarative route configuration // add additional routes arc.antMatchers("/webjars/**", "/resources/**", "/css/**", "/auth/register").permitAll(); arc.anyRequest().fullyAuthenticated(); }); // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow, // we explicitly enable the PKCE customization here. http.oauth2Client(o2cc -> { var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( // clientRegistrationRepository, // OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI // ); oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); o2cc.authorizationCodeGrant() // .authorizationRequestResolver(oauth2AuthRequestResolver) // .authorizationRequestRepository(authorizationRequestRepository); }); http.oauth2Login(o2lc -> { o2lc.userInfoEndpoint().userAuthoritiesMapper(userAuthoritiesMapper()); }); http.logout(lc -> { lc.addLogoutHandler(keycloakLogoutHandler); }); return http.build(); } /** * The explicit declaration of {@link AuthorizationRequestRepository} is only necessary, if dynamic user self-registration is required. * See {@link com.github.thomasdarimont.keycloak.webapp.web.AuthController#register(HttpServletRequest, HttpServletResponse)}. * If this is not needed, this bean can be removed. * * @return */ @Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); } private GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { var mappedAuthorities = new HashSet(); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { var oidcUserAuthority = (OidcUserAuthority) authority; var userInfo = oidcUserAuthority.getUserInfo(); // TODO extract roles from userInfo response // List groupAuthorities = userInfo.getClaimAsStringList("groups").stream().map(g -> new SimpleGrantedAuthority("ROLE_" + g.toUpperCase())).collect(Collectors.toList()); // mappedAuthorities.addAll(groupAuthorities); } }); return mappedAuthorities; }; } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/ApplicationEntry.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class ApplicationEntry { String clientId; String name; String url; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/CredentialEntry.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class CredentialEntry { String id; String label; String type; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/SettingEntry.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class SettingEntry { String name; String value; String type; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/UserProfile.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class UserProfile { String firstname; String lastname; String email; String phoneNumber; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/OAuth2AuthorizedClientAccessor.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import com.github.thomasdarimont.keycloak.webapp.support.keycloakclient.KeycloakClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor @Slf4j public class OAuth2AuthorizedClientAccessor { private final OAuth2AuthorizedClientService authorizedClientService; private final KeycloakClient defaultKeycloakService; public OAuth2AuthorizedClient getOAuth2AuthorizedClient(Authentication auth) { var authToken = (OAuth2AuthenticationToken) auth; var registeredId = authToken.getAuthorizedClientRegistrationId(); var username = auth.getName(); var authorizedClient = authorizedClientService.loadAuthorizedClient(registeredId, username); if (authorizedClient == null) { return null; } var refreshToken = authorizedClient.getRefreshToken(); try { if (refreshToken == null) { return null; } var introspectResponse = defaultKeycloakService.introspect(refreshToken.getTokenValue()); var active = introspectResponse.getActive(); if (active != null && !Boolean.parseBoolean(active)) { return null; } } catch (Exception e) { log.warn("Token introspection failed." + e.getMessage()); return null; } return authorizedClient; } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenAccessor.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class TokenAccessor { private final OAuth2AuthorizedClientService authorizedClientService; public OAuth2AccessToken getAccessTokenForCurrentUser() { return getAccessToken(SecurityContextHolder.getContext().getAuthentication()); } public OAuth2AccessToken getAccessToken(Authentication auth) { OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) auth; String clientId = authToken.getAuthorizedClientRegistrationId(); String username = auth.getName(); OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient(clientId, username); if (client == null) { return null; } return client.getAccessToken(); } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenIntrospector.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import com.fasterxml.jackson.annotation.JsonAnySetter; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; @Component @RequiredArgsConstructor public class TokenIntrospector { private final OAuth2AuthorizedClientService authorizedClientService; private final TokenAccessor tokenAccessor; public IntrospectionResult introspectToken(Authentication auth) { if (!(auth instanceof OAuth2AuthenticationToken)) { return null; } var authToken = (OAuth2AuthenticationToken) auth; var authorizedClient = authorizedClientService.loadAuthorizedClient( authToken.getAuthorizedClientRegistrationId(), auth.getName() ); if (authorizedClient == null) { return null; } var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", authorizedClient.getClientRegistration().getClientId()); requestBody.add("client_secret", authorizedClient.getClientRegistration().getClientSecret()); var accessToken = tokenAccessor.getAccessToken(auth); requestBody.add("token", accessToken.getTokenValue()); requestBody.add("token_type_hint", "access_token"); var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + "/protocol/openid-connect/token/introspect"; var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class); var responseData = responseEntity.getBody(); if (responseData == null || !responseData.isActive()) { return null; } return responseData; } @Data public static class IntrospectionResult { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/DefaultKeycloakClient.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Set; @Service @Configuration @EnableConfigurationProperties public class DefaultKeycloakClient implements KeycloakClient { private final String keycloakClientId; private final String keycloakAuthUri; private final byte[] keycloakClientSecret; private final Set keycloakClientScopes; private final Duration keycloakRequestTimeout; private final WebClient client; private final OidcUserService keycloakUserService; public DefaultKeycloakClient(@Qualifier("keycloakWebClient") WebClient client, ClientRegistrationRepository clientRegistrations, OidcUserService keycloakUserService) { this.client = client; var keycloak = clientRegistrations.findByRegistrationId("keycloak"); var providerDetails = keycloak.getProviderDetails(); this.keycloakAuthUri = providerDetails.getAuthorizationUri(); this.keycloakClientId = keycloak.getClientId(); this.keycloakClientSecret = keycloak.getClientSecret().getBytes(StandardCharsets.UTF_8); this.keycloakClientScopes = keycloak.getScopes(); this.keycloakRequestTimeout = Duration.ofSeconds(3); this.keycloakUserService = keycloakUserService; } @Override public KeycloakIntrospectResponse introspect(String token) { var payload = new LinkedMultiValueMap(); payload.set("client_id", this.keycloakClientId); payload.set("client_secret", new String(this.keycloakClientSecret, StandardCharsets.UTF_8)); payload.set("token", token); return this.client.method(HttpMethod.POST) // .uri("/protocol/openid-connect/token/introspect") // .contentType(MediaType.APPLICATION_FORM_URLENCODED) // .body(BodyInserters.fromFormData(payload)) // .retrieve() // .bodyToMono(KeycloakIntrospectResponse.class) // .block(keycloakRequestTimeout); } @Override public KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken) { var oidcUserRequest = new OidcUserRequest(authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(), oidcIdToken); var oidcUser = this.keycloakUserService.loadUser(oidcUserRequest); var keycloakUserInfo = new KeycloakUserInfo(); keycloakUserInfo.setName(oidcUser.getClaimAsString("name")); keycloakUserInfo.setPreferredUsername(oidcUser.getPreferredUsername()); keycloakUserInfo.setFirstname(oidcUser.getGivenName()); keycloakUserInfo.setLastname(oidcUser.getFamilyName()); keycloakUserInfo.setEmail(oidcUser.getEmail()); keycloakUserInfo.setEmailVerified(oidcUser.getEmailVerified()); keycloakUserInfo.setPhoneNumber(oidcUser.getPhoneNumber()); keycloakUserInfo.setPhoneNumberVerified(oidcUser.getPhoneNumberVerified()); return keycloakUserInfo; } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakClient.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.core.oidc.OidcIdToken; public interface KeycloakClient { KeycloakIntrospectResponse introspect(String token); KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken); } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakIntrospectResponse.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class KeycloakIntrospectResponse { public String active; @JsonProperty("token_type") public String tokenType; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakServiceException.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import lombok.Data; @Data public class KeycloakServiceException extends RuntimeException { private final String errorBody; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakUserInfo.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class KeycloakUserInfo { @JsonProperty("name") private String name; @JsonProperty("preferred_username") private String preferredUsername; @JsonProperty("family_name") private String lastname; @JsonProperty("given_name") private String firstname; @JsonProperty("email") private String email; @JsonProperty("email_verified") private Boolean emailVerified; @JsonProperty("phone_number") private String phoneNumber; @JsonProperty("phone_number_verified") private Boolean phoneNumberVerified; } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/security/KeycloakLogoutHandler.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.security; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Component public class KeycloakLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { var principal = (DefaultOidcUser) auth.getPrincipal(); var idToken = principal.getIdToken(); log.info("Propagate logout to keycloak for user. userId={}", idToken.getSubject()); var issuerUri = idToken.getIssuer().toString(); var idTokenValue = idToken.getTokenValue(); var defaultRedirectUri = generateAppUri(request); var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri); try { response.sendRedirect(logoutUrl); } catch (IOException e) { e.printStackTrace(); } } private String generateAppUri(HttpServletRequest request) { var hostname = request.getServerName() + ":" + request.getServerPort(); var isStandardHttps = "https".equals(request.getScheme()) && request.getServerPort() == 443; var isStandardHttp = "http".equals(request.getScheme()) && request.getServerPort() == 80; if (isStandardHttps || isStandardHttp) { hostname = request.getServerName(); } return request.getScheme() + "://" + hostname + request.getContextPath() + "/"; } private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) { return issuerUri + "/protocol/openid-connect/logout?id_token_hint=" + idTokenValue + "&post_logout_redirect_uri=" + defaultRedirectUri; } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/AuthController.java ================================================ package com.github.thomasdarimont.keycloak.webapp.web; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.github.thomasdarimont.keycloak.webapp.support.TokenIntrospector; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.util.HashMap; import java.util.Map; @RestController @RequiredArgsConstructor public class AuthController { private final TokenIntrospector tokenIntrospector; private final ClientRegistrationRepository clientRegistrationRepository; private final AuthorizationRequestRepository authorizationRequestRepository; /** * Init anonymous registration via: https://apps.acme.test:4633/webapp/auth/register * * @param request * @param response * @return */ @GetMapping("/auth/register") public ResponseEntity register(HttpServletRequest request, HttpServletResponse response) { var resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, request.getContextPath()); resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); var authzRequest = resolver // .resolve(request, "keycloak"); authorizationRequestRepository.saveAuthorizationRequest(authzRequest, request, response); var registerUriString = authzRequest.getAuthorizationRequestUri() // .replaceFirst("/openid-connect/auth", "/openid-connect/registrations"); var registerUri = URI.create(registerUriString); return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(registerUri).build(); } @GetMapping("/auth/check-session") public ResponseEntity checkSession(Authentication auth) { var introspectionResult = tokenIntrospector.introspectToken(auth); if (introspectionResult == null || !introspectionResult.isActive()) { SecurityContextHolder.clearContext(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } return ResponseEntity.ok().build(); } @Data static class IntrospectionResponse { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/UiController.java ================================================ package com.github.thomasdarimont.keycloak.webapp.web; import com.github.thomasdarimont.keycloak.webapp.domain.ApplicationEntry; import com.github.thomasdarimont.keycloak.webapp.domain.CredentialEntry; import com.github.thomasdarimont.keycloak.webapp.domain.SettingEntry; import com.github.thomasdarimont.keycloak.webapp.domain.UserProfile; import com.github.thomasdarimont.keycloak.webapp.support.OAuth2AuthorizedClientAccessor; import com.github.thomasdarimont.keycloak.webapp.support.keycloakclient.KeycloakClient; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; @Controller @RequiredArgsConstructor class UiController { private final OAuth2AuthorizedClientAccessor oauth2AuthorizedClientAccessor; private final KeycloakClient keycloakClient; @GetMapping("/") public String showIndex(Model model) { return "index"; } @GetMapping("/profile") public String showProfile(Model model, Authentication auth) { var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth); if (authorizedClient == null) { SecurityContextHolder.clearContext(); return "redirect:"; } var principal = (DefaultOidcUser) auth.getPrincipal(); var profile = buildUserProfile(authorizedClient, principal); model.addAttribute("profile", profile); return "profile"; } private UserProfile buildUserProfile(OAuth2AuthorizedClient oAuth2AuthorizedClient, DefaultOidcUser oidcUser) { var keycloakUserInfo = keycloakClient.userInfo(oAuth2AuthorizedClient, oidcUser.getIdToken()); var profile = new UserProfile(); profile.setFirstname(keycloakUserInfo.getFirstname()); profile.setLastname(keycloakUserInfo.getLastname()); profile.setEmail(keycloakUserInfo.getEmail()); profile.setPhoneNumber(keycloakUserInfo.getPhoneNumber()); return profile; } @GetMapping("/settings") public String showSettings(Model model, Authentication auth) { var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth); if (authorizedClient == null) { SecurityContextHolder.clearContext(); return "redirect:settings"; } var setting1 = new SettingEntry(); setting1.setName("setting1"); setting1.setValue("value1"); setting1.setType("string"); var setting2 = new SettingEntry(); setting2.setName("setting2"); setting2.setValue("on"); setting2.setType("boolean"); var settings = List.of(setting1, setting2); model.addAttribute("settings", settings); return "settings"; } @GetMapping("/security") public String showSecurity(Model model, Authentication auth) { var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth); if (authorizedClient == null) { SecurityContextHolder.clearContext(); return "redirect:security"; } var credential1 = new CredentialEntry(); credential1.setId("cred1"); credential1.setLabel("value1"); credential1.setType("password"); var credential2 = new CredentialEntry(); credential2.setId("cred2"); credential2.setLabel("value2"); credential2.setType("totp"); var credentials = List.of(credential1, credential2); model.addAttribute("credentials", credentials); return "security"; } @GetMapping("/applications") public String showApplications(Model model, Authentication auth) { var authorizedClient = oauth2AuthorizedClientAccessor.getOAuth2AuthorizedClient(auth); if (authorizedClient == null) { SecurityContextHolder.clearContext(); return "redirect:applications"; } var appEntry1 = new ApplicationEntry(); appEntry1.setClientId("app1"); appEntry1.setName("App 1"); appEntry1.setUrl("http://localhost/app1"); var appEntry2 = new ApplicationEntry(); appEntry2.setClientId("app2"); appEntry2.setName("App 2"); appEntry2.setUrl("http://localhost/app2"); var apps = List.of(appEntry1, appEntry2); model.addAttribute("apps", apps); return "applications"; } } ================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/application.yml ================================================ server: port: 4633 ssl: enabled: true key-store: config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 servlet: context-path: /webapp error: include-stacktrace: never spring: thymeleaf: cache: false security: oauth2: client: provider: keycloak: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal user-name-attribute: preferred_username registration: keycloak: client-name: 'Acme Internal' client-id: 'frontend-webapp-springboot' client-secret: 'secret' client-authentication-method: client_secret_post authorizationGrantType: authorization_code redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' scope: openid logging: level: root: info org: springframework: web: info ================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/templates/applications.html ================================================

Applications

================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/templates/fragments.html ================================================ Account Console ================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/templates/index.html ================================================ Account Console

This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or information.


It uses utility classes for typography and spacing to space content out within the larger container.

Learn more

================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/templates/profile.html ================================================

Profile

Firstname

Johnatan


Lastname

Smith


Email

example@example.com


Phone

(097) 234-5678

================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/templates/security.html ================================================

Security

================================================ FILE: apps/frontend-webapp-springboot/src/main/resources/templates/settings.html ================================================

Settings

================================================ FILE: apps/frontend-webapp-springboot/src/test/java/com/github/thomasdarimont/keycloak/cac/WebApplicationTestsSpringBoot.java ================================================ package com.github.thomasdarimont.keycloak.cac; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class WebApplicationTestsSpringBoot { @Test void contextLoads() { } } ================================================ FILE: apps/frontend-webapp-springboot3/otel-config.yaml ================================================ otel: trace: ignore: resources: [ "/health", "/metrics", "/webjars/**", "/static" ] ================================================ FILE: apps/frontend-webapp-springboot3/pom.xml ================================================ org.springframework.boot spring-boot-starter-parent 3.4.7 4.0.0 frontend-webapp-springboot3 17 3.1.2.RELEASE org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-thymeleaf org.thymeleaf.extras thymeleaf-extras-springsecurity6 ${thymeleaf-extras-springsecurity6.version} org.springframework.boot spring-boot-starter-web org.webjars webjars-locator-core org.webjars jquery 3.6.0 org.webjars bootstrap 5.1.3 org.webjars tempusdominus-bootstrap-4 5.39.0 org.webjars font-awesome 5.15.4 org.webjars momentjs 2.29.1 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin spring-milestones Spring Milestones https://repo.spring.io/milestone false spring-milestones Spring Milestones https://repo.spring.io/milestone false ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/WebAppSpringBoot3.java ================================================ package com.github.thomasdarimont.keycloak.webapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class WebAppSpringBoot3 { public static void main(String[] args) { SpringApplication.run(WebAppSpringBoot3.class, args); } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/KeycloakWebClientConfig.java ================================================ package com.github.thomasdarimont.keycloak.webapp.config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInitializer; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.util.Assert; import org.springframework.web.client.RestClient; import java.io.IOException; @Configuration class KeycloakWebClientConfig { @Bean @Qualifier("keycloakRestClient") public RestClient keycloakWebClient(OAuth2AuthorizedClientManager authorizedClientManager, ClientRegistrationRepository clientRegistrations, OAuth2AuthorizedClientRepository authorizedClients) { var clientRegistration = clientRegistrations.findByRegistrationId("keycloak"); var oauthInterceptor = new OAuth2ClientInterceptor(authorizedClientManager, clientRegistration); return RestClient.builder() // .requestInterceptor(oauthInterceptor) // .defaultHeaders(headers -> { headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); })// .baseUrl(clientRegistration.getProviderDetails().getIssuerUri()) // .build(); } public static class OAuth2ClientInterceptor implements ClientHttpRequestInterceptor, ClientHttpRequestInitializer { private final OAuth2AuthorizedClientManager manager; private final ClientRegistration clientRegistration; public OAuth2ClientInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) { this.manager = manager; this.clientRegistration = clientRegistration; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().setBearerAuth(getBearerToken()); return execution.execute(request, body); } @Override public void initialize(ClientHttpRequest request) { request.getHeaders().setBearerAuth(getBearerToken()); } private String getBearerToken() { OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistration.getRegistrationId()).principal(clientRegistration.getClientId()).build(); OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest); Assert.notNull(client, () -> "Authorized client failed for Registration id: '" + clientRegistration.getRegistrationId() + "', returned client is null"); return client.getAccessToken().getTokenValue(); } } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/OidcUserServiceConfig.java ================================================ package com.github.thomasdarimont.keycloak.webapp.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; @Configuration class OidcUserServiceConfig { @Bean public OidcUserService keycloakUserService() { return new OidcUserService(); } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/config/WebSecurityConfig.java ================================================ package com.github.thomasdarimont.keycloak.webapp.config; import com.github.thomasdarimont.keycloak.webapp.support.HttpSessionOAuth2AuthorizedClientService; import com.github.thomasdarimont.keycloak.webapp.support.security.KeycloakLogoutHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; import org.springframework.security.oauth2.client.endpoint.RestClientAuthorizationCodeTokenResponseClient; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.util.MultiValueMap; import java.util.HashSet; @Slf4j @Configuration @RequiredArgsConstructor class WebSecurityConfig { private final KeycloakLogoutHandler keycloakLogoutHandler; @Bean public SecurityFilterChain filterChain(HttpSecurity http, ClientRegistrationRepository clientRegistrationRepository, CorsEndpointProperties corsEndpointProperties) throws Exception { http.authorizeHttpRequests(ahrc -> { // declarative route configuration // add additional routes ahrc.requestMatchers("/webjars/**", "/resources/**", "/css/**", "/auth/register").permitAll(); ahrc.anyRequest().fullyAuthenticated(); }); // by default spring security oauth2 client does not support PKCE for confidential clients for auth code grant flow, // we explicitly enable the PKCE customization here. http.oauth2Client(o2cc -> { var oauth2AuthRequestResolver = new DefaultOAuth2AuthorizationRequestResolver( // clientRegistrationRepository, // OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI // ); oauth2AuthRequestResolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); o2cc.authorizationCodeGrant(customizer -> { customizer.authorizationRequestResolver(oauth2AuthRequestResolver); }); }); http.oauth2Login(o2lc -> { o2lc.userInfoEndpoint(customizer -> { customizer.userAuthoritiesMapper(userAuthoritiesMapper()); }); // customizeTokenEndpointRequest(o2lc); }); http.logout(lc -> { lc.addLogoutHandler(keycloakLogoutHandler); }); return http.build(); } private static void customizeTokenEndpointRequest(OAuth2LoginConfigurer o2lc) { // customize the token endpoint request parameters o2lc.tokenEndpoint(tec -> { tec.accessTokenResponseClient( createCustomAccessTokenResponseClientNew() // createCustomAccessTokenResponseClientOld() ); }); } private static OAuth2AccessTokenResponseClient createCustomAccessTokenResponseClientNew() { var accessTokenResponseClient = new RestClientAuthorizationCodeTokenResponseClient(); accessTokenResponseClient.setParametersCustomizer(parameters -> { parameters.add("client_session_state", "bubu123"); parameters.add("client_session_host", "apps.acme.test"); }); return accessTokenResponseClient; } private static OAuth2AccessTokenResponseClient createCustomAccessTokenResponseClientOld() { var accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); accessTokenResponseClient.setRequestEntityConverter(new OAuth2AuthorizationCodeGrantRequestEntityConverter(){ @Override protected MultiValueMap createParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) { // if used with instance specific backchannel logout url: https://${application.session.host}:4633/webapp/logout MultiValueMap parameters = super.createParameters(authorizationCodeGrantRequest); parameters.add("client_session_state", "bubu123"); parameters.add("client_session_host", "apps.acme.test"); return parameters; } }); return accessTokenResponseClient; } /** * The explicit declaration of {@link AuthorizationRequestRepository} is only necessary, if dynamic user self-registration is required. * See {@link com.github.thomasdarimont.keycloak.webapp.web.AuthController#register(HttpServletRequest, HttpServletResponse)}. * If this is not needed, this bean can be removed. * * @return */ @Bean public AuthorizationRequestRepository authorizationRequestRepository() { return new HttpSessionOAuth2AuthorizationRequestRepository(); } @Bean public OAuth2AuthorizedClientRepository authorizedClientRepository() { return new HttpSessionOAuth2AuthorizedClientRepository(); } @Bean public OAuth2AuthorizedClientService oAuth2AuthorizedClientService(OAuth2AuthorizedClientRepository clientRegistrationRepository) { // var oauthAuthorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository); return new HttpSessionOAuth2AuthorizedClientService(clientRegistrationRepository); } private GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { var mappedAuthorities = new HashSet(); authorities.forEach(authority -> { if (authority instanceof OidcUserAuthority) { var oidcUserAuthority = (OidcUserAuthority) authority; var userInfo = oidcUserAuthority.getUserInfo(); // TODO extract roles from userInfo response // List groupAuthorities = userInfo.getClaimAsStringList("groups").stream().map(g -> new SimpleGrantedAuthority("ROLE_" + g.toUpperCase())).collect(Collectors.toList()); // mappedAuthorities.addAll(groupAuthorities); log.info("Got userinfo. userinfo={}", userInfo); } }); return mappedAuthorities; }; } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/ApplicationEntry.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class ApplicationEntry { String clientId; String name; String url; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/CredentialEntry.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class CredentialEntry { String id; String label; String type; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/SettingEntry.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class SettingEntry { String name; String value; String type; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/domain/UserProfile.java ================================================ package com.github.thomasdarimont.keycloak.webapp.domain; import lombok.Data; @Data public class UserProfile { String firstname; String lastname; String email; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/HttpServletRequestUtils.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.Optional; public class HttpServletRequestUtils { public static Optional getCurrentHttpServletRequest() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getRequest); } public static Optional getCurrentHttpServletResponse() { ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); return Optional.ofNullable(servletRequestAttributes).map(ServletRequestAttributes::getResponse); } public static Optional getCurrentHttpSession(boolean create) { return getCurrentHttpServletRequest().map(req -> req.getSession(false)); } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/HttpSessionOAuth2AuthorizedClientService.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; @RequiredArgsConstructor public class HttpSessionOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService { private final OAuth2AuthorizedClientRepository authorizedClientRepository; @Override public T loadAuthorizedClient(String clientRegistrationId, String principalName) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authorizedClientRepository.loadAuthorizedClient(clientRegistrationId, authentication, HttpServletRequestUtils.getCurrentHttpServletRequest().get()); } @Override public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) { HttpServletRequest request = HttpServletRequestUtils.getCurrentHttpServletRequest().get(); HttpServletResponse response = HttpServletRequestUtils.getCurrentHttpServletResponse().get(); authorizedClientRepository.saveAuthorizedClient(authorizedClient, principal, request, response); } @Override public void removeAuthorizedClient(String clientRegistrationId, String principalName) { HttpServletRequest request = HttpServletRequestUtils.getCurrentHttpServletRequest().get(); HttpServletResponse response = HttpServletRequestUtils.getCurrentHttpServletResponse().get(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); authorizedClientRepository.removeAuthorizedClient(clientRegistrationId, authentication, request, response); } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenAccessor.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class TokenAccessor { private final OAuth2AuthorizedClientService authorizedClientService; public OAuth2AccessToken getAccessTokenForCurrentUser() { return getAccessToken(SecurityContextHolder.getContext().getAuthentication()); } public OAuth2AccessToken getAccessToken(Authentication auth) { OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) auth; String clientId = authToken.getAuthorizedClientRegistrationId(); String username = auth.getName(); OAuth2AuthorizedClient client = authorizedClientService.loadAuthorizedClient(clientId, username); if (client == null) { return null; } return client.getAccessToken(); } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/TokenIntrospector.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support; import com.fasterxml.jackson.annotation.JsonAnySetter; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; @Component @RequiredArgsConstructor public class TokenIntrospector { private final OAuth2AuthorizedClientService authorizedClientService; private final TokenAccessor tokenAccessor; public IntrospectionResult introspectToken(Authentication auth) { if (!(auth instanceof OAuth2AuthenticationToken)) { return null; } var authToken = (OAuth2AuthenticationToken) auth; var authorizedClient = authorizedClientService.loadAuthorizedClient( authToken.getAuthorizedClientRegistrationId(), auth.getName() ); if (authorizedClient == null) { return null; } var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", authorizedClient.getClientRegistration().getClientId()); requestBody.add("client_secret", authorizedClient.getClientRegistration().getClientSecret()); var accessToken = tokenAccessor.getAccessToken(auth); requestBody.add("token", accessToken.getTokenValue()); requestBody.add("token_type_hint", "access_token"); var tokenIntrospection = authorizedClient.getClientRegistration().getProviderDetails().getIssuerUri() + "/protocol/openid-connect/token/introspect"; var responseEntity = rt.postForEntity(tokenIntrospection, new HttpEntity<>(requestBody, headers), IntrospectionResult.class); var responseData = responseEntity.getBody(); if (responseData == null || !responseData.isActive()) { return null; } return responseData; } @Data public static class IntrospectionResult { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/DefaultKeycloakClient.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestClient; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Set; @Service @Configuration @EnableConfigurationProperties public class DefaultKeycloakClient implements KeycloakClient { private final String keycloakClientId; private final String keycloakAuthUri; private final byte[] keycloakClientSecret; private final Set keycloakClientScopes; private final Duration keycloakRequestTimeout; private final RestClient client; private final OidcUserService keycloakUserService; public DefaultKeycloakClient(@Qualifier("keycloakRestClient") RestClient client, ClientRegistrationRepository clientRegistrations, OidcUserService keycloakUserService) { this.client = client; var keycloak = clientRegistrations.findByRegistrationId("keycloak"); var providerDetails = keycloak.getProviderDetails(); this.keycloakAuthUri = providerDetails.getAuthorizationUri(); this.keycloakClientId = keycloak.getClientId(); this.keycloakClientSecret = keycloak.getClientSecret().getBytes(StandardCharsets.UTF_8); this.keycloakClientScopes = keycloak.getScopes(); this.keycloakRequestTimeout = Duration.ofSeconds(3); this.keycloakUserService = keycloakUserService; } @Override public KeycloakIntrospectResponse introspect(String token) { var payload = new LinkedMultiValueMap(); payload.set("client_id", this.keycloakClientId); payload.set("client_secret", new String(this.keycloakClientSecret, StandardCharsets.UTF_8)); payload.set("token", token); return this.client.method(HttpMethod.POST) // .uri("/protocol/openid-connect/token/introspect") // .contentType(MediaType.APPLICATION_FORM_URLENCODED) // .body(payload) // .retrieve() // .body(KeycloakIntrospectResponse.class); } @Override public KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken) { var oidcUserRequest = new OidcUserRequest(authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(), oidcIdToken); var oidcUser = this.keycloakUserService.loadUser(oidcUserRequest); var keycloakUserInfo = new KeycloakUserInfo(); keycloakUserInfo.setName(oidcUser.getClaimAsString("name")); keycloakUserInfo.setPreferredUsername(oidcUser.getPreferredUsername()); keycloakUserInfo.setFirstname(oidcUser.getGivenName()); keycloakUserInfo.setLastname(oidcUser.getFamilyName()); keycloakUserInfo.setEmail(oidcUser.getEmail()); keycloakUserInfo.setEmailVerified(oidcUser.getEmailVerified()); keycloakUserInfo.setPhoneNumber(oidcUser.getPhoneNumber()); keycloakUserInfo.setPhoneNumberVerified(oidcUser.getPhoneNumberVerified()); return keycloakUserInfo; } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakClient.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.core.oidc.OidcIdToken; public interface KeycloakClient { KeycloakIntrospectResponse introspect(String token); KeycloakUserInfo userInfo(OAuth2AuthorizedClient authorizedClient, OidcIdToken oidcIdToken); } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakIntrospectResponse.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class KeycloakIntrospectResponse { public String active; @JsonProperty("token_type") public String tokenType; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakServiceException.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import lombok.Data; @Data public class KeycloakServiceException extends RuntimeException { private final String errorBody; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/keycloakclient/KeycloakUserInfo.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.keycloakclient; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class KeycloakUserInfo { @JsonProperty("name") private String name; @JsonProperty("preferred_username") private String preferredUsername; @JsonProperty("family_name") private String lastname; @JsonProperty("given_name") private String firstname; @JsonProperty("email") private String email; @JsonProperty("email_verified") private Boolean emailVerified; @JsonProperty("phone_number") private String phoneNumber; @JsonProperty("phone_number_verified") private Boolean phoneNumberVerified; } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/support/security/KeycloakLogoutHandler.java ================================================ package com.github.thomasdarimont.keycloak.webapp.support.security; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.stereotype.Component; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Component public class KeycloakLogoutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { var principal = (DefaultOidcUser) auth.getPrincipal(); var idToken = principal.getIdToken(); log.info("Propagate logout to keycloak for user. userId={}", idToken.getSubject()); var issuerUri = idToken.getIssuer().toString(); var idTokenValue = idToken.getTokenValue(); var defaultRedirectUri = generateAppUri(request); var logoutUrl = createKeycloakLogoutUrl(issuerUri, idTokenValue, defaultRedirectUri); try { response.sendRedirect(logoutUrl); } catch (IOException e) { log.error("Could not send redirect to logoutUrl", e); } } private String generateAppUri(HttpServletRequest request) { var hostname = request.getServerName() + ":" + request.getServerPort(); var isStandardHttps = "https".equals(request.getScheme()) && request.getServerPort() == 443; var isStandardHttp = "http".equals(request.getScheme()) && request.getServerPort() == 80; if (isStandardHttps || isStandardHttp) { hostname = request.getServerName(); } return request.getScheme() + "://" + hostname + request.getContextPath() + "/"; } private String createKeycloakLogoutUrl(String issuerUri, String idTokenValue, String defaultRedirectUri) { return issuerUri + "/protocol/openid-connect/logout?id_token_hint=" + idTokenValue + "&post_logout_redirect_uri=" + defaultRedirectUri; } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/AuthController.java ================================================ package com.github.thomasdarimont.keycloak.webapp.web; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.github.thomasdarimont.keycloak.webapp.support.TokenIntrospector; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Data; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.net.URI; import java.util.HashMap; import java.util.Map; @RestController @RequiredArgsConstructor class AuthController { private final TokenIntrospector tokenIntrospector; private final ClientRegistrationRepository clientRegistrationRepository; private final AuthorizationRequestRepository authorizationRequestRepository; /** * Init anonymous registration via: https://apps.acme.test:4633/webapp/auth/register * * @param request * @param response * @return */ @GetMapping("/auth/register") public ResponseEntity register(HttpServletRequest request, HttpServletResponse response) { var resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, request.getContextPath()); resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce()); var authzRequest = resolver // .resolve(request, "keycloak"); authorizationRequestRepository.saveAuthorizationRequest(authzRequest, request, response); var registerUriString = authzRequest.getAuthorizationRequestUri() // .replaceFirst("/openid-connect/auth", "/openid-connect/registrations"); var registerUri = URI.create(registerUriString); return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT).location(registerUri).build(); } @GetMapping("/auth/check-session") public ResponseEntity checkSession(Authentication auth, HttpServletRequest request, HttpServletResponse response) throws ServletException { var introspectionResult = tokenIntrospector.introspectToken(auth); if (introspectionResult == null || !introspectionResult.isActive()) { request.logout(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } return ResponseEntity.ok().build(); } @Data static class IntrospectionResponse { private boolean active; private Map data = new HashMap<>(); @JsonAnySetter public void setDataEntry(String key, Object value) { data.put(key, value); } } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/java/com/github/thomasdarimont/keycloak/webapp/web/UiController.java ================================================ package com.github.thomasdarimont.keycloak.webapp.web; import com.github.thomasdarimont.keycloak.webapp.domain.ApplicationEntry; import com.github.thomasdarimont.keycloak.webapp.domain.CredentialEntry; import com.github.thomasdarimont.keycloak.webapp.domain.SettingEntry; import com.github.thomasdarimont.keycloak.webapp.domain.UserProfile; import com.github.thomasdarimont.keycloak.webapp.support.TokenAccessor; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; @Controller @RequiredArgsConstructor class UiController { private final TokenAccessor tokenAccessor; @GetMapping("/") public String showIndex(Model model) { return "index"; } @GetMapping("/profile") public String showProfile(Model model, Authentication auth) { OAuth2AccessToken accessToken = tokenAccessor.getAccessToken(auth); var oauth = (OAuth2AuthenticationToken)auth; var oauthUser = (DefaultOidcUser)oauth.getPrincipal(); var profile = new UserProfile(); profile.setFirstname(oauthUser.getGivenName()); profile.setLastname(oauthUser.getFamilyName()); profile.setEmail(oauthUser.getEmail()); model.addAttribute("profile", profile); return "profile"; } @GetMapping("/settings") public String showSettings(Model model) { var setting1 = new SettingEntry(); setting1.setName("setting1"); setting1.setValue("value1"); setting1.setType("string"); var setting2 = new SettingEntry(); setting2.setName("setting2"); setting2.setValue("on"); setting2.setType("boolean"); var settings = List.of(setting1, setting2); model.addAttribute("settings", settings); return "settings"; } @GetMapping("/security") public String showSecurity(Model model) { var credential1 = new CredentialEntry(); credential1.setId("cred1"); credential1.setLabel("value1"); credential1.setType("password"); var credential2 = new CredentialEntry(); credential2.setId("cred2"); credential2.setLabel("value2"); credential2.setType("totp"); var credentials = List.of(credential1, credential2); model.addAttribute("credentials", credentials); return "security"; } @GetMapping("/applications") public String showApplications(Model model) { var appEntry1 = new ApplicationEntry(); appEntry1.setClientId("app1"); appEntry1.setName("App 1"); appEntry1.setUrl("http://localhost/app1"); var appEntry2 = new ApplicationEntry(); appEntry2.setClientId("app2"); appEntry2.setName("App 2"); appEntry2.setUrl("http://localhost/app2"); var apps = List.of(appEntry1, appEntry2); model.addAttribute("apps", apps); return "applications"; } } ================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/application.yml ================================================ server: port: 4633 ssl: enabled: true key-store: ../../config/stage/dev/tls/acme.test+1.p12 key-store-password: changeit key-store-type: PKCS12 servlet: context-path: /webapp error: include-stacktrace: never spring: thymeleaf: cache: false security: oauth2: client: provider: keycloak: issuerUri: https://id.acme.test:8443/auth/realms/acme-internal user-name-attribute: preferred_username registration: keycloak: client-id: 'frontend-webapp-springboot' client-secret: 'secret' client-authentication-method: client_secret_post authorizationGrantType: authorization_code redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' scope: openid logging: level: root: info org: springframework: web: info # security: trace ================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/templates/applications.html ================================================

Applications

================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/templates/fragments.html ================================================ Account Console ================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/templates/index.html ================================================ Account Console

This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or information.


It uses utility classes for typography and spacing to space content out within the larger container.

Learn more

================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/templates/profile.html ================================================

Profile

Full Name

Johnatan Smith


Email

example@example.com


================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/templates/security.html ================================================

Security

================================================ FILE: apps/frontend-webapp-springboot3/src/main/resources/templates/settings.html ================================================

Settings

================================================ FILE: apps/frontend-webapp-springboot3/src/test/java/com/github/thomasdarimont/keycloak/cac/WebApplicationTestsSpringBoot.java ================================================ package com.github.thomasdarimont.keycloak.cac; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class WebApplicationTestsSpringBoot { @Test void contextLoads() { } } ================================================ FILE: apps/java-opa-embedded/.gitignore ================================================ .idea/ scratch/ target/ ================================================ FILE: apps/java-opa-embedded/jd-gui.cfg ================================================ false com.apple.laf.AquaLookAndFeel /Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded/target/java-opa-embedded-1.0.0.0-SNAPSHOT.jar /Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded/target/java-opa-embedded-1.0.0.0-SNAPSHOT.jar /Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded/target/java-opa-embedded-1.0.0.0-SNAPSHOT.jar /Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded /Users/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-example/apps/java-opa-embedded 0xFF6666 1.1.3 ================================================ FILE: apps/java-opa-embedded/pom.xml ================================================ 4.0.0 com.github.thomasdarimont.keycloak java-opa-embedded 1.0.0.0-SNAPSHOT 21 21 UTF-8 com.styra.opa opa-java-wasm 1.0.3 org.apache.maven.plugins maven-antrun-plugin 3.1.0 process-resources run org.codehaus.mojo exec-maven-plugin 3.5.1 demo.OpaEmbeddedDemo ================================================ FILE: apps/java-opa-embedded/readme.md ================================================ Opa Embedded Policy Example --- # Compile REGO Policy to WASM ``` opa build \ -t wasm \ -o scratch/bundle.tar.gz \ -e app/rbac \ src/main/resources/policy/app/rbac/policy.rego ``` # Extract compiled wasm module ``` tar xzf scratch/bundle.tar.gz -C src/main/resources/policy "/policy.wasm" ``` # Build ``` mvn clean package ``` # Run ``` mvn exec:java ``` ================================================ FILE: apps/java-opa-embedded/src/main/java/demo/OpaEmbeddedDemo.java ================================================ package demo; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.styra.opa.wasm.DefaultMappers; import com.styra.opa.wasm.OpaPolicy; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; /** * See: https://github.com/StyraOSS/opa-java-wasm */ public class OpaEmbeddedDemo { public static void main(String[] args) throws Exception { var jsonMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); var policy = OpaPolicy.builder() .withMaxMemory(16) // 16 pages a 64kb .withJsonMapper(DefaultMappers.jsonMapper) .withPolicy(Paths.get(OpaEmbeddedDemo.class.getResource("../policy/policy.wasm").toURI())) .build(); String data = Files.readString(Paths.get(OpaEmbeddedDemo.class.getResource("../data/user_roles.json").toURI())); policy.data(data); var requests = List.of( """ { "user": "alice", "action": "read", "object": "id123", "type": "dog" } """, """ { "user": "bob", "action": "read", "object": "id123", "type": "dog" } """); for (var input : requests) { System.out.println("#####"); System.out.println("Input: " + input); String result = policy.evaluate(input); System.out.println("Output: " + jsonMapper.writeValueAsString(jsonMapper.readValue(result, Object.class))); System.out.println(); } } } ================================================ FILE: apps/java-opa-embedded/src/main/resources/data/user_roles.json ================================================ { "user_roles": { "alice": [ "admin" ], "bob": [ "employee", "billing" ], "eve": [ "customer" ] }, "role_grants": { "customer": [ { "action": "read", "type": "dog" }, { "action": "read", "type": "cat" }, { "action": "adopt", "type": "dog" }, { "action": "adopt", "type": "cat" } ], "employee": [ { "action": "read", "type": "dog" }, { "action": "read", "type": "cat" }, { "action": "update", "type": "dog" }, { "action": "update", "type": "cat" } ], "billing": [ { "action": "read", "type": "finance" }, { "action": "update", "type": "finance" } ] } } ================================================ FILE: apps/java-opa-embedded/src/main/resources/policy/app/rbac/policy.rego ================================================ # Role-based Access Control (RBAC) # -------------------------------- # # This example defines an RBAC model for a Pet Store API. The Pet Store API allows # users to look at pets, adopt them, update their stats, and so on. The policy # controls which users can perform actions on which resources. The policy implements # a classic Role-based Access Control model where users are assigned to roles and # roles are granted the ability to perform some action(s) on some type of resource. # # This example shows how to: # # * Define an RBAC model in Rego that interprets role mappings represented in JSON. # * Iterate/search across JSON data structures (e.g., role mappings) # # For more information see: # # * Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/ # * Rego Iteration: https://www.openpolicyagent.org/docs/latest/#iteration package app.rbac # By default, deny requests. default allow := false # Allow admins to do anything. allow if user_is_admin # Allow the action if the user is granted permission to perform the action. allow if { # Find grants for the user. some grant in user_is_granted # Check if the grant permits the action. input.action == grant.action input.type == grant.type } # user_is_admin is true if "admin" is among the user's roles as per data.user_roles user_is_admin if "admin" in data.user_roles[input.user] # user_is_granted is a set of grants for the user identified in the request. # The `grant` will be contained if the set `user_is_granted` for every... user_is_granted contains grant if { # `role` assigned an element of the user_roles for this user... some role in data.user_roles[input.user] # `grant` assigned a single grant from the grants list for 'role'... some grant in data.role_grants[role] } ================================================ FILE: apps/jwt-client-authentication/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/jwt-client-authentication/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar ================================================ FILE: apps/jwt-client-authentication/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /usr/local/etc/mavenrc ] ; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`\\unset -f command; \\command -v java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" else jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/jwt-client-authentication/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: apps/jwt-client-authentication/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.4.7 com.github.thomasdarimont.keycloak jwt-client-authentication 0.0.1-SNAPSHOT jwt-client-authentication jwt-client-authentication 17 1.18.38 org.springframework.boot spring-boot-starter-oauth2-resource-server org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/jwt-client-authentication/readme.md ================================================ Example for JWT Client Authentication with Keycloak --- # Generate Public / Private Key Pair ``` openssl req \ -x509 \ -newkey rsa:4096 \ -keyout client_key.pem \ -out client_cert.pem \ -days 365 \ -nodes \ -subj "/CN=acme-service-client-jwt-auth" ``` ================================================ FILE: apps/jwt-client-authentication/src/main/java/demo/jwtclientauth/JwtClientAuthApp.java ================================================ package demo.jwtclientauth; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSObject; import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.util.Base64URL; import com.nimbusds.jose.util.X509CertUtils; import lombok.extern.slf4j.Slf4j; import org.apache.tomcat.util.codec.binary.Base64; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.time.Duration; import java.time.Instant; import java.util.Map; import java.util.UUID; import java.util.function.Function; /** * Example for client authentication via private_key_jwt https://oauth.net/private-key-jwt/ */ @Slf4j @SpringBootApplication public class JwtClientAuthApp { public static void main(String[] args) { new SpringApplicationBuilder(JwtClientAuthApp.class).web(WebApplicationType.NONE).run(args); } @Bean CommandLineRunner cli() { return args -> { log.info("Jwt Client Authentication"); var clientId = "acme-service-client-jwt-auth"; var issuer = "https://id.acme.test:8443/auth/realms/acme-internal"; var issuedAt = Instant.now(); var tokenLifeTime = Duration.ofSeconds(5); var clientJwtPayload = Map.ofEntries( // Map.entry("iss", clientId), // Map.entry("sub", clientId), // Map.entry("aud", issuer), // see: aud in private_key_jwt in https://openid.net/specs/openid-connect-core-1_0-36.html#rfc.section.9 Map.entry("iat", issuedAt.getEpochSecond()), // Map.entry("exp", issuedAt.plus(tokenLifeTime).getEpochSecond()), // Map.entry("jti", UUID.randomUUID().toString()) // ); { // Signed JWT example // generate Signed JWT var clientJwtToken = generateClientAssertionSignedWithPrivateKey(clientJwtPayload, // "apps/jwt-client-authentication/client_cert.pem", // "apps/jwt-client-authentication/client_key.pem" // ); log.info("Client JWT Token: {}", clientJwtToken); // use clientjwt to request token for service var accessTokenResponse = requestToken(issuer, clientJwtToken); log.info("AccessToken: {}", accessTokenResponse.get("access_token")); // use clientjwt perform PAR request // var requestUri = requestPAR(issuer, clientId, UUID.randomUUID().toString(), "https://www.keycloak.org/app/", "openid profile", clientJwtToken); // log.info("RequestUri: {}", requestUri); } { // Signed JWT with Client Secret example // generate Signed JWT with client secret // String clientSecret = "8FKyMMDOiBp2CIdu4TtssY6HRP5nHRsI"; // var clientJwtToken = generateTokenSignedWithClientSecret(clientJwtPayload, clientSecret, // "apps/jwt-client-authentication/client_cert.pem"); // log.info("Client JWT Token: {}", clientJwtToken); // use Signed JWT with client secret to request token for service // var accessTokenResponse = requestToken(issuer, clientJwtToken); // log.info("AccessToken: {}", accessTokenResponse.get("access_token")); // use clientjwt perform PAR request // var requestUri = requestPAR(issuer, clientId, UUID.randomUUID().toString(), "https://www.keycloak.org/app/", "openid profile", clientJwtToken); // log.info("RequestUri: {}", requestUri); } }; } private Map requestToken(String issuer, String clientAssertion) { var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("grant_type", "client_credentials"); requestBody.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); requestBody.add("client_assertion", clientAssertion); var tokenUrl = issuer + "/protocol/openid-connect/token"; var responseEntity = rt.postForEntity(tokenUrl, new HttpEntity<>(requestBody, headers), Map.class); var accessTokenResponse = responseEntity.getBody(); return accessTokenResponse; } private String requestPAR(String issuer, String clientId, String nonce, String redirectUri, String scope, String clientJwtToken) { var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("response_type", "code"); requestBody.add("client_id", clientId); requestBody.add("nonce", nonce); requestBody.add("redirect_uri", redirectUri); requestBody.add("scope", scope); requestBody.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); requestBody.add("client_assertion", clientJwtToken); var tokenUrl = issuer + "/protocol/openid-connect/ext/par/request"; var responseEntity = rt.postForEntity(tokenUrl, new HttpEntity<>(requestBody, headers), Map.class); var parResponse = responseEntity.getBody(); return String.valueOf(parResponse.get("request_uri")); } private String generateClientAssertionSignedWithPrivateKey(Map clientJwtPayload, String certLocation, String keyLocation) { try { // x5t header log.info("Payload: {}", new ObjectMapper().writeValueAsString(clientJwtPayload)); var cert = parseCertificate(certLocation); var privateKey = readPrivateKeyFile(keyLocation); var base64URL = createKeyThumbprint(cert, "SHA-1"); var jwsObject = new JWSObject(new JWSHeader .Builder(JWSAlgorithm.RS256) .type(JOSEObjectType.JWT) // .keyID("mykey") // explicit kid // .x509CertThumbprint(base64URL) // SHA-1 .x509CertSHA256Thumbprint(base64URL) // SHA256 .build(), new Payload(clientJwtPayload)); var signer = new RSASSASigner(privateKey); jwsObject.sign(signer); var clientAssertion = jwsObject.serialize(); return clientAssertion; } catch (Exception e) { throw new RuntimeException(e); } } private String generateClientAssertionSignedWithClientSecret(Map clientJwtPayload, String clientSecret, String certLocation) { var cert = parseCertificate(certLocation); var base64URL = createKeyThumbprint(cert, "SHA-1"); var jwsObject = new JWSObject(new JWSHeader .Builder(JWSAlgorithm.HS256) .type(JOSEObjectType.JWT) // .x509CertThumbprint(base64URL) // SHA-1 .x509CertSHA256Thumbprint(base64URL) // SHA256 .build(), new Payload(clientJwtPayload)); try { var signer = new MACSigner(clientSecret); jwsObject.sign(signer); } catch (JOSEException e) { throw new RuntimeException(e); } var clientAssertion = jwsObject.serialize(); return clientAssertion; } private String generateClientSignedJwtToken(String clientId, String issuer, Instant issuedAt, Duration tokenLifeTime, Function, String> jwtGenerator) throws JsonProcessingException, JOSEException { var clientJwtPayload = Map.ofEntries( // Map.entry("iss", clientId), // Map.entry("sub", clientId), // Map.entry("aud", issuer), // Map.entry("iat", issuedAt.getEpochSecond()), // Map.entry("exp", issuedAt.plus(tokenLifeTime).getEpochSecond()), // Map.entry("jti", UUID.randomUUID().toString()) // ); return jwtGenerator.apply(clientJwtPayload); } private X509Certificate parseCertificate(String path) { try { var cert = X509CertUtils.parse(Files.readString(Path.of(path), Charset.defaultCharset())); return cert; } catch (IOException e) { throw new RuntimeException(e); } } private static Base64URL createKeyThumbprint(X509Certificate cert, String hashAlgorithm) { try { RSAKey rsaKey = RSAKey.parse(cert); return rsaKey.computeThumbprint(hashAlgorithm); } catch (JOSEException e) { throw new RuntimeException(e); } } static RSAPrivateKey readPrivateKeyFile(String path) { try { var key = Files.readString(Path.of(path), Charset.defaultCharset()); var privateKeyPEM = key // .replace("-----BEGIN PRIVATE KEY-----", "") // .replaceAll(System.lineSeparator(), "") // .replace("-----END PRIVATE KEY-----", ""); var encodedBytes = Base64.decodeBase64(privateKeyPEM); var keySpec = new PKCS8EncodedKeySpec(encodedBytes); return (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(keySpec); } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { throw new RuntimeException(e); } } } ================================================ FILE: apps/jwt-client-authentication/src/main/resources/application.properties ================================================ ================================================ FILE: apps/keycloak-js/package.json ================================================ { "name": "keycloak-js", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "keycloak-js": "^26.1.2" } } ================================================ FILE: apps/keycloak-js/readme.md ================================================ Keycloak JS Resource --- # Build ``` npm install && cp node_modules/keycloak-js/lib/*.js ../site/lib/keycloak-js ``` ================================================ FILE: apps/oauth2-proxy/Dockerfile ================================================ FROM quay.io/oauth2-proxy/oauth2-proxy:v7.4.0-amd64 USER 0 COPY --chown=65532:0 "./acme.test+1.pem" /cert.pem COPY --chown=65532:0 "./acme.test+1-key.pem" /cert.key USER 65532 ================================================ FILE: apps/oauth2-proxy/app/main.go ================================================ package main import ( "fmt" "html/template" "net/http" "strings" ) func greet(w http.ResponseWriter, r *http.Request) { headers := make(map[string]string) for name, values := range r.Header { if len(values) > 1 { headers[name] = strings.Join(values, ", ") } else { headers[name] = values[0] } } authorization := r.Header.Get("Authorization") idToken := strings.Split(authorization, "Bearer ")[1] data := &struct { Username string Email string Roles string Headers map[string]string LogoutUri string }{ Username: r.Header.Get("X-Forwarded-Preferred-Username"), Email: r.Header.Get("X-Forwarded-Email"), Roles: r.Header.Get("X-Forwarded-Groups"), Headers: headers, // oauth2-proxy logout URL uses Keycloaks end_session endpoint LogoutUri: "/oauth2/sign_out?rd=https%3A%2F%2Fid.acme.test%3A8443%2Fauth%2Frealms%2Facme-internal%2Fprotocol%2Fopenid-connect%2Flogout%3Fclient_id%3Dapp-oauth2-proxy%26post_logout_redirect_uri%3Dhttps%3A%2F%2Fapps.acme.test%3A6443%2F%26id_token_hint%3D" + idToken, } htmlTemplate := `

app-oauth2-proxy

Greeting

Hello {{.Username}} Logout
  • Email: {{.Email}}
  • Roles: {{.Roles}}

Headers

    {{range $name, $value := .Headers}}
  • {{$name}}: {{$value}}
  • {{end}}
` t, _ := template.New("greet").Parse(htmlTemplate) t.Execute(w, data) } func main() { http.HandleFunc("/", greet) addr := ":6080" fmt.Printf("Listening on http://%s/\n", addr) fmt.Printf("External address https://apps.acme.test:6443/\n", addr) http.ListenAndServe(addr, nil) } ================================================ FILE: apps/oauth2-proxy/docker-compose.yml ================================================ services: proxy: # image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0-amd64 build: context: "../../config/stage/dev/tls" dockerfile: "../../../../apps/oauth2-proxy/Dockerfile" ports: - 6443:6443 volumes: - ./oauth2-proxy.cfg:/oauth2-proxy.cfg:z command: - "--config" - "/oauth2-proxy.cfg" - "--tls-cert-file=/cert.pem" - "--tls-key-file=/cert.key" extra_hosts: # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal - "id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" upstream-app: image: golang:1.19.3-alpine volumes: - ./app/main.go:/main.go:z command: - "go" - "run" - "/main.go" ================================================ FILE: apps/oauth2-proxy/oauth2-proxy.cfg ================================================ https_address = "0.0.0.0:6443" # See: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/#keycloak-oidc-auth-provider provider = "keycloak-oidc" oidc_issuer_url="https://id.acme.test:8443/auth/realms/acme-internal" redirect_url="https://apps.acme.test:6443/oauth2/callback" #-keycloak-group= client_id = "app-oauth2-proxy" client_secret = "secret" ## Enable PKCE code_challenge_method="S256" ## Allow account aud claim oidc_extra_audiences="account" scope = "openid profile email" # Automatically redirect to Keycloak skip_provider_button=true whitelist_domains="*.acme.test:8443" ssl_insecure_skip_verify = "true" ssl_upstream_insecure_skip_verify= "true" cookie_secret = "1234567890123456" cookie_secure = "false" email_domains = "*" ## Pass OAuth Access token to upstream via "X-Forwarded-Access-Token" pass_access_token = true ## Pass OIDC IDToken via Authorization header pass_authorization_header= true ## pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream pass_basic_auth = true pass_user_headers = true ## pass the request Host Header to upstream ## when disabled the upstream Host is used as the Host Header pass_host_header = true ## the http url(s) of the upstream endpoint. If multiple, routing is based on path # nc -l -p 40002 upstreams = [ "http://upstream-app:6080/" ] ================================================ FILE: apps/oauth2-proxy/readme.md ================================================ oauth2-proxy example app --- Simple usage example for securing an app behind [oauth2-proxy](https://oauth2-proxy.github.io/) with Keycloak and OpenID Connect. # Run Ensure that the acme-keycloak example environment is running. ``` docker compose up ``` Browse to https://apps.acme.test:6443/ ================================================ FILE: apps/offline-session-client/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.4.7 com.github.thomasdarimont.apps offline-session-client 1.0-SNAPSHOT 17 ${java.version} ${java.version} org.springframework.boot spring-boot-starter-web org.apache.httpcomponents.client5 httpclient5 org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.boot spring-boot-maven-plugin ================================================ FILE: apps/offline-session-client/src/main/java/demo/OfflineSessionClient.java ================================================ package demo; import com.fasterxml.jackson.annotation.JsonAnySetter; import lombok.Builder; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; import org.apache.hc.core5.ssl.SSLContexts; import org.apache.hc.core5.ssl.TrustStrategy; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import javax.net.ssl.SSLContext; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * keytool -importcert -noprompt -cacerts -alias "id.acme.test" -storepass changeit -file * './config/stage/dev/tls/acme.test+1.pem' */ @Slf4j @SpringBootApplication public class OfflineSessionClient { public static void main(String[] args) { new SpringApplicationBuilder(OfflineSessionClient.class).web(WebApplicationType.NONE).run(args); } @Bean CommandLineRunner clr(TlsRestTemplateCustomizer tlsRestTemplateCustomizer) { return args -> { var oauthInfo = OAuthInfo.builder() // .issuer("https://id.acme.test:8443/auth/realms/acme-internal") // .clientId("app-mobile") // // openid scope required for userinfo! // profile scope allows to read profile info // offline_access scope instructs keycloak to create an offline_session in the KC database .scope("openid profile offline_access") // .grantType("password") // for the sake of the demo we use grant_type=password .username("tester") // .password("test") // .build(); var rt = new RestTemplateBuilder(tlsRestTemplateCustomizer).build(); var oauthClient = new OAuthClient(rt, oauthInfo, 3); var offlineAccessValid = oauthClient.loadOfflineToken(true, "apps/offline-session-client/data/offline_token"); log.info("Offline access valid: {}", offlineAccessValid); if (Arrays.asList(args).contains("--logout")) { log.info("Logout started..."); var loggedOut = oauthClient.logout(); log.info("Logout success: {}", loggedOut); System.exit(0); return; } var token = oauthClient.getAccessToken(); log.info("Token: {}", token); var userInfo = oauthClient.fetchUserInfo(); log.info("UserInfo: {}", userInfo); }; } @Builder @Data static class OAuthInfo { final String issuer; final String clientId; final String clientSecret; final String grantType; final String scope; final String username; final String password; public String getUserInfoUrl() { return getIssuer() + "/protocol/openid-connect/userinfo"; } public String getTokenUrl() { return getIssuer() + "/protocol/openid-connect/token"; } public String getLogoutUrl() { return getIssuer() + "/protocol/openid-connect/logout"; } } @Slf4j @RequiredArgsConstructor static class OAuthClient { private final RestTemplate rt; private final OAuthInfo oauthInfo; private final int tokenMinSecondsValid; private AccessTokenResponse accessTokenResponse; private Path offlineTokenPath; public boolean loadOfflineToken(boolean obtainIfMissing, String offlineTokenLocation) { File offlineTokenFile = new File(offlineTokenLocation); this.offlineTokenPath = offlineTokenFile.toPath(); if (offlineTokenFile.exists()) { log.info("Found existing offline token..."); String offlineToken; try { offlineToken = Files.readString(offlineTokenPath); } catch (IOException e) { log.error("Could not read offline_token", e); return false; } var offlineRefreshTokenValid = false; try { offlineRefreshTokenValid = doRefreshToken(offlineToken); } catch (HttpClientErrorException hcee) { if (hcee.getStatusCode().value() == 400 && hcee.getMessage() != null && hcee.getMessage().contains("invalid_grant")) { log.info("Detected stale refresh token"); } else { throw new RuntimeException(hcee); } } if (offlineRefreshTokenValid) { log.info("Refreshed with existing offline token."); return offlineRefreshTokenValid; } else { log.warn("Refresh with existing offline token failed"); try { log.warn("Removing stale offline token"); Files.delete(offlineTokenPath); log.warn("Removed stale offline token"); } catch (IOException e) { log.error("Failed to remove stale offline token", e); return false; } } } if (!obtainIfMissing) { return false; } boolean success = this.sendOfflineTokenRequest(); if (success) { log.info("Obtain new offline token..."); try { Files.write(offlineTokenPath, accessTokenResponse.getRefresh_token().getBytes(StandardCharsets.UTF_8)); return true; } catch (IOException e) { log.error("Could not write offline_token", e); } } return false; } public UserInfoResponse fetchUserInfo() { ensureTokenValidSeconds(tokenMinSecondsValid); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBearerAuth(accessTokenResponse.getAccess_token()); log.info("Fetching data form userinfo: {}", oauthInfo.getUserInfoUrl()); var userInfoResponseEntity = rt.exchange(oauthInfo.getUserInfoUrl(), HttpMethod.GET, new HttpEntity<>(headers), UserInfoResponse.class); return userInfoResponseEntity.getBody(); } private boolean doRefreshToken(String refreshToken) { var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", oauthInfo.clientId); requestBody.add("grant_type", "refresh_token"); requestBody.add("refresh_token", refreshToken); requestBody.add("scope", oauthInfo.scope); var responseEntity = rt.postForEntity(oauthInfo.getTokenUrl(), new HttpEntity<>(requestBody, headers), AccessTokenResponse.class); if (!responseEntity.getStatusCode().is2xxSuccessful()) { return false; } AccessTokenResponse body = responseEntity.getBody(); if (body == null || body.getError() != null) { return false; } accessTokenResponse = body; return true; } private boolean sendOfflineTokenRequest() { var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", oauthInfo.clientId); requestBody.add("grant_type", oauthInfo.grantType); requestBody.add("username", oauthInfo.username); requestBody.add("password", oauthInfo.password); requestBody.add("scope", oauthInfo.scope); var responseEntity = rt.postForEntity(oauthInfo.getTokenUrl(), new HttpEntity<>(requestBody, headers), AccessTokenResponse.class); if (!responseEntity.getStatusCode().is2xxSuccessful()) { return false; } AccessTokenResponse body = responseEntity.getBody(); if (body == null || body.getError() != null) { return false; } accessTokenResponse = body; return true; } public void ensureTokenValidSeconds(int minSecondsValid) { Objects.requireNonNull(accessTokenResponse, "accessTokenResponse"); long accessTokenExpiresAtSeconds = accessTokenResponse.getCreatedAtSeconds() + accessTokenResponse.getExpires_in(); long nowSeconds = System.currentTimeMillis() / 1000; long remainingLifetimeSeconds = accessTokenExpiresAtSeconds - nowSeconds; if (remainingLifetimeSeconds < minSecondsValid) { doRefreshToken(accessTokenResponse.refresh_token); } } public String getAccessToken() { ensureTokenValidSeconds(tokenMinSecondsValid); return this.accessTokenResponse.access_token; } public boolean logout() { if (accessTokenResponse == null || accessTokenResponse.getRefresh_token() == null) { log.error("Could not logout offline-client: missing offline token"); return false; } var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", oauthInfo.clientId); requestBody.add("refresh_token", accessTokenResponse.getRefresh_token()); var responseEntity = rt.postForEntity(oauthInfo.getLogoutUrl(), new HttpEntity<>(requestBody, headers), Map.class); if (!responseEntity.getStatusCode().is2xxSuccessful()) { log.error("Could not logout offline-client: logout failed"); return false; } if (offlineTokenPath != null) { try { Files.delete(offlineTokenPath); } catch (IOException e) { log.error("Could not delete offline_token", e); } } accessTokenResponse = null; return true; } } @Slf4j @Component @RequiredArgsConstructor static class TlsRestTemplateCustomizer implements RestTemplateCustomizer { @Override public void customize(RestTemplate restTemplate) { var sslConnectionSocketFactory = SSLConnectionSocketFactoryBuilder.create().setSslContext(createSslContext()).build(); var cm = PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(sslConnectionSocketFactory) // .build(); var httpClient = HttpClients.custom().setConnectionManager(cm).build(); var requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); restTemplate.setRequestFactory(requestFactory); } private SSLContext createSslContext() { SSLContext sslContext = null; try { TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true; sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build(); } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { e.printStackTrace(); } return sslContext; } } @Data static class AccessTokenResponse { final long createdAtSeconds = System.currentTimeMillis() / 1000; String access_token; String refresh_token; String error; int expires_in; Map metadata = new HashMap<>(); @JsonAnySetter public void setMetadata(String key, Object value) { metadata.put(key, value); } } @Data static class UserInfoResponse { Map userdata = new HashMap<>(); @JsonAnySetter public void setMetadata(String key, Object value) { userdata.put(key, value); } } } ================================================ FILE: apps/site/accountdeleted.html ================================================

Acme Account Deleted

================================================ FILE: apps/site/imprint.html ================================================

Acme Imprint

================================================ FILE: apps/site/lib/keycloak-js/keycloak-authz.js ================================================ /* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ var KeycloakAuthorization = function (keycloak, options) { var _instance = this; this.rpt = null; // Only here for backwards compatibility, as the configuration is now loaded on demand. // See: // - https://github.com/keycloak/keycloak/pull/6619 // - https://issues.redhat.com/browse/KEYCLOAK-10894 // TODO: Remove both `ready` property and `init` method in a future version Object.defineProperty(this, 'ready', { get() { console.warn("The 'ready' property is deprecated and will be removed in a future version. Initialization now happens automatically, using this property is no longer required."); return Promise.resolve(); }, }); this.init = () => { console.warn("The 'init()' method is deprecated and will be removed in a future version. Initialization now happens automatically, calling this method is no longer required."); }; /** @type {Promise | undefined} */ let configPromise; /** * Initializes the configuration or re-uses the existing one if present. * @returns {Promise} A promise that resolves when the configuration is loaded. */ async function initializeConfigIfNeeded() { if (_instance.config) { return _instance.config; } if (configPromise) { return await configPromise; } if (!keycloak.didInitialize) { throw new Error('The Keycloak instance has not been initialized yet.'); } configPromise = loadConfig(keycloak.authServerUrl, keycloak.realm); _instance.config = await configPromise; } /** * This method enables client applications to better integrate with resource servers protected by a Keycloak * policy enforcer using UMA protocol. * * The authorization request must be provided with a ticket. */ this.authorize = function (authorizationRequest) { this.then = async function (onGrant, onDeny, onError) { try { await initializeConfigIfNeeded(); } catch (error) { handleError(error, onError); return; } if (authorizationRequest && authorizationRequest.ticket) { var request = new XMLHttpRequest(); request.open('POST', _instance.config.token_endpoint, true); request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token); request.onreadystatechange = function () { if (request.readyState == 4) { var status = request.status; if (status >= 200 && status < 300) { var rpt = JSON.parse(request.responseText).access_token; _instance.rpt = rpt; onGrant(rpt); } else if (status == 403) { if (onDeny) { onDeny(); } else { console.error('Authorization request was denied by the server.'); } } else { if (onError) { onError(); } else { console.error('Could not obtain authorization data from server.'); } } } }; var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId + "&ticket=" + authorizationRequest.ticket; if (authorizationRequest.submitRequest != undefined) { params += "&submit_request=" + authorizationRequest.submitRequest; } var metadata = authorizationRequest.metadata; if (metadata) { if (metadata.responseIncludeResourceName) { params += "&response_include_resource_name=" + metadata.responseIncludeResourceName; } if (metadata.responsePermissionsLimit) { params += "&response_permissions_limit=" + metadata.responsePermissionsLimit; } } if (_instance.rpt && (authorizationRequest.incrementalAuthorization == undefined || authorizationRequest.incrementalAuthorization)) { params += "&rpt=" + _instance.rpt; } request.send(params); } }; return this; }; /** * Obtains all entitlements from a Keycloak Server based on a given resourceServerId. */ this.entitlement = function (resourceServerId, authorizationRequest) { this.then = async function (onGrant, onDeny, onError) { try { await initializeConfigIfNeeded(); } catch (error) { handleError(error, onError); return; } var request = new XMLHttpRequest(); request.open('POST', _instance.config.token_endpoint, true); request.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token); request.onreadystatechange = function () { if (request.readyState == 4) { var status = request.status; if (status >= 200 && status < 300) { var rpt = JSON.parse(request.responseText).access_token; _instance.rpt = rpt; onGrant(rpt); } else if (status == 403) { if (onDeny) { onDeny(); } else { console.error('Authorization request was denied by the server.'); } } else { if (onError) { onError(); } else { console.error('Could not obtain authorization data from server.'); } } } }; if (!authorizationRequest) { authorizationRequest = {}; } var params = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=" + keycloak.clientId; if (authorizationRequest.claimToken) { params += "&claim_token=" + authorizationRequest.claimToken; if (authorizationRequest.claimTokenFormat) { params += "&claim_token_format=" + authorizationRequest.claimTokenFormat; } } params += "&audience=" + resourceServerId; var permissions = authorizationRequest.permissions; if (!permissions) { permissions = []; } for (var i = 0; i < permissions.length; i++) { var resource = permissions[i]; var permission = resource.id; if (resource.scopes && resource.scopes.length > 0) { permission += "#"; for (var j = 0; j < resource.scopes.length; j++) { var scope = resource.scopes[j]; if (permission.indexOf('#') != permission.length - 1) { permission += ","; } permission += scope; } } params += "&permission=" + permission; } var metadata = authorizationRequest.metadata; if (metadata) { if (metadata.responseIncludeResourceName) { params += "&response_include_resource_name=" + metadata.responseIncludeResourceName; } if (metadata.responsePermissionsLimit) { params += "&response_permissions_limit=" + metadata.responsePermissionsLimit; } } if (_instance.rpt) { params += "&rpt=" + _instance.rpt; } request.send(params); }; return this; }; return this; }; /** * Obtains the configuration from the server. * @param {string} serverUrl The URL of the Keycloak server. * @param {string} realm The realm name. * @returns {Promise} A promise that resolves when the configuration is loaded. */ async function loadConfig(serverUrl, realm) { const url = `${serverUrl}/realms/${encodeURIComponent(realm)}/.well-known/uma2-configuration`; try { return await fetchJSON(url); } catch (error) { throw new Error('Could not obtain configuration from server.', { cause: error }); } } /** * Fetches the JSON data from the given URL. * @param {string} url The URL to fetch the data from. * @returns {Promise} A promise that resolves when the data is loaded. */ async function fetchJSON(url) { let response; try { response = await fetch(url); } catch (error) { throw new Error('Server did not respond.', { cause: error }); } if (!response.ok) { throw new Error('Server responded with an invalid status.'); } try { return await response.json(); } catch (error) { throw new Error('Server responded with invalid JSON.', { cause: error }); } } /** * @param {unknown} error * @param {((error: unknown) => void) | undefined} handler */ function handleError(error, handler) { if (handler) { handler(error); } else { console.error(message, error); } } export default KeycloakAuthorization; ================================================ FILE: apps/site/lib/keycloak-js/keycloak.js ================================================ /* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ function Keycloak (config) { if (!(this instanceof Keycloak)) { throw new Error("The 'Keycloak' constructor must be invoked with 'new'.") } if (typeof config !== 'string' && !isObject(config)) { throw new Error("The 'Keycloak' constructor must be provided with a configuration object, or a URL to a JSON configuration file."); } if (isObject(config)) { const requiredProperties = 'oidcProvider' in config ? ['clientId'] : ['url', 'realm', 'clientId']; for (const property of requiredProperties) { if (!config[property]) { throw new Error(`The configuration object is missing the required '${property}' property.`); } } } var kc = this; var adapter; var refreshQueue = []; var callbackStorage; var loginIframe = { enable: true, callbackList: [], interval: 5 }; kc.didInitialize = false; var useNonce = true; var logInfo = createLogger(console.info); var logWarn = createLogger(console.warn); if (!globalThis.isSecureContext) { logWarn( "[KEYCLOAK] Keycloak JS must be used in a 'secure context' to function properly as it relies on browser APIs that are otherwise not available.\n" + "Continuing to run your application insecurely will lead to unexpected behavior and breakage.\n\n" + "For more information see: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" ); } kc.init = function (initOptions = {}) { if (kc.didInitialize) { throw new Error("A 'Keycloak' instance can only be initialized once."); } kc.didInitialize = true; kc.authenticated = false; callbackStorage = createCallbackStorage(); var adapters = ['default', 'cordova', 'cordova-native']; if (adapters.indexOf(initOptions.adapter) > -1) { adapter = loadAdapter(initOptions.adapter); } else if (typeof initOptions.adapter === "object") { adapter = initOptions.adapter; } else { if (window.Cordova || window.cordova) { adapter = loadAdapter('cordova'); } else { adapter = loadAdapter(); } } if (typeof initOptions.useNonce !== 'undefined') { useNonce = initOptions.useNonce; } if (typeof initOptions.checkLoginIframe !== 'undefined') { loginIframe.enable = initOptions.checkLoginIframe; } if (initOptions.checkLoginIframeInterval) { loginIframe.interval = initOptions.checkLoginIframeInterval; } if (initOptions.onLoad === 'login-required') { kc.loginRequired = true; } if (initOptions.responseMode) { if (initOptions.responseMode === 'query' || initOptions.responseMode === 'fragment') { kc.responseMode = initOptions.responseMode; } else { throw 'Invalid value for responseMode'; } } if (initOptions.flow) { switch (initOptions.flow) { case 'standard': kc.responseType = 'code'; break; case 'implicit': kc.responseType = 'id_token token'; break; case 'hybrid': kc.responseType = 'code id_token token'; break; default: throw 'Invalid value for flow'; } kc.flow = initOptions.flow; } if (initOptions.timeSkew != null) { kc.timeSkew = initOptions.timeSkew; } if(initOptions.redirectUri) { kc.redirectUri = initOptions.redirectUri; } if (initOptions.silentCheckSsoRedirectUri) { kc.silentCheckSsoRedirectUri = initOptions.silentCheckSsoRedirectUri; } if (typeof initOptions.silentCheckSsoFallback === 'boolean') { kc.silentCheckSsoFallback = initOptions.silentCheckSsoFallback; } else { kc.silentCheckSsoFallback = true; } if (typeof initOptions.pkceMethod !== "undefined") { if (initOptions.pkceMethod !== "S256" && initOptions.pkceMethod !== false) { throw new TypeError(`Invalid value for pkceMethod', expected 'S256' or false but got ${initOptions.pkceMethod}.`); } kc.pkceMethod = initOptions.pkceMethod; } else { kc.pkceMethod = "S256"; } if (typeof initOptions.enableLogging === 'boolean') { kc.enableLogging = initOptions.enableLogging; } else { kc.enableLogging = false; } if (initOptions.logoutMethod === 'POST') { kc.logoutMethod = 'POST'; } else { kc.logoutMethod = 'GET'; } if (typeof initOptions.scope === 'string') { kc.scope = initOptions.scope; } if (typeof initOptions.acrValues === 'string') { kc.acrValues = initOptions.acrValues; } if (typeof initOptions.messageReceiveTimeout === 'number' && initOptions.messageReceiveTimeout > 0) { kc.messageReceiveTimeout = initOptions.messageReceiveTimeout; } else { kc.messageReceiveTimeout = 10000; } if (!kc.responseMode) { kc.responseMode = 'fragment'; } if (!kc.responseType) { kc.responseType = 'code'; kc.flow = 'standard'; } var promise = createPromise(); var initPromise = createPromise(); initPromise.promise.then(function() { kc.onReady && kc.onReady(kc.authenticated); promise.setSuccess(kc.authenticated); }).catch(function(error) { promise.setError(error); }); var configPromise = loadConfig(); function onLoad() { var doLogin = function(prompt) { if (!prompt) { options.prompt = 'none'; } if (initOptions.locale) { options.locale = initOptions.locale; } kc.login(options).then(function () { initPromise.setSuccess(); }).catch(function (error) { initPromise.setError(error); }); } var checkSsoSilently = async function() { var ifrm = document.createElement("iframe"); var src = await kc.createLoginUrl({prompt: 'none', redirectUri: kc.silentCheckSsoRedirectUri}); ifrm.setAttribute("src", src); ifrm.setAttribute("sandbox", "allow-storage-access-by-user-activation allow-scripts allow-same-origin"); ifrm.setAttribute("title", "keycloak-silent-check-sso"); ifrm.style.display = "none"; document.body.appendChild(ifrm); var messageCallback = function(event) { if (event.origin !== window.location.origin || ifrm.contentWindow !== event.source) { return; } var oauth = parseCallback(event.data); processCallback(oauth, initPromise); document.body.removeChild(ifrm); window.removeEventListener("message", messageCallback); }; window.addEventListener("message", messageCallback); }; var options = {}; switch (initOptions.onLoad) { case 'check-sso': if (loginIframe.enable) { setupCheckLoginIframe().then(function() { checkLoginIframe().then(function (unchanged) { if (!unchanged) { kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false); } else { initPromise.setSuccess(); } }).catch(function (error) { initPromise.setError(error); }); }); } else { kc.silentCheckSsoRedirectUri ? checkSsoSilently() : doLogin(false); } break; case 'login-required': doLogin(true); break; default: throw 'Invalid value for onLoad'; } } function processInit() { var callback = parseCallback(window.location.href); if (callback) { window.history.replaceState(window.history.state, null, callback.newUrl); } if (callback && callback.valid) { return setupCheckLoginIframe().then(function() { processCallback(callback, initPromise); }).catch(function (error) { initPromise.setError(error); }); } if (initOptions.token && initOptions.refreshToken) { setToken(initOptions.token, initOptions.refreshToken, initOptions.idToken); if (loginIframe.enable) { setupCheckLoginIframe().then(function() { checkLoginIframe().then(function (unchanged) { if (unchanged) { kc.onAuthSuccess && kc.onAuthSuccess(); initPromise.setSuccess(); scheduleCheckIframe(); } else { initPromise.setSuccess(); } }).catch(function (error) { initPromise.setError(error); }); }); } else { kc.updateToken(-1).then(function() { kc.onAuthSuccess && kc.onAuthSuccess(); initPromise.setSuccess(); }).catch(function(error) { kc.onAuthError && kc.onAuthError(); if (initOptions.onLoad) { onLoad(); } else { initPromise.setError(error); } }); } } else if (initOptions.onLoad) { onLoad(); } else { initPromise.setSuccess(); } } configPromise.then(function () { check3pCookiesSupported() .then(processInit) .catch(function (error) { promise.setError(error); }); }); configPromise.catch(function (error) { promise.setError(error); }); return promise.promise; } kc.login = function (options) { return adapter.login(options); } function generateRandomData(len) { if (typeof crypto === "undefined" || typeof crypto.getRandomValues === "undefined") { throw new Error("Web Crypto API is not available."); } return crypto.getRandomValues(new Uint8Array(len)); } function generateCodeVerifier(len) { return generateRandomString(len, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'); } function generateRandomString(len, alphabet){ var randomData = generateRandomData(len); var chars = new Array(len); for (var i = 0; i < len; i++) { chars[i] = alphabet.charCodeAt(randomData[i] % alphabet.length); } return String.fromCharCode.apply(null, chars); } async function generatePkceChallenge(pkceMethod, codeVerifier) { if (pkceMethod !== "S256") { throw new TypeError(`Invalid value for 'pkceMethod', expected 'S256' but got '${pkceMethod}'.`); } // hash codeVerifier, then encode as url-safe base64 without padding const hashBytes = new Uint8Array(await sha256Digest(codeVerifier)); const encodedHash = bytesToBase64(hashBytes) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/\=/g, ''); return encodedHash; } function buildClaimsParameter(requestedAcr){ var claims = { id_token: { acr: requestedAcr } } return JSON.stringify(claims); } kc.createLoginUrl = async function(options) { var state = createUUID(); var nonce = createUUID(); var redirectUri = adapter.redirectUri(options); var callbackState = { state: state, nonce: nonce, redirectUri: encodeURIComponent(redirectUri), loginOptions: options }; if (options && options.prompt) { callbackState.prompt = options.prompt; } var baseUrl; if (options && options.action == 'register') { baseUrl = kc.endpoints.register(); } else { baseUrl = kc.endpoints.authorize(); } var scope = options && options.scope || kc.scope; if (!scope) { // if scope is not set, default to "openid" scope = "openid"; } else if (scope.indexOf("openid") === -1) { // if openid scope is missing, prefix the given scopes with it scope = "openid " + scope; } var url = baseUrl + '?client_id=' + encodeURIComponent(kc.clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri) + '&state=' + encodeURIComponent(state) + '&response_mode=' + encodeURIComponent(kc.responseMode) + '&response_type=' + encodeURIComponent(kc.responseType) + '&scope=' + encodeURIComponent(scope); if (useNonce) { url = url + '&nonce=' + encodeURIComponent(nonce); } if (options && options.prompt) { url += '&prompt=' + encodeURIComponent(options.prompt); } if (options && typeof options.maxAge === 'number') { url += '&max_age=' + encodeURIComponent(options.maxAge); } if (options && options.loginHint) { url += '&login_hint=' + encodeURIComponent(options.loginHint); } if (options && options.idpHint) { url += '&kc_idp_hint=' + encodeURIComponent(options.idpHint); } if (options && options.action && options.action != 'register') { url += '&kc_action=' + encodeURIComponent(options.action); } if (options && options.locale) { url += '&ui_locales=' + encodeURIComponent(options.locale); } if (options && options.acr) { var claimsParameter = buildClaimsParameter(options.acr); url += '&claims=' + encodeURIComponent(claimsParameter); } if ((options && options.acrValues) || kc.acrValues) { url += '&acr_values=' + encodeURIComponent(options.acrValues || kc.acrValues); } if (kc.pkceMethod) { try { const codeVerifier = generateCodeVerifier(96); const pkceChallenge = await generatePkceChallenge(kc.pkceMethod, codeVerifier); callbackState.pkceCodeVerifier = codeVerifier; url += '&code_challenge=' + pkceChallenge; url += '&code_challenge_method=' + kc.pkceMethod; } catch (error) { throw new Error("Failed to generate PKCE challenge.", { cause: error }); } } callbackStorage.add(callbackState); return url; } kc.logout = function(options) { return adapter.logout(options); } kc.createLogoutUrl = function(options) { const logoutMethod = options?.logoutMethod ?? kc.logoutMethod; if (logoutMethod === 'POST') { return kc.endpoints.logout(); } var url = kc.endpoints.logout() + '?client_id=' + encodeURIComponent(kc.clientId) + '&post_logout_redirect_uri=' + encodeURIComponent(adapter.redirectUri(options, false)); if (kc.idToken) { url += '&id_token_hint=' + encodeURIComponent(kc.idToken); } return url; } kc.register = function (options) { return adapter.register(options); } kc.createRegisterUrl = async function(options) { if (!options) { options = {}; } options.action = 'register'; return await kc.createLoginUrl(options); } kc.createAccountUrl = function(options) { var realm = getRealmUrl(); var url = undefined; if (typeof realm !== 'undefined') { url = realm + '/account' + '?referrer=' + encodeURIComponent(kc.clientId) + '&referrer_uri=' + encodeURIComponent(adapter.redirectUri(options)); } return url; } kc.accountManagement = function() { return adapter.accountManagement(); } kc.hasRealmRole = function (role) { var access = kc.realmAccess; return !!access && access.roles.indexOf(role) >= 0; } kc.hasResourceRole = function(role, resource) { if (!kc.resourceAccess) { return false; } var access = kc.resourceAccess[resource || kc.clientId]; return !!access && access.roles.indexOf(role) >= 0; } kc.loadUserProfile = function() { var url = getRealmUrl() + '/account'; var req = new XMLHttpRequest(); req.open('GET', url, true); req.setRequestHeader('Accept', 'application/json'); req.setRequestHeader('Authorization', 'bearer ' + kc.token); var promise = createPromise(); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200) { kc.profile = JSON.parse(req.responseText); promise.setSuccess(kc.profile); } else { promise.setError(); } } } req.send(); return promise.promise; } kc.loadUserInfo = function() { var url = kc.endpoints.userinfo(); var req = new XMLHttpRequest(); req.open('GET', url, true); req.setRequestHeader('Accept', 'application/json'); req.setRequestHeader('Authorization', 'bearer ' + kc.token); var promise = createPromise(); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200) { kc.userInfo = JSON.parse(req.responseText); promise.setSuccess(kc.userInfo); } else { promise.setError(); } } } req.send(); return promise.promise; } kc.isTokenExpired = function(minValidity) { if (!kc.tokenParsed || (!kc.refreshToken && kc.flow != 'implicit' )) { throw 'Not authenticated'; } if (kc.timeSkew == null) { logInfo('[KEYCLOAK] Unable to determine if token is expired as timeskew is not set'); return true; } var expiresIn = kc.tokenParsed['exp'] - Math.ceil(new Date().getTime() / 1000) + kc.timeSkew; if (minValidity) { if (isNaN(minValidity)) { throw 'Invalid minValidity'; } expiresIn -= minValidity; } return expiresIn < 0; } kc.updateToken = function(minValidity) { var promise = createPromise(); if (!kc.refreshToken) { promise.setError(); return promise.promise; } minValidity = minValidity || 5; var exec = function() { var refreshToken = false; if (minValidity == -1) { refreshToken = true; logInfo('[KEYCLOAK] Refreshing token: forced refresh'); } else if (!kc.tokenParsed || kc.isTokenExpired(minValidity)) { refreshToken = true; logInfo('[KEYCLOAK] Refreshing token: token expired'); } if (!refreshToken) { promise.setSuccess(false); } else { var params = 'grant_type=refresh_token&' + 'refresh_token=' + kc.refreshToken; var url = kc.endpoints.token(); refreshQueue.push(promise); if (refreshQueue.length == 1) { var req = new XMLHttpRequest(); req.open('POST', url, true); req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); req.withCredentials = true; params += '&client_id=' + encodeURIComponent(kc.clientId); var timeLocal = new Date().getTime(); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200) { logInfo('[KEYCLOAK] Token refreshed'); timeLocal = (timeLocal + new Date().getTime()) / 2; var tokenResponse = JSON.parse(req.responseText); setToken(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], timeLocal); kc.onAuthRefreshSuccess && kc.onAuthRefreshSuccess(); for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { p.setSuccess(true); } } else { logWarn('[KEYCLOAK] Failed to refresh token'); if (req.status == 400) { kc.clearToken(); } kc.onAuthRefreshError && kc.onAuthRefreshError(); for (var p = refreshQueue.pop(); p != null; p = refreshQueue.pop()) { p.setError("Failed to refresh token: An unexpected HTTP error occurred while attempting to refresh the token."); } } } }; req.send(params); } } } if (loginIframe.enable) { var iframePromise = checkLoginIframe(); iframePromise.then(function() { exec(); }).catch(function(error) { promise.setError(error); }); } else { exec(); } return promise.promise; } kc.clearToken = function() { if (kc.token) { setToken(null, null, null); kc.onAuthLogout && kc.onAuthLogout(); if (kc.loginRequired) { kc.login(); } } } function getRealmUrl() { if (typeof kc.authServerUrl !== 'undefined') { if (kc.authServerUrl.charAt(kc.authServerUrl.length - 1) == '/') { return kc.authServerUrl + 'realms/' + encodeURIComponent(kc.realm); } else { return kc.authServerUrl + '/realms/' + encodeURIComponent(kc.realm); } } else { return undefined; } } function getOrigin() { if (!window.location.origin) { return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); } else { return window.location.origin; } } function processCallback(oauth, promise) { var code = oauth.code; var error = oauth.error; var prompt = oauth.prompt; var timeLocal = new Date().getTime(); if (oauth['kc_action_status']) { kc.onActionUpdate && kc.onActionUpdate(oauth['kc_action_status'], oauth['kc_action']); } if (error) { if (prompt != 'none') { if (oauth.error_description && oauth.error_description === "authentication_expired") { kc.login(oauth.loginOptions); } else { var errorData = { error: error, error_description: oauth.error_description }; kc.onAuthError && kc.onAuthError(errorData); promise && promise.setError(errorData); } } else { promise && promise.setSuccess(); } return; } else if ((kc.flow != 'standard') && (oauth.access_token || oauth.id_token)) { authSuccess(oauth.access_token, null, oauth.id_token, true); } if ((kc.flow != 'implicit') && code) { var params = 'code=' + code + '&grant_type=authorization_code'; var url = kc.endpoints.token(); var req = new XMLHttpRequest(); req.open('POST', url, true); req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); params += '&client_id=' + encodeURIComponent(kc.clientId); params += '&redirect_uri=' + oauth.redirectUri; if (oauth.pkceCodeVerifier) { params += '&code_verifier=' + oauth.pkceCodeVerifier; } req.withCredentials = true; req.onreadystatechange = function() { if (req.readyState == 4) { if (req.status == 200) { var tokenResponse = JSON.parse(req.responseText); authSuccess(tokenResponse['access_token'], tokenResponse['refresh_token'], tokenResponse['id_token'], kc.flow === 'standard'); scheduleCheckIframe(); } else { kc.onAuthError && kc.onAuthError(); promise && promise.setError(); } } }; req.send(params); } function authSuccess(accessToken, refreshToken, idToken, fulfillPromise) { timeLocal = (timeLocal + new Date().getTime()) / 2; setToken(accessToken, refreshToken, idToken, timeLocal); if (useNonce && (kc.idTokenParsed && kc.idTokenParsed.nonce != oauth.storedNonce)) { logInfo('[KEYCLOAK] Invalid nonce, clearing token'); kc.clearToken(); promise && promise.setError(); } else { if (fulfillPromise) { kc.onAuthSuccess && kc.onAuthSuccess(); promise && promise.setSuccess(); } } } } function loadConfig() { var promise = createPromise(); var configUrl; if (typeof config === 'string') { configUrl = config; } function setupOidcEndoints(oidcConfiguration) { if (! oidcConfiguration) { kc.endpoints = { authorize: function() { return getRealmUrl() + '/protocol/openid-connect/auth'; }, token: function() { return getRealmUrl() + '/protocol/openid-connect/token'; }, logout: function() { return getRealmUrl() + '/protocol/openid-connect/logout'; }, checkSessionIframe: function() { return getRealmUrl() + '/protocol/openid-connect/login-status-iframe.html'; }, thirdPartyCookiesIframe: function() { return getRealmUrl() + '/protocol/openid-connect/3p-cookies/step1.html'; }, register: function() { return getRealmUrl() + '/protocol/openid-connect/registrations'; }, userinfo: function() { return getRealmUrl() + '/protocol/openid-connect/userinfo'; } }; } else { kc.endpoints = { authorize: function() { return oidcConfiguration.authorization_endpoint; }, token: function() { return oidcConfiguration.token_endpoint; }, logout: function() { if (!oidcConfiguration.end_session_endpoint) { throw "Not supported by the OIDC server"; } return oidcConfiguration.end_session_endpoint; }, checkSessionIframe: function() { if (!oidcConfiguration.check_session_iframe) { throw "Not supported by the OIDC server"; } return oidcConfiguration.check_session_iframe; }, register: function() { throw 'Redirection to "Register user" page not supported in standard OIDC mode'; }, userinfo: function() { if (!oidcConfiguration.userinfo_endpoint) { throw "Not supported by the OIDC server"; } return oidcConfiguration.userinfo_endpoint; } } } } if (configUrl) { var req = new XMLHttpRequest(); req.open('GET', configUrl, true); req.setRequestHeader('Accept', 'application/json'); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200 || fileLoaded(req)) { var config = JSON.parse(req.responseText); kc.authServerUrl = config['auth-server-url']; kc.realm = config['realm']; kc.clientId = config['resource']; setupOidcEndoints(null); promise.setSuccess(); } else { promise.setError(); } } }; req.send(); } else { kc.clientId = config.clientId; var oidcProvider = config['oidcProvider']; if (!oidcProvider) { kc.authServerUrl = config.url; kc.realm = config.realm; setupOidcEndoints(null); promise.setSuccess(); } else { if (typeof oidcProvider === 'string') { var oidcProviderConfigUrl; if (oidcProvider.charAt(oidcProvider.length - 1) == '/') { oidcProviderConfigUrl = oidcProvider + '.well-known/openid-configuration'; } else { oidcProviderConfigUrl = oidcProvider + '/.well-known/openid-configuration'; } var req = new XMLHttpRequest(); req.open('GET', oidcProviderConfigUrl, true); req.setRequestHeader('Accept', 'application/json'); req.onreadystatechange = function () { if (req.readyState == 4) { if (req.status == 200 || fileLoaded(req)) { var oidcProviderConfig = JSON.parse(req.responseText); setupOidcEndoints(oidcProviderConfig); promise.setSuccess(); } else { promise.setError(); } } }; req.send(); } else { setupOidcEndoints(oidcProvider); promise.setSuccess(); } } } return promise.promise; } function fileLoaded(xhr) { return xhr.status == 0 && xhr.responseText && xhr.responseURL.startsWith('file:'); } function setToken(token, refreshToken, idToken, timeLocal) { if (kc.tokenTimeoutHandle) { clearTimeout(kc.tokenTimeoutHandle); kc.tokenTimeoutHandle = null; } if (refreshToken) { kc.refreshToken = refreshToken; kc.refreshTokenParsed = decodeToken(refreshToken); } else { delete kc.refreshToken; delete kc.refreshTokenParsed; } if (idToken) { kc.idToken = idToken; kc.idTokenParsed = decodeToken(idToken); } else { delete kc.idToken; delete kc.idTokenParsed; } if (token) { kc.token = token; kc.tokenParsed = decodeToken(token); kc.sessionId = kc.tokenParsed.sid; kc.authenticated = true; kc.subject = kc.tokenParsed.sub; kc.realmAccess = kc.tokenParsed.realm_access; kc.resourceAccess = kc.tokenParsed.resource_access; if (timeLocal) { kc.timeSkew = Math.floor(timeLocal / 1000) - kc.tokenParsed.iat; } if (kc.timeSkew != null) { logInfo('[KEYCLOAK] Estimated time difference between browser and server is ' + kc.timeSkew + ' seconds'); if (kc.onTokenExpired) { var expiresIn = (kc.tokenParsed['exp'] - (new Date().getTime() / 1000) + kc.timeSkew) * 1000; logInfo('[KEYCLOAK] Token expires in ' + Math.round(expiresIn / 1000) + ' s'); if (expiresIn <= 0) { kc.onTokenExpired(); } else { kc.tokenTimeoutHandle = setTimeout(kc.onTokenExpired, expiresIn); } } } } else { delete kc.token; delete kc.tokenParsed; delete kc.subject; delete kc.realmAccess; delete kc.resourceAccess; kc.authenticated = false; } } function createUUID() { if (typeof crypto === "undefined" || typeof crypto.randomUUID === "undefined") { throw new Error("Web Crypto API is not available."); } return crypto.randomUUID(); } function parseCallback(url) { var oauth = parseCallbackUrl(url); if (!oauth) { return; } var oauthState = callbackStorage.get(oauth.state); if (oauthState) { oauth.valid = true; oauth.redirectUri = oauthState.redirectUri; oauth.storedNonce = oauthState.nonce; oauth.prompt = oauthState.prompt; oauth.pkceCodeVerifier = oauthState.pkceCodeVerifier; oauth.loginOptions = oauthState.loginOptions; } return oauth; } function parseCallbackUrl(url) { var supportedParams; switch (kc.flow) { case 'standard': supportedParams = ['code', 'state', 'session_state', 'kc_action_status', 'kc_action', 'iss']; break; case 'implicit': supportedParams = ['access_token', 'token_type', 'id_token', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss']; break; case 'hybrid': supportedParams = ['access_token', 'token_type', 'id_token', 'code', 'state', 'session_state', 'expires_in', 'kc_action_status', 'kc_action', 'iss']; break; } supportedParams.push('error'); supportedParams.push('error_description'); supportedParams.push('error_uri'); var queryIndex = url.indexOf('?'); var fragmentIndex = url.indexOf('#'); var newUrl; var parsed; if (kc.responseMode === 'query' && queryIndex !== -1) { newUrl = url.substring(0, queryIndex); parsed = parseCallbackParams(url.substring(queryIndex + 1, fragmentIndex !== -1 ? fragmentIndex : url.length), supportedParams); if (parsed.paramsString !== '') { newUrl += '?' + parsed.paramsString; } if (fragmentIndex !== -1) { newUrl += url.substring(fragmentIndex); } } else if (kc.responseMode === 'fragment' && fragmentIndex !== -1) { newUrl = url.substring(0, fragmentIndex); parsed = parseCallbackParams(url.substring(fragmentIndex + 1), supportedParams); if (parsed.paramsString !== '') { newUrl += '#' + parsed.paramsString; } } if (parsed && parsed.oauthParams) { if (kc.flow === 'standard' || kc.flow === 'hybrid') { if ((parsed.oauthParams.code || parsed.oauthParams.error) && parsed.oauthParams.state) { parsed.oauthParams.newUrl = newUrl; return parsed.oauthParams; } } else if (kc.flow === 'implicit') { if ((parsed.oauthParams.access_token || parsed.oauthParams.error) && parsed.oauthParams.state) { parsed.oauthParams.newUrl = newUrl; return parsed.oauthParams; } } } } function parseCallbackParams(paramsString, supportedParams) { var p = paramsString.split('&'); var result = { paramsString: '', oauthParams: {} } for (var i = 0; i < p.length; i++) { var split = p[i].indexOf("="); var key = p[i].slice(0, split); if (supportedParams.indexOf(key) !== -1) { result.oauthParams[key] = p[i].slice(split + 1); } else { if (result.paramsString !== '') { result.paramsString += '&'; } result.paramsString += p[i]; } } return result; } function createPromise() { // Need to create a native Promise which also preserves the // interface of the custom promise type previously used by the API var p = { setSuccess: function(result) { p.resolve(result); }, setError: function(result) { p.reject(result); } }; p.promise = new Promise(function(resolve, reject) { p.resolve = resolve; p.reject = reject; }); return p; } // Function to extend existing native Promise with timeout function applyTimeoutToPromise(promise, timeout, errorMessage) { var timeoutHandle = null; var timeoutPromise = new Promise(function (resolve, reject) { timeoutHandle = setTimeout(function () { reject({ "error": errorMessage || "Promise is not settled within timeout of " + timeout + "ms" }); }, timeout); }); return Promise.race([promise, timeoutPromise]).finally(function () { clearTimeout(timeoutHandle); }); } function setupCheckLoginIframe() { var promise = createPromise(); if (!loginIframe.enable) { promise.setSuccess(); return promise.promise; } if (loginIframe.iframe) { promise.setSuccess(); return promise.promise; } var iframe = document.createElement('iframe'); loginIframe.iframe = iframe; iframe.onload = function() { var authUrl = kc.endpoints.authorize(); if (authUrl.charAt(0) === '/') { loginIframe.iframeOrigin = getOrigin(); } else { loginIframe.iframeOrigin = authUrl.substring(0, authUrl.indexOf('/', 8)); } promise.setSuccess(); } var src = kc.endpoints.checkSessionIframe(); iframe.setAttribute('src', src ); iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin'); iframe.setAttribute('title', 'keycloak-session-iframe' ); iframe.style.display = 'none'; document.body.appendChild(iframe); var messageCallback = function(event) { if ((event.origin !== loginIframe.iframeOrigin) || (loginIframe.iframe.contentWindow !== event.source)) { return; } if (!(event.data == 'unchanged' || event.data == 'changed' || event.data == 'error')) { return; } if (event.data != 'unchanged') { kc.clearToken(); } var callbacks = loginIframe.callbackList.splice(0, loginIframe.callbackList.length); for (var i = callbacks.length - 1; i >= 0; --i) { var promise = callbacks[i]; if (event.data == 'error') { promise.setError(); } else { promise.setSuccess(event.data == 'unchanged'); } } }; window.addEventListener('message', messageCallback, false); return promise.promise; } function scheduleCheckIframe() { if (loginIframe.enable) { if (kc.token) { setTimeout(function() { checkLoginIframe().then(function(unchanged) { if (unchanged) { scheduleCheckIframe(); } }); }, loginIframe.interval * 1000); } } } function checkLoginIframe() { var promise = createPromise(); if (loginIframe.iframe && loginIframe.iframeOrigin ) { var msg = kc.clientId + ' ' + (kc.sessionId ? kc.sessionId : ''); loginIframe.callbackList.push(promise); var origin = loginIframe.iframeOrigin; if (loginIframe.callbackList.length == 1) { loginIframe.iframe.contentWindow.postMessage(msg, origin); } } else { promise.setSuccess(); } return promise.promise; } function check3pCookiesSupported() { var promise = createPromise(); if ((loginIframe.enable || kc.silentCheckSsoRedirectUri) && typeof kc.endpoints.thirdPartyCookiesIframe === 'function') { var iframe = document.createElement('iframe'); iframe.setAttribute('src', kc.endpoints.thirdPartyCookiesIframe()); iframe.setAttribute('sandbox', 'allow-storage-access-by-user-activation allow-scripts allow-same-origin'); iframe.setAttribute('title', 'keycloak-3p-check-iframe' ); iframe.style.display = 'none'; document.body.appendChild(iframe); var messageCallback = function(event) { if (iframe.contentWindow !== event.source) { return; } if (event.data !== "supported" && event.data !== "unsupported") { return; } else if (event.data === "unsupported") { logWarn( "[KEYCLOAK] Your browser is blocking access to 3rd-party cookies, this means:\n\n" + " - It is not possible to retrieve tokens without redirecting to the Keycloak server (a.k.a. no support for silent authentication).\n" + " - It is not possible to automatically detect changes to the session status (such as the user logging out in another tab).\n\n" + "For more information see: https://www.keycloak.org/securing-apps/javascript-adapter#_modern_browsers" ); loginIframe.enable = false; if (kc.silentCheckSsoFallback) { kc.silentCheckSsoRedirectUri = false; } } document.body.removeChild(iframe); window.removeEventListener("message", messageCallback); promise.setSuccess(); }; window.addEventListener('message', messageCallback, false); } else { promise.setSuccess(); } return applyTimeoutToPromise(promise.promise, kc.messageReceiveTimeout, "Timeout when waiting for 3rd party check iframe message."); } function loadAdapter(type) { if (!type || type == 'default') { return { login: async function(options) { window.location.assign(await kc.createLoginUrl(options)); return createPromise().promise; }, logout: async function(options) { const logoutMethod = options?.logoutMethod ?? kc.logoutMethod; if (logoutMethod === "GET") { window.location.replace(kc.createLogoutUrl(options)); return; } // Create form to send POST request. const form = document.createElement("form"); form.setAttribute("method", "POST"); form.setAttribute("action", kc.createLogoutUrl(options)); form.style.display = "none"; // Add data to form as hidden input fields. const data = { id_token_hint: kc.idToken, client_id: kc.clientId, post_logout_redirect_uri: adapter.redirectUri(options, false) }; for (const [name, value] of Object.entries(data)) { const input = document.createElement("input"); input.setAttribute("type", "hidden"); input.setAttribute("name", name); input.setAttribute("value", value); form.appendChild(input); } // Append form to page and submit it to perform logout and redirect. document.body.appendChild(form); form.submit(); }, register: async function(options) { window.location.assign(await kc.createRegisterUrl(options)); return createPromise().promise; }, accountManagement : function() { var accountUrl = kc.createAccountUrl(); if (typeof accountUrl !== 'undefined') { window.location.href = accountUrl; } else { throw "Not supported by the OIDC server"; } return createPromise().promise; }, redirectUri: function(options, encodeHash) { if (arguments.length == 1) { encodeHash = true; } if (options && options.redirectUri) { return options.redirectUri; } else if (kc.redirectUri) { return kc.redirectUri; } else { return location.href; } } }; } if (type == 'cordova') { loginIframe.enable = false; var cordovaOpenWindowWrapper = function(loginUrl, target, options) { if (window.cordova && window.cordova.InAppBrowser) { // Use inappbrowser for IOS and Android if available return window.cordova.InAppBrowser.open(loginUrl, target, options); } else { return window.open(loginUrl, target, options); } }; var shallowCloneCordovaOptions = function (userOptions) { if (userOptions && userOptions.cordovaOptions) { return Object.keys(userOptions.cordovaOptions).reduce(function (options, optionName) { options[optionName] = userOptions.cordovaOptions[optionName]; return options; }, {}); } else { return {}; } }; var formatCordovaOptions = function (cordovaOptions) { return Object.keys(cordovaOptions).reduce(function (options, optionName) { options.push(optionName+"="+cordovaOptions[optionName]); return options; }, []).join(","); }; var createCordovaOptions = function (userOptions) { var cordovaOptions = shallowCloneCordovaOptions(userOptions); cordovaOptions.location = 'no'; if (userOptions && userOptions.prompt == 'none') { cordovaOptions.hidden = 'yes'; } return formatCordovaOptions(cordovaOptions); }; var getCordovaRedirectUri = function() { return kc.redirectUri || 'http://localhost'; } return { login: async function(options) { var promise = createPromise(); var cordovaOptions = createCordovaOptions(options); var loginUrl = await kc.createLoginUrl(options); var ref = cordovaOpenWindowWrapper(loginUrl, '_blank', cordovaOptions); var completed = false; var closed = false; var closeBrowser = function() { closed = true; ref.close(); }; ref.addEventListener('loadstart', function(event) { if (event.url.indexOf(getCordovaRedirectUri()) == 0) { var callback = parseCallback(event.url); processCallback(callback, promise); closeBrowser(); completed = true; } }); ref.addEventListener('loaderror', function(event) { if (!completed) { if (event.url.indexOf(getCordovaRedirectUri()) == 0) { var callback = parseCallback(event.url); processCallback(callback, promise); closeBrowser(); completed = true; } else { promise.setError(); closeBrowser(); } } }); ref.addEventListener('exit', function(event) { if (!closed) { promise.setError({ reason: "closed_by_user" }); } }); return promise.promise; }, logout: function(options) { var promise = createPromise(); var logoutUrl = kc.createLogoutUrl(options); var ref = cordovaOpenWindowWrapper(logoutUrl, '_blank', 'location=no,hidden=yes,clearcache=yes'); var error; ref.addEventListener('loadstart', function(event) { if (event.url.indexOf(getCordovaRedirectUri()) == 0) { ref.close(); } }); ref.addEventListener('loaderror', function(event) { if (event.url.indexOf(getCordovaRedirectUri()) == 0) { ref.close(); } else { error = true; ref.close(); } }); ref.addEventListener('exit', function(event) { if (error) { promise.setError(); } else { kc.clearToken(); promise.setSuccess(); } }); return promise.promise; }, register : async function(options) { var promise = createPromise(); var registerUrl = await kc.createRegisterUrl(); var cordovaOptions = createCordovaOptions(options); var ref = cordovaOpenWindowWrapper(registerUrl, '_blank', cordovaOptions); ref.addEventListener('loadstart', function(event) { if (event.url.indexOf(getCordovaRedirectUri()) == 0) { ref.close(); var oauth = parseCallback(event.url); processCallback(oauth, promise); } }); return promise.promise; }, accountManagement : function() { var accountUrl = kc.createAccountUrl(); if (typeof accountUrl !== 'undefined') { var ref = cordovaOpenWindowWrapper(accountUrl, '_blank', 'location=no'); ref.addEventListener('loadstart', function(event) { if (event.url.indexOf(getCordovaRedirectUri()) == 0) { ref.close(); } }); } else { throw "Not supported by the OIDC server"; } }, redirectUri: function(options) { return getCordovaRedirectUri(); } } } if (type == 'cordova-native') { loginIframe.enable = false; return { login: async function(options) { var promise = createPromise(); var loginUrl = await kc.createLoginUrl(options); universalLinks.subscribe('keycloak', function(event) { universalLinks.unsubscribe('keycloak'); window.cordova.plugins.browsertab.close(); var oauth = parseCallback(event.url); processCallback(oauth, promise); }); window.cordova.plugins.browsertab.openUrl(loginUrl); return promise.promise; }, logout: function(options) { var promise = createPromise(); var logoutUrl = kc.createLogoutUrl(options); universalLinks.subscribe('keycloak', function(event) { universalLinks.unsubscribe('keycloak'); window.cordova.plugins.browsertab.close(); kc.clearToken(); promise.setSuccess(); }); window.cordova.plugins.browsertab.openUrl(logoutUrl); return promise.promise; }, register : async function(options) { var promise = createPromise(); var registerUrl = await kc.createRegisterUrl(options); universalLinks.subscribe('keycloak' , function(event) { universalLinks.unsubscribe('keycloak'); window.cordova.plugins.browsertab.close(); var oauth = parseCallback(event.url); processCallback(oauth, promise); }); window.cordova.plugins.browsertab.openUrl(registerUrl); return promise.promise; }, accountManagement : function() { var accountUrl = kc.createAccountUrl(); if (typeof accountUrl !== 'undefined') { window.cordova.plugins.browsertab.openUrl(accountUrl); } else { throw "Not supported by the OIDC server"; } }, redirectUri: function(options) { if (options && options.redirectUri) { return options.redirectUri; } else if (kc.redirectUri) { return kc.redirectUri; } else { return "http://localhost"; } } } } throw 'invalid adapter type: ' + type; } const STORAGE_KEY_PREFIX = 'kc-callback-'; var LocalStorage = function() { if (!(this instanceof LocalStorage)) { return new LocalStorage(); } localStorage.setItem('kc-test', 'test'); localStorage.removeItem('kc-test'); var cs = this; /** * Clears all values from local storage that are no longer valid. */ function clearInvalidValues() { const currentTime = Date.now(); for (const [key, value] of getStoredEntries()) { // Attempt to parse the expiry time from the value. const expiry = parseExpiry(value); // Discard the value if it is malformed or expired. if (expiry === null || expiry < currentTime) { localStorage.removeItem(key); } } } /** * Clears all known values from local storage. */ function clearAllValues() { for (const [key] of getStoredEntries()) { localStorage.removeItem(key); } } /** * Gets all entries stored in local storage that are known to be managed by this class. * @returns {Array<[string, unknown]>} An array of key-value pairs. */ function getStoredEntries() { return Object.entries(localStorage).filter(([key]) => key.startsWith(STORAGE_KEY_PREFIX)); } /** * Parses the expiry time from a value stored in local storage. * @param {unknown} value * @returns {number | null} The expiry time in milliseconds, or `null` if the value is malformed. */ function parseExpiry(value) { let parsedValue; // Attempt to parse the value as JSON. try { parsedValue = JSON.parse(value); } catch (error) { return null; } // Attempt to extract the 'expires' property. if (isObject(parsedValue) && 'expires' in parsedValue && typeof parsedValue.expires === 'number') { return parsedValue.expires; } return null; } cs.get = function(state) { if (!state) { return; } var key = STORAGE_KEY_PREFIX + state; var value = localStorage.getItem(key); if (value) { localStorage.removeItem(key); value = JSON.parse(value); } clearInvalidValues(); return value; }; cs.add = function(state) { clearInvalidValues(); const key = STORAGE_KEY_PREFIX + state.state; const value = JSON.stringify({ ...state, // Set the expiry time to 1 hour from now. expires: Date.now() + (60 * 60 * 1000) }); try { localStorage.setItem(key, value); } catch (error) { // If the storage is full, clear all known values and try again. clearAllValues(); localStorage.setItem(key, value); } }; }; var CookieStorage = function() { if (!(this instanceof CookieStorage)) { return new CookieStorage(); } var cs = this; cs.get = function(state) { if (!state) { return; } var value = getCookie(STORAGE_KEY_PREFIX + state); setCookie(STORAGE_KEY_PREFIX + state, '', cookieExpiration(-100)); if (value) { return JSON.parse(value); } }; cs.add = function(state) { setCookie(STORAGE_KEY_PREFIX + state.state, JSON.stringify(state), cookieExpiration(60)); }; cs.removeItem = function(key) { setCookie(key, '', cookieExpiration(-100)); }; var cookieExpiration = function (minutes) { var exp = new Date(); exp.setTime(exp.getTime() + (minutes*60*1000)); return exp; }; var getCookie = function (key) { var name = key + '='; var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ''; }; var setCookie = function (key, value, expirationDate) { var cookie = key + '=' + value + '; ' + 'expires=' + expirationDate.toUTCString() + '; '; document.cookie = cookie; } }; function createCallbackStorage() { try { return new LocalStorage(); } catch (err) { } return new CookieStorage(); } function createLogger(fn) { return function() { if (kc.enableLogging) { fn.apply(console, Array.prototype.slice.call(arguments)); } }; } } export default Keycloak; /** * @param {ArrayBuffer} bytes * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ function bytesToBase64(bytes) { const binString = String.fromCodePoint(...bytes); return btoa(binString); } /** * @param {string} message * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#basic_example */ async function sha256Digest(message) { const encoder = new TextEncoder(); const data = encoder.encode(message); if (typeof crypto === "undefined" || typeof crypto.subtle === "undefined") { throw new Error("Web Crypto API is not available."); } return await crypto.subtle.digest("SHA-256", data); } /** * @param {string} token */ function decodeToken(token) { const [header, payload] = token.split("."); if (typeof payload !== "string") { throw new Error("Unable to decode token, payload not found."); } let decoded; try { decoded = base64UrlDecode(payload); } catch (error) { throw new Error("Unable to decode token, payload is not a valid Base64URL value.", { cause: error }); } try { return JSON.parse(decoded); } catch (error) { throw new Error("Unable to decode token, payload is not a valid JSON value.", { cause: error }); } } /** * @param {string} input */ function base64UrlDecode(input) { let output = input .replaceAll("-", "+") .replaceAll("_", "/"); switch (output.length % 4) { case 0: break; case 2: output += "=="; break; case 3: output += "="; break; default: throw new Error("Input is not of the correct length."); } try { return b64DecodeUnicode(output); } catch (error) { return atob(output); } } /** * @param {string} input */ function b64DecodeUnicode(input) { return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => { let code = p.charCodeAt(0).toString(16).toUpperCase(); if (code.length < 2) { code = "0" + code; } return "%" + code; })); } /** * Check if the input is an object that can be operated on. * @param {unknown} input */ function isObject(input) { return typeof input === 'object' && input !== null; } ================================================ FILE: apps/site/privacy.html ================================================

Acme Privacy

================================================ FILE: apps/site/site.html ================================================

Acme Site

================================================ FILE: apps/site/terms.html ================================================

Acme Terms

================================================ FILE: apps/spring-boot-device-flow-client/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: apps/spring-boot-device-flow-client/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.4/apache-maven-3.8.4-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar ================================================ FILE: apps/spring-boot-device-flow-client/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /usr/local/etc/mavenrc ] ; then . /usr/local/etc/mavenrc fi if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`\\unset -f command; \\command -v java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" else jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: apps/spring-boot-device-flow-client/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %WRAPPER_JAR% ^ "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%"=="on" pause if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% cmd /C exit /B %ERROR_CODE% ================================================ FILE: apps/spring-boot-device-flow-client/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.4.7 com.github.thomasdarimont.keycloak spring-boot-device-flow-client 0.0.1-SNAPSHOT spring-boot-device-flow spring-boot-device-flow 11 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok ================================================ FILE: apps/spring-boot-device-flow-client/src/main/java/demo/SpringBootDeviceFlowApplication.java ================================================ package demo; import com.fasterxml.jackson.annotation.JsonAnySetter; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import java.time.Instant; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @Slf4j @SpringBootApplication public class SpringBootDeviceFlowApplication { public static void main(String[] args) { new SpringApplicationBuilder(SpringBootDeviceFlowApplication.class).web(WebApplicationType.NONE).run(args); } @Bean CommandLineRunner clr() { return args -> { log.info("Running"); var clientId = "acme-device-client"; var scope = "email"; var authServerUrl = "https://id.acme.test:8443/auth"; var realm = "acme-demo"; var issuerUrl = authServerUrl + "/realms/" + realm; var deviceAuthUrl = issuerUrl + "/protocol/openid-connect/auth/device"; var tokenUrl = issuerUrl + "/protocol/openid-connect/token"; log.info("Browse to {} and enter the following code.", deviceAuthUrl); var deviceCodeResponseEntity = requestDeviceCode(clientId, scope, deviceAuthUrl); log.info("Response code: {}", deviceCodeResponseEntity.getStatusCodeValue()); var deviceCodeResponse = deviceCodeResponseEntity.getBody(); log.info("{}", deviceCodeResponse); log.info("Browse to {} and enter the code {}", deviceCodeResponse.getVerification_uri(), deviceCodeResponse.getUser_code()); log.info("--- OR ----"); log.info("Browse to {}", deviceCodeResponse.getVerification_uri_complete()); System.out.println("Waiting for completion..."); var expiresAt = Instant.now().plusSeconds(deviceCodeResponse.expires_in); while (Instant.now().isBefore(expiresAt)) { log.info("Start device flow"); try { var deviceFlowResponse = checkForDeviceFlowCompletion(clientId, deviceCodeResponse.getDevice_code(), tokenUrl); log.info("Got response status: {}", deviceFlowResponse.getStatusCodeValue()); if (deviceFlowResponse.getStatusCodeValue() == 200) { log.info("Success!"); log.info("{}", deviceFlowResponse.getBody()); } else { log.info("Problem!"); log.info("{}", deviceFlowResponse.getBody()); } break; } catch (HttpClientErrorException.BadRequest badRequest) { log.info("Failed ..."); log.info("Continue with polling - sleeping..."); TimeUnit.SECONDS.sleep(deviceCodeResponse.getInterval()); } } }; } private ResponseEntity requestDeviceCode(String clientId, String scope, String deviceAuthUrl) { var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", clientId); requestBody.add("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); requestBody.add("scope", scope); var rt = new RestTemplate(); return rt.postForEntity(deviceAuthUrl, new HttpEntity<>(requestBody, headers), DeviceCodeResponse.class); } private ResponseEntity checkForDeviceFlowCompletion(String clientId, String deviceCode, String tokenUrl) { var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", clientId); requestBody.add("device_code", deviceCode); requestBody.add("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); var rt = new RestTemplate(); return rt.postForEntity(tokenUrl, new HttpEntity<>(requestBody, headers), AccessTokenResponse.class); } @Data static class DeviceCodeResponse { String device_code; String user_code; String verification_uri; String verification_uri_complete; int expires_in; int interval; Map other = new HashMap<>(); @JsonAnySetter public void setValue(String key, Object value) { other.put(key, value); } } @Data static class AccessTokenResponse { String access_token; String refresh_token; Map other = new HashMap<>(); @JsonAnySetter public void setValue(String key, Object value) { other.put(key, value); } } } ================================================ FILE: apps/spring-boot-device-flow-client/src/main/resources/application.properties ================================================ ================================================ FILE: bin/applyRealmConfig.java ================================================ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; class applyKeycloakConfigCli { public static void main(String[] args) throws IOException, InterruptedException { var commandLine = new ArrayList(); commandLine.add("docker"); commandLine.add("compose"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-keycloakx.yml"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-provisioning.yml"); commandLine.add("restart"); commandLine.add("acme-keycloak-provisioning"); var pb = new ProcessBuilder(commandLine); pb.inheritIO(); var process = pb.start(); System.exit(process.waitFor()); } } ================================================ FILE: bin/createTlsCerts.java ================================================ import java.io.File; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; /** * Controller script to generate valid tls certs locally. * *

Generate certificates/h2> *
{@code
 *  java createTlsCerts.java
 * }
* *

Show help

*
{@code
 *  java createTlsCerts.java --help
 * }
*

* Hint: */ class createTlsCerts { static final String HELP_CMD = "--help"; static final String DOMAIN_OPT = "--domain"; static final String DOMAIN_ENV = "DOMAIN"; static final String DOMAIN_DEFAULT = "acme.test"; static final String TARGET_DIR_OPT = "--target"; static final String TARGET_DIR_ENV = "TARGET_DIR"; static final String TARGET_DIR_DEFAULT = "./config/stage/dev/tls"; static final String P12_OPT = "--pkcs12"; static final String P12_FILE_OPT= "--p12-file"; static final String P12_FILE_ENV= "P12_FILE"; static final String P12_FILE_DEFAULT = "acme.test+1.p12"; static final String CLIENT = "--client"; static final String KEEP_OPT = "--keep"; static final String PEM_FILE_GLOB = "glob:**/*.pem"; public static void main(String[] args) throws IOException, InterruptedException { var argList = Arrays.asList(args); var showHelp = argList.contains(HELP_CMD); if (showHelp) { System.out.println("Certificates generator for keycloak environment"); System.out.printf("%s will support the following options as command line parameters: %n", "createTlsCerts.java"); System.out.println(""); System.out.printf("Options can be set by environment-variables %s and %s", DOMAIN_ENV, TARGET_DIR_ENV); System.out.println(""); System.out.printf("%s: %s%n", DOMAIN_OPT, "override the domain used for certificats"); System.out.printf("%s: %s%n", TARGET_DIR_OPT, "override the target folder to place the certificates in"); System.out.printf("%s: %s%n", P12_OPT, "Generate a legacy .p12 (PFX) pkcs12 container instead if a domain.pem and domain-key.pem file."); System.out.printf("%s: %s%n", KEEP_OPT, "Keep existing (.pem and .p12) files."); System.out.println(""); System.out.printf("Example: %s=%s %s=%s", DOMAIN_OPT, DOMAIN_DEFAULT, TARGET_DIR_OPT, TARGET_DIR_DEFAULT); System.out.println(""); System.exit(0); } /* Set options from env, commandline or default */ var domain = Optional.ofNullable(System.getenv(DOMAIN_ENV)).orElse(argList.stream().filter(s -> s.startsWith(DOMAIN_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(DOMAIN_DEFAULT)); var targetDir = Optional.ofNullable(System.getenv(TARGET_DIR_ENV)).orElse(argList.stream().filter(s -> s.startsWith(TARGET_DIR_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(TARGET_DIR_DEFAULT)); var p12File = Optional.ofNullable(System.getenv(P12_FILE_ENV)).orElse(argList.stream().filter(s -> s.startsWith(P12_FILE_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(P12_FILE_DEFAULT)); /* Assure required folder exists */ var folder = new File(targetDir); if (!folder.exists()) { System.out.printf("Creating missing %s folder at %s success:%s%n" , targetDir, folder.getAbsolutePath(), folder.mkdirs()); } if (!argList.contains(KEEP_OPT)) { /* Delete existing cert-files */ Files.list(Paths.get(targetDir)) .filter(p -> FileSystems.getDefault().getPathMatcher(PEM_FILE_GLOB).matches(p)) .forEach(f -> f.toFile().delete()); } /* Create mkcert command */ var commandLine = new ArrayList(); commandLine.add("mkcert"); commandLine.add("-install"); if (argList.contains(P12_OPT)) { commandLine.add("-pkcs12"); } if (argList.contains(CLIENT)) { commandLine.add("-p12-file"); commandLine.add(p12File); commandLine.add("-client"); } commandLine.add(domain); commandLine.add("*." + domain); /* Execute mkcert command */ var pb = new ProcessBuilder(commandLine); pb.directory(new File(targetDir)); pb.inheritIO(); var mkCertCommandsReturnCode = 0; try { var processMkcert = pb.start(); mkCertCommandsReturnCode = processMkcert.waitFor(); if (mkCertCommandsReturnCode > 0) { System.out.println("Please install mkcert."); System.exit(mkCertCommandsReturnCode); } } catch (Exception e) { System.out.println("Please install mkcert."); System.exit(mkCertCommandsReturnCode); } /* List created files */ Files.list(Paths.get(targetDir)).filter(p -> FileSystems.getDefault().getPathMatcher(PEM_FILE_GLOB).matches(p)).forEach(System.out::println); System.exit(mkCertCommandsReturnCode); } } ================================================ FILE: bin/envcheck.java ================================================ import java.io.File; import java.io.IOException; import java.nio.file.LinkOption; import java.util.List; import static java.nio.file.Files.getOwner; class envcheck { public static void main(String[] args) throws IOException, InterruptedException { var returnCode = 0; /* Check required tools: maven */ var pbMaven = new ProcessBuilder(List.of("mvn", "-version")); pbMaven.inheritIO(); var processMaven = pbMaven.start(); returnCode += processMaven.waitFor(); if (returnCode > 0) { System.out.println("Please install maven."); } /* Check required tools: docker compose */ var pbDockerComposer = new ProcessBuilder(List.of("docker", "compose", "version")); pbDockerComposer.inheritIO(); var processDockerComposer = pbDockerComposer.start(); returnCode += processDockerComposer.waitFor(); if (returnCode > 0) { System.out.println("Please install docker compose."); } /* Check required tools: mkcert */ System.out.print("mkcert: "); var pbMkcert = new ProcessBuilder(List.of("mkcert", "-version")); pbMkcert.inheritIO(); try { pbMkcert.start(); var processMkcert = pbMkcert.start(); returnCode += processMkcert.waitFor(); if (returnCode > 0) { System.out.println("Please install mkcert."); } } catch (Exception e) { System.out.println("Please install mkcert."); } /*Check directories exist */ var requiredDirectories = List.of("./keycloak/extensions/target/classes", "./keycloak/imex","./keycloak/themes/apps", "./deployments/local/dev/run/keycloak/data", "./keycloak/extensions/target/classes", "./keycloak/themes/internal", "./keycloak/config", "./keycloak/cli"); requiredDirectories.forEach(requiredDirectoryString -> { var requiredDirectory = new File(requiredDirectoryString); if (!requiredDirectory.exists()) { System.out.printf("Path \"%s\" required. Please create it or build the project with maven.%n", requiredDirectoryString); } else { try { var currentUser = System.getProperty("user.name"); var fileOwner = getOwner(requiredDirectory.toPath(), LinkOption.NOFOLLOW_LINKS).getName(); if (!currentUser.equals(fileOwner)) { System.out.printf("Path \"%s\" has wrong owner \"%s\" required. Please adjust it to \"%s\"%n", requiredDirectoryString, fileOwner, currentUser); } } catch (IOException e) { e.printStackTrace(); } } }); System.exit(returnCode); } } ================================================ FILE: bin/importCertificateIntoTruststore.java ================================================ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; public class importCertificateIntoTruststore { static final String HELP_CMD = "--help"; static final String FILE_OPT = "--file"; static final String ALIAS_OPT = "--alias"; static final String TRUST_STORE_OPT = "--truststore"; static final String TRUST_STORE_PASSWORD_OPT = "--password"; public static void main(String[] args) throws IOException, InterruptedException { /* keytool \ -import \ -file config/stage/dev/tls/acme.test+1.pem \ -cacerts \ -alias id.acme.test -noprompt \ -storepass changeit */ var argList = Arrays.asList(args); var showHelp = argList.contains(HELP_CMD); if (showHelp) { System.out.println("Imports the given certificate in the given truststore"); System.out.printf("%s will support the following options as command line parameters: %n", "importCertIntoTruststore.java"); System.out.println(); System.out.printf("%s: %s%n", FILE_OPT, "Path to the certificate file"); System.out.printf("%s: %s%n", ALIAS_OPT, "Alias for import"); System.out.printf("%s: %s%n", TRUST_STORE_OPT, "Path to the truststore or cacerts for the JVM truststore"); System.out.printf("%s: %s%n", TRUST_STORE_PASSWORD_OPT, "Oasswird for truststore. cacerts default password is changeit"); System.out.println(); System.out.println("Example: java bin/importCertificateIntoTruststore.java --file=config/stage/dev/tls/acme.test+1.pem --alias=id.acme.test --truststore=cacerts --password=changeit"); System.exit(0); } var file = argList.stream().filter(arg -> arg.matches(FILE_OPT + "=[^ ]+")) .findFirst().map(arg -> arg.split("=")[1]) .orElseThrow(() -> new IllegalArgumentException("Missing --file parameter")); var alias = argList.stream().filter(arg -> arg.matches(ALIAS_OPT + "=[^ ]+")) .findFirst().map(arg -> arg.split("=")[1]) .orElseThrow(() -> new IllegalArgumentException("Missing --alias parameter")); var truststorePath = argList.stream().filter(arg -> arg.matches(TRUST_STORE_OPT + "=[^ ]+")) .findFirst().map(arg -> arg.split("=")[1]) .orElseThrow(() -> new IllegalArgumentException("Missing --truststore parameter")); var password = argList.stream().filter(arg -> arg.matches(TRUST_STORE_PASSWORD_OPT + "=[^ ]+")) .findFirst().map(arg -> arg.split("=")[1]) .orElseThrow(() -> new IllegalArgumentException("Missing --password parameter")); var commandLine = new ArrayList(); commandLine.add("keytool"); commandLine.add("-import"); commandLine.add("-file"); commandLine.add(file); // "config/stage/dev/tls/acme.test+1.pem" if ("cacerts".equals(truststorePath)) { commandLine.add("-cacerts"); } else { commandLine.add("-keystore"); commandLine.add(truststorePath); } commandLine.add("-alias"); commandLine.add(alias); commandLine.add("-storepass"); commandLine.add(password); var pb = new ProcessBuilder(commandLine); pb.directory(new File(".")); pb.inheritIO(); var process = pb.start(); var exitCode = process.waitFor(); System.out.println("Certificate imported"); System.exit(exitCode); } } ================================================ FILE: bin/installOtel.java ================================================ import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; class installOtel { public static void main(String[] args) throws Exception { var otelFilePath = Paths.get("bin/opentelemetry-javaagent.jar"); var otelAlreadyPresent = Files.exists(otelFilePath); if (otelAlreadyPresent) { System.out.println("OpenTelemetry javaagent already installed at " + otelFilePath); System.exit(0); return; } var otelVersion = args.length == 0 ? "v1.25.0" : args[0]; System.out.println("Downloading OpenTelemetry javaagent version " + otelVersion); downloadFile("https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/" + otelVersion + "/opentelemetry-javaagent.jar", otelFilePath); System.out.println("OpenTelemetry javaagent saved to " + otelFilePath); } public static void downloadFile(String url, Path filePath) { try { try (InputStream in = new URL(url).openStream()) { Files.copy(in, filePath); } } catch (Exception e) { throw new RuntimeException("Error downloading file: " + e.getMessage(), e); } } } ================================================ FILE: bin/keycloakConfigCli.default.env ================================================ KEYCLOAK_USER=admin KEYCLOAK_PASSWORD=admin KEYCLOAK_SSLVERIFY=false KEYCLOAK_AVAILABILITYCHECK_ENABLED=true KEYCLOAK_AVAILABILITYCHECK_TIMEOUT=3s IMPORT_FORCE=false IMPORT_VARSUBSTITUTION=true ================================================ FILE: bin/keycloakConfigCli.java ================================================ import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Optional; class keycloakConfigCli { public static final String CONFIG_PATH_IN_CONTAINER = "/config"; static final String HELP_CMD = "--help"; static final String IMPORT_OPT = "--import"; static final String IMPORT_ENV = "IMPORT"; static final String ENV_FILE_OPT = "--env-file"; static final String ENV_FILE_ENV = "ENV_FILE"; static final String ENV_FILE_DEFAULT = "bin/keycloakConfigCli.default.env"; static final String KEYCLOAK_URL_OPT = "--keycloak-url"; static final String KEYCLOAK_URL_ENV = "KEYCLOAK_FRONTEND_URL"; static final String KEYCLOAOK_CONFIG_CLI_VERSION_OPT = "--cli-version"; static final String KEYCLOAOK_CONFIG_CLI_VERSION_ENV = "KEYCLOAOK_CONFIG_CLI_VERSION"; static final String KEYCLOAOK_CONFIG_CLI_VERSION_DEFAULT = "latest"; public static void main(String[] args) throws Exception { var argList = Arrays.asList(args); var showHelp = argList.contains(HELP_CMD); if (showHelp) { System.out.println("Execute a defined keycloak-config-cli folder or file against a running keycloak instance"); System.out.printf("%s will support the following options as command line parameters: %n", "keycloakConfigCli.java"); System.out.println(""); System.out.printf("Options can be set by environment-variables %s, %s, %s and %s", IMPORT_ENV, ENV_FILE_ENV, KEYCLOAK_URL_ENV, KEYCLOAOK_CONFIG_CLI_VERSION_ENV); System.out.println(""); System.out.printf("%s: %s%n", IMPORT_OPT, "override file or folder (all files inside will be used) to import"); System.out.printf("%s: %s%n", ENV_FILE_OPT, "override default env file for further options"); System.out.printf("%s: %s%n", KEYCLOAK_URL_OPT, "override default keycloak url to apply config to"); System.out.printf("%s: %s%n", KEYCLOAOK_CONFIG_CLI_VERSION_OPT, "override default version of keycloak-config-cli"); System.out.println(""); System.out.printf("Example: %s=%s %s=%s %s=%s %s=%s", IMPORT_OPT, "my-config-cli.yaml", KEYCLOAK_URL_OPT, "http://localhost:8080/auth", ENV_FILE_OPT, ENV_FILE_DEFAULT, KEYCLOAOK_CONFIG_CLI_VERSION_OPT, KEYCLOAOK_CONFIG_CLI_VERSION_DEFAULT); System.out.println(""); System.exit(0); } var configFileOrFolder = Optional.ofNullable(System.getenv(IMPORT_ENV)).orElse(argList.stream().filter(s -> s.startsWith(IMPORT_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElseThrow(() -> new IllegalStateException("Please provide a keycloak-config-cli file or folder to import with " + IMPORT_OPT))); var keycloakUrl = Optional.ofNullable(System.getenv(KEYCLOAK_URL_ENV)).orElse(argList.stream().filter(s -> s.startsWith(KEYCLOAK_URL_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElseThrow(() -> new IllegalStateException("Please provide a keycloak-url to apply import to " + KEYCLOAK_URL_OPT))); var envFile = Optional.ofNullable(System.getenv(ENV_FILE_ENV)).orElse(argList.stream().filter(s -> s.startsWith(ENV_FILE_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(ENV_FILE_DEFAULT)); var keycloakConfigCliVersion = Optional.ofNullable(System.getenv(KEYCLOAOK_CONFIG_CLI_VERSION_ENV)).orElse(argList.stream().filter(s -> s.startsWith(KEYCLOAOK_CONFIG_CLI_VERSION_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(KEYCLOAOK_CONFIG_CLI_VERSION_DEFAULT)); var configFileOrFolderAsFile = Path.of(configFileOrFolder).toRealPath(LinkOption.NOFOLLOW_LINKS).toFile(); var pathToConfig = ""; var fileOrDirectoryNameOfConfig = ""; if (configFileOrFolderAsFile.isFile()) { pathToConfig = configFileOrFolderAsFile.getParent(); fileOrDirectoryNameOfConfig = CONFIG_PATH_IN_CONTAINER + "/" + configFileOrFolderAsFile.getName(); } else { pathToConfig = configFileOrFolderAsFile.getPath(); fileOrDirectoryNameOfConfig = CONFIG_PATH_IN_CONTAINER + "/"; } var commandLine = new ArrayList(); commandLine.add("docker"); commandLine.add("run"); commandLine.add("--rm"); commandLine.add("--network"); commandLine.add("host"); commandLine.add("--env-file"); commandLine.add(envFile); commandLine.add("-e"); commandLine.add("IMPORT_PATH=" + fileOrDirectoryNameOfConfig); commandLine.add("-e"); commandLine.add("KEYCLOAK_URL=" + keycloakUrl); commandLine.add("-v"); commandLine.add(pathToConfig + ":" + CONFIG_PATH_IN_CONTAINER); commandLine.add("quay.io/adorsys/keycloak-config-cli:" + keycloakConfigCliVersion); var pb = new ProcessBuilder(commandLine); pb.inheritIO(); var process = pb.start(); System.exit(process.waitFor()); } } ================================================ FILE: bin/realmImex.java ================================================ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.Buffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Scanner; class realmImex { static final String HELP_CMD = "--help"; static final String MIGRATION_REALM_OPT = "--realm"; static final String MIGRATION_REALM_ENV = "REALM"; static final String MIGRATION_REALM_DEFAULT = "custom"; static final String MIGRATION_ACTION_OPT = "--action"; static final String MIGRATION_ACTION_ENV = "ACTION"; static final String MIGRATION_ACTION_DEFAULT = "export"; static final String ADDITIONAL_OPTIONS_OPT = "--options"; static final String ADDITIONAL_OPTIONS_ENV = "OPTIONS"; static final String ADDITIONAL_OPTIONS_DEFAULT = ""; static final String VERBOSE_CMD = "--verbose"; public static void main(String[] args) throws IOException, InterruptedException { var argList = Arrays.asList(args); var showHelp = argList.contains(HELP_CMD); if (showHelp) { System.out.println("Realm import/export for keycloak environment"); System.out.printf("%s will support the following options as command line parameters: %n", "realmImex.java"); System.out.println(""); System.out.printf("Options can be set by environment-variables %s,%s and %s", MIGRATION_REALM_ENV, MIGRATION_ACTION_ENV, ADDITIONAL_OPTIONS_ENV); System.out.println(""); System.out.printf("%s: %s%n", MIGRATION_REALM_OPT, "override the realm to migrate"); System.out.printf("%s: %s%n", MIGRATION_ACTION_OPT, "override migration action: import or export"); System.out.printf("%s: %s%n", ADDITIONAL_OPTIONS_OPT, "override the target folder to place the certificates in"); System.out.printf("%s: %s%n", VERBOSE_CMD, "make the output of the migrate process visible on stdout"); System.out.println(""); System.out.printf("Example: %s=%s %s=%s %s=%s", MIGRATION_REALM_OPT, MIGRATION_REALM_DEFAULT, MIGRATION_ACTION_OPT, MIGRATION_ACTION_DEFAULT, ADDITIONAL_OPTIONS_OPT, ADDITIONAL_OPTIONS_DEFAULT); System.out.println(""); System.exit(0); } var realmName = Optional.ofNullable(System.getenv(MIGRATION_REALM_ENV)).orElse(argList.stream().filter(s -> s.startsWith(MIGRATION_REALM_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(MIGRATION_REALM_DEFAULT)); var additionalOptions = Optional.ofNullable(System.getenv(ADDITIONAL_OPTIONS_ENV)).orElse(argList.stream().filter(s -> s.startsWith(ADDITIONAL_OPTIONS_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(ADDITIONAL_OPTIONS_DEFAULT)); var verbose = argList.contains(VERBOSE_CMD); var migrationAction = Optional.ofNullable(System.getenv(MIGRATION_ACTION_ENV)).orElse(argList.stream().filter(s -> s.startsWith(MIGRATION_ACTION_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(MIGRATION_ACTION_DEFAULT)); var commandLine = new ArrayList(); commandLine.add("docker"); commandLine.add("compose"); commandLine.add("--env-file"); commandLine.add("keycloak.env"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose.yml"); commandLine.add("exec"); commandLine.add("-T"); commandLine.add("-e"); commandLine.add("DEBUG=false"); commandLine.add("acme-keycloak"); commandLine.add("/opt/keycloak/bin/kc.sh"); commandLine.add(migrationAction); commandLine.add("--file"); commandLine.add("/opt/keycloak/imex/" + realmName + "-realm.json"); commandLine.add("--users"); commandLine.add("same_file"); commandLine.add("--realm"); commandLine.add(realmName); if (additionalOptions != null && !"".equals(additionalOptions.trim())) { commandLine.add(additionalOptions); } if (verbose) { System.out.println("Command-Line: "); System.out.println(commandLine); } System.out.printf("Starting realm %s.%n", migrationAction); var pb = new ProcessBuilder(commandLine); pb.redirectErrorStream(true); var process = pb.start(); try (var scanner = new Scanner(process.getInputStream())) { while (scanner.hasNextLine()) { var line = scanner.nextLine(); if (line.contains("KC-SERVICES0034") || line.contains("KC-SERVICES0031")) { System.out.println(line); continue; } if (line.contains("KC-SERVICES0035") || line.contains("KC-SERVICES0032")) { System.out.println(line); process.destroy(); System.exit(0); } if (verbose) { System.out.println(line); } } System.out.printf("Something went wrong, please check output with %s%n", VERBOSE_CMD); System.exit(process.waitFor()); } } } ================================================ FILE: config/stage/dev/grafana/provisioning/dashboards/dashboard.yml ================================================ apiVersion: 1 providers: - name: 'Prometheus' orgId: 1 folder: '' type: file disableDeletion: false editable: true options: path: /etc/grafana/provisioning/dashboards ================================================ FILE: config/stage/dev/grafana/provisioning/dashboards/keycloak-capacity-planning-dashboard.json ================================================ { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "9.4.7" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "links": [], "liveNow": false, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "Shows the number of password validations performed by Keycloak", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 0, "y": 0 }, "id": 8, "options": { "legend": { "calcs": [], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_credentials_password_hashing_validations_total{namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "legendFormat": "{{pod}} => {{outcome}} - {{algorithm}}:{{hashing_strength}}", "range": true, "refId": "A" } ], "title": "Password validations rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 10, "y": 0 }, "id": 2, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"code_to_token\", namespace=\"$namespace\", realm=\"$realm\", error!=\"\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{pod}}:{{error}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"code_to_token\", namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}", "range": true, "refId": "B" } ], "title": "Code to Token Events Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 0, "y": 10 }, "id": 1, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"client_login\", namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{pod}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"client_login\", namespace=\"$namespace\", realm=\"$realm\", error!=\"\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}:{{error}}", "range": true, "refId": "B" } ], "title": "Client Login Events Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 10, "y": 10 }, "id": 5, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"refresh_token\", namespace=\"$namespace\", realm=\"$realm\", error!=\"\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{pod}}:{{error}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"refresh_token\", namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}", "range": true, "refId": "B" } ], "title": "Refresh Token Events Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 0, "y": 20 }, "id": 3, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"login\", namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"login\", namespace=\"$namespace\", realm=\"$realm\", error!=\"\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}:{{error}}", "range": true, "refId": "B" } ], "title": "Login Events Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 10, "y": 20 }, "id": 4, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"logout\", namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{pod}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"logout\", namespace=\"$namespace\", realm=\"$realm\", error!=\"\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}:{{error}}", "range": true, "refId": "B" } ], "title": "Logout Events Rate", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 10, "x": 0, "y": 30 }, "id": 6, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"token_exchange\", namespace=\"$namespace\", realm=\"$realm\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "irate(keycloak_user_events_total{event=\"token_exchange\", namespace=\"$namespace\", realm=\"$realm\", error!=\"\"}[$__rate_interval])", "hide": false, "intervalFactor": 1, "legendFormat": "{{pod}}:{{error}}", "range": true, "refId": "B" } ], "title": "Token Exchange Events Rate", "type": "timeseries" } ], "refresh": "", "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [], "templating": { "list": [ { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(namespace)", "hide": 0, "includeAll": false, "label": "namespace", "multi": false, "name": "namespace", "options": [], "query": { "query": "label_values(namespace)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(realm)", "hide": 0, "includeAll": false, "label": "realm", "multi": false, "name": "realm", "options": [], "query": { "query": "label_values(realm)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query" } ] }, "time": { "from": "now-30m", "to": "now" }, "timepicker": {}, "timezone": "browser", "title": "Keycloak capacity planning dashboard", "uid": "dtvmgcVNk", "version": 1, "weekStart": "monday" } ================================================ FILE: config/stage/dev/grafana/provisioning/dashboards/keycloak-metrics_rev1.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "datasource", "uid": "grafana" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "description": "Visualize all Metrics for Keycloak 21+", "editable": true, "fiscalYearStartMonth": 0, "gnetId": 13489, "graphTooltip": 0, "links": [ { "asDropdown": true, "icon": "external link", "includeVars": false, "keepTime": false, "tags": [], "targetBlank": true, "title": "MicroProfile Home", "type": "link", "url": "https://microprofile.io" }, { "asDropdown": true, "icon": "external link", "includeVars": false, "keepTime": false, "tags": [], "targetBlank": true, "title": "SmallRye Home", "type": "link", "url": "https://smallrye.io" } ], "liveNow": false, "panels": [ { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 159, "panels": [], "repeat": "PROMETHEUS_DS", "repeatDirection": "h", "title": "Keycloak Metrics", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 1 }, "id": 160, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "builder", "expr": "sum by(client_id) (keycloak_auth_user_login_success_total{instance=\"$instance\"})", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Logins", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 1 }, "id": 161, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "builder", "expr": "sum by(error) (keycloak_auth_user_login_error_total{instance=\"$instance\"})", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Login Errors", "type": "timeseries" }, { "collapse": false, "collapsed": false, "datasource": { "uid": "$PROMETHEUS_DS" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "id": 139, "panels": [], "showTitle": true, "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "refId": "A" } ], "title": "Base Metrics", "titleSize": "h1", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the \"recent cpu usage\" for the Java Virtual Machine process.", "fieldConfig": { "defaults": { "unit": "percentunit" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 6, "w": 6, "x": 0, "y": 10 }, "hiddenSeries": false, "id": 144, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "maxDataPoints": 100, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "9.5.1", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_cpu_processCpuLoad{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Process CPU Load", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:1215", "format": "percentunit", "logBase": 1, "show": true }, { "$$hashKey": "object:1216", "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the uptime of the Java virtual machine", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "format": "s", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 6, "y": 10 }, "id": 149, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 2, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_jvm_uptime{env=\"$env\",job=\"$job\",instance=~\"$instance\"}/1000", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": "", "title": "JVM Uptime", "type": "stat", "valueFontSize": "110%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the number of processors available to the Java virtual machine. This value may change during a particular invocation of the virtual machine.", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 9, "y": 10 }, "id": 143, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 2, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_cpu_availableProcessors{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": "", "title": "Available Processors", "type": "stat", "valueFontSize": "110%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Number of currently deployed threads", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 6, "x": 12, "y": 10 }, "id": 156, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 2, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_thread_count{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": "", "title": "Current Thread count", "type": "stat", "valueFontSize": "110%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the peak live thread count since the Java virtual machine started or peak was reset. This includes daemon and non-daemon threads.", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 6, "x": 18, "y": 10 }, "id": 158, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 2, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_thread_max_count{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": "", "title": "Peak Thread Count", "type": "stat", "valueFontSize": "110%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the system load average for the last minute. The system load average is the sum of the number of runnable entities queued to the available processors and the number of runnable entities running on the available processors averaged over a period of time. The way in which the load average is calculated is operating system specific but is typically a damped time-dependent average. If the load average is not available, a negative value is displayed. This attribute is designed to provide a hint about the system load and may be queried frequently. The load average may be unavailable on some platform where it is expensive to implement this method.", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 6, "y": 13 }, "id": 146, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_cpu_systemLoadAverage{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "title": "System Load Average", "type": "stat" }, { "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the number of classes that are currently loaded in the Java virtual machine.", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 6, "x": 12, "y": 13 }, "id": 140, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 2, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_classloader_loadedClasses_count{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": "", "title": "Current Loaded Class Count", "type": "stat", "valueFontSize": "110%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "description": "Displays the current number of live daemon threads.", "fieldConfig": { "defaults": { "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 6, "x": 18, "y": 13 }, "id": 157, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.5.1", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "span": 2, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": true, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_thread_daemon_count{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{job}}/{{instance}} ", "refId": "A" } ], "thresholds": "", "title": "Daemon Thread Count", "type": "stat", "valueFontSize": "110%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "decimals": 2, "description": "Displays the amount of used memory and max memory.", "fill": 1, "fillGradient": 0, "gridPos": { "h": 10, "w": 8, "x": 0, "y": 16 }, "hiddenSeries": false, "id": 154, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": false, "show": true, "sort": "current", "sortDesc": false, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "maxPerRow": 4, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "9.5.1", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_memory_usedHeap_bytes{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ __name__ }}/{{ instance }}", "refId": "A" }, { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_memory_maxHeap_bytes{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ __name__ }}/{{ instance }}", "refId": "B" }, { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_memory_committedHeap_bytes{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "interval": "", "intervalFactor": 2, "legendFormat": "{{ __name__ }}/{{ instance }}", "refId": "C" } ], "thresholds": [], "timeRegions": [], "title": "Heap Memory", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:255", "decimals": 2, "format": "bytes", "logBase": 1, "min": 0, "show": true }, { "$$hashKey": "object:256", "decimals": 2, "format": "bytes", "logBase": 1, "min": 0, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": { "type": "datasource", "uid": "-- Mixed --" }, "decimals": 2, "description": "Displays the amount of used memory and max memory.", "fill": 1, "fillGradient": 0, "gridPos": { "h": 10, "w": 8, "x": 0, "y": 26 }, "hiddenSeries": false, "id": 155, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "maxPerRow": 4, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "9.5.1", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "span": 6, "stack": false, "steppedLine": false, "targets": [ { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_memory_usedNonHeap_bytes{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{ __name__ }}/{{instance}}", "refId": "A" }, { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_memory_maxNonHeap_bytes{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{ __name__ }}/{{instance}}", "refId": "B" }, { "datasource": { "uid": "$PROMETHEUS_DS" }, "expr": "base_memory_commitedNonHeap_bytes{env=\"$env\",job=\"$job\",instance=~\"$instance\"}", "interval": "", "intervalFactor": 2, "legendFormat": "a{{ __name__ }}/{{instance}}", "refId": "C" } ], "thresholds": [], "timeRegions": [], "title": "Non Heap Memory", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:173", "decimals": 2, "format": "bytes", "logBase": 1, "min": 0, "show": true }, { "$$hashKey": "object:174", "decimals": 2, "format": "bytes", "logBase": 1, "min": 0, "show": true } ], "yaxis": { "align": false } } ], "refresh": "", "schemaVersion": 38, "style": "dark", "tags": [ "java", "wildfly" ], "templating": { "list": [ { "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, "hide": 1, "includeAll": false, "multi": false, "name": "PROMETHEUS_DS", "options": [], "query": "prometheus", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "current": { "selected": false, "text": "dev", "value": "dev" }, "datasource": { "type": "prometheus", "uid": "$PROMETHEUS_DS" }, "definition": "", "hide": 0, "includeAll": false, "label": "Environment", "multi": false, "name": "env", "options": [], "query": "label_values(base_thread_count, env)", "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": false, "text": "keycloak", "value": "keycloak" }, "datasource": { "type": "prometheus", "uid": "$PROMETHEUS_DS" }, "definition": "", "hide": 0, "includeAll": false, "label": "Job", "multi": false, "name": "job", "options": [], "query": "label_values(base_thread_count{env=\"$env\"}, job)", "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": true, "text": [ "acme-keycloak:8080" ], "value": [ "acme-keycloak:8080" ] }, "datasource": { "type": "prometheus", "uid": "$PROMETHEUS_DS" }, "definition": "", "hide": 0, "includeAll": false, "label": "Instance", "multi": true, "name": "instance", "options": [], "query": "label_values(base_thread_count{env=\"$env\",job=\"$job\"}, instance)", "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-3h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "browser", "title": "Keycloak Metrics", "uid": "gPTqKTAGk", "version": 2, "weekStart": "" } ================================================ FILE: config/stage/dev/grafana/provisioning/dashboards/keycloak-troubleshooting-dashboard.json ================================================ { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "panel", "id": "gauge", "name": "Gauge", "version": "" }, { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "9.4.7" }, { "type": "panel", "id": "heatmap", "name": "Heatmap", "version": "" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" }, { "type": "panel", "id": "table", "name": "Table", "version": "" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, "id": null, "links": [], "liveNow": false, "panels": [ { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 8, "panels": [], "title": "SLO Metrics", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 2, "mappings": [], "max": 1, "min": 0, "thresholds": { "mode": "percentage", "steps": [ { "color": "red", "value": null }, { "color": "dark-green", "value": 99.9 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 0, "y": 1 }, "id": 6, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "count_over_time(\n sum (up{\n container=\"keycloak\", \n namespace=\"$namespace\"\n } > 0)[$__range:$__interval]\n)\n/\ncount_over_time(vector(1)[$__range:$__interval])", "hide": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Availability", "transformations": [], "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 2, "mappings": [], "max": 1, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 0.95 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 3, "y": 1 }, "id": 14, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum(\n rate(\n http_server_requests_seconds_bucket{\n uri=~\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\", \n le=\"0.25\", \n container=\"keycloak\", \n namespace=\"$namespace\"}\n [$__range] \n )\n) without (le,uri,status,outcome,method,pod,instance) \n/\nsum(\n rate(\n http_server_requests_seconds_count{\n uri=~\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\", \n container=\"keycloak\",\n namespace=\"$namespace\"}\n [$__range] \n )\n) without (le,uri,status,outcome,method,pod,instance)", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Responses below 250ms", "transformations": [], "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 0.001 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 5, "w": 3, "x": 6, "y": 1 }, "id": 16, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum(\n rate(\n http_server_requests_seconds_count{\n uri=~\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\", \n outcome=\"SERVER_ERROR\", \n container=\"keycloak\", \n namespace=\"$namespace\"}\n [$__range] \n )\n) or vector(0) \n/\nsum(\n rate(\n http_server_requests_seconds_count{\n uri=~\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\", \n container=\"keycloak\", \n namespace=\"$namespace\"}\n [$__range] \n )\n)", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Error responses", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "This Prometheus query calculates the percentage of authentication requests that completed within 0.25 seconds relative to all authentication requests for specific Keycloak endpoints, targeting a particular namespace and pod, over the past 5 minutes.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "dashed+area" } }, "mappings": [], "max": 1.01, "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "dark-green", "value": 0.95 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 6 }, "id": 2, "options": { "legend": { "calcs": [ "mean", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": true, "expr": "sum(\n irate(\n http_server_requests_seconds_bucket{\n uri=~\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\", \n le=\"0.25\", \n container=\"keycloak\", \n namespace=\"$namespace\"}\n [5m] \n )\n) without (le,uri,status,outcome,method,instance) \n/\nsum(\n irate(\n http_server_requests_seconds_count{\n uri=~\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions.*\", \n container=\"keycloak\",\n namespace=\"$namespace\"}\n [5m] \n )\n) without (le,uri,status,outcome,method,instance)", "instant": false, "legendFormat": "{{pod}}", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum(\n irate(\n http_server_requests_seconds_bucket{\n uri=~\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\", \n le=\"0.25\", \n container=\"keycloak\", \n namespace=\"$namespace\"}\n [5m] \n )\n) without (le,uri,status,outcome,method,pod,instance) \n/\nsum(\n irate(\n http_server_requests_seconds_count{\n uri=~\"/realms/{realm}/protocol/{protocol}/.*|/realms/{realm}/login-actions/.*\", \n container=\"keycloak\",\n namespace=\"$namespace\"}\n [5m] \n )\n) without (le,uri,status,outcome,method,pod,instance)", "hide": false, "legendFormat": "All pods", "range": true, "refId": "B" } ], "title": "Changes in % of responses below 250ms", "transformations": [], "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "This Prometheus query calculates the percentage of authentication requests that returned a server side error for all authentication requests, targeting a particular namespace, over the past 5 minutes.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 6, "y": 6 }, "id": 4, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by(uri)\n(\n rate(\n http_server_requests_seconds_count{\n outcome=\"SERVER_ERROR\",\n uri=~\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions/.*\", \n container=\"keycloak\", \n namespace=\"$namespace\"}\n [5m] \n )\n)\n/\nsum by (uri)\n(\n rate(\n http_server_requests_seconds_count{\n uri=~\"/realms/{realm}/protocol/{protocol}.*|/realms/{realm}/login-actions/.*\", \n container=\"keycloak\",\n namespace=\"$namespace\"}\n [5m] \n )\n)", "hide": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Changes in % of Error responses", "transformations": [], "type": "timeseries" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }, "id": 10, "panels": [], "title": "JVM Metrics", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "align": "auto", "cellOptions": { "type": "auto" }, "inspect": false }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [ { "matcher": { "id": "byName", "options": "runtime" }, "properties": [ { "id": "custom.width", "value": 154 } ] } ] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 23 }, "id": 12, "options": { "footer": { "countRows": false, "fields": "", "reducer": [ "sum" ], "show": false }, "frameIndex": 0, "showHeader": true, "sortBy": [] }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": true, "expr": "max(jvm_info_total{namespace=\"$namespace\"}) by (pod, vendor, runtime, version)", "format": "table", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "JVM Info", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "Value": true }, "indexByName": {}, "renameByName": {} } } ], "type": "table" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "This is available only in Kubernetes and only if your pods have cpu limits set", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 6, "y": 23 }, "id": 346, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum(rate(container_cpu_usage_seconds_total{container=\"keycloak\", namespace=\"$namespace\"}[5m])) by (pod) /\nsum(container_spec_cpu_quota{container=\"keycloak\", namespace=\"$namespace\"}/container_spec_cpu_period{container=\"keycloak\", namespace=\"$namespace\"}) by (pod)", "hide": false, "legendFormat": "__auto", "range": true, "refId": "B" } ], "title": "KUBERNETES - CPU Usage percentage", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 12, "y": 23 }, "id": 20, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum_over_time((sum by (pod) (jvm_memory_used_bytes{namespace=\"$namespace\"}))[$__range:$__interval]) / count_over_time((sum by (pod) (jvm_memory_used_bytes{namespace=\"$namespace\"}))[$__range:$__interval]) ", "hide": false, "legendFormat": "__auto", "range": true, "refId": "B" } ], "title": "Average memory usage", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 39 }, "id": 18, "options": { "legend": { "calcs": [ "lastNotNull", "min", "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (jvm_memory_used_bytes{namespace=\"$namespace\"})", "hide": false, "legendFormat": "{{pod}} - used", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (jvm_memory_committed_bytes{namespace=\"$namespace\"})", "hide": false, "legendFormat": "{{pod}} - committed", "range": true, "refId": "B" } ], "title": "JVM memory used vs committed", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 6, "y": 39 }, "id": 22, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum(\n rate(\n jvm_gc_pause_seconds_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}\n [$__range] \n )\n) without (action,instance,cause) \n/\nsum(\n rate(\n jvm_gc_pause_seconds_count{\n container=\"keycloak\",\n namespace=\"$namespace\"}\n [$__range] \n )\n) without (action,instance,cause)", "hide": false, "legendFormat": "{{pod}} - {{gc}}", "range": true, "refId": "B" } ], "title": "Average GC time", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 41, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 12, "y": 39 }, "id": 142, "options": { "legend": { "calcs": [ "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod,cause)(irate(jvm_gc_pause_seconds_sum{container=\"keycloak\", namespace=\"$namespace\"}[5m]))", "legendFormat": "{{pod}} - {{cause}}", "range": true, "refId": "A" } ], "title": "Changes in average GC times in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 55 }, "id": 140, "options": { "legend": { "calcs": [ "lastNotNull", "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod,cause)(irate(jvm_gc_pause_seconds_count{container=\"keycloak\", namespace=\"$namespace\"}[5m]))", "legendFormat": "{{pod}} - {{cause}}", "range": true, "refId": "A" } ], "title": "Number of GC events in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "I am not sure about this metric. I would expect _max to be only increasing number while here it is going up and down so I am not sure what this really means", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 6, "y": 55 }, "id": 24, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "max by (pod,cause) (jvm_gc_pause_seconds_max{container=\"keycloak\", namespace=\"$namespace\"})", "legendFormat": "{{pod}} - {{cause}}", "range": true, "refId": "A" } ], "title": "Maximum GC time and cause", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The percentage of CPU time spent on garbage collection, indicating the impact of GC on application performance in the JVM. It refers to the proportion of the total CPU processing time that is dedicated to executing garbage collection (GC) operations, as opposed to running application code or performing other tasks. This metric helps determine how much overhead GC introduces, affecting the overall performance of the Keycloak’s JVM.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 12, "y": 55 }, "id": 26, "options": { "legend": { "calcs": [ "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (jvm_gc_overhead{container=\"keycloak\", namespace=\"$namespace\"})", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "JVM GC CPU overhead %", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 71 }, "id": 224, "options": { "legend": { "calcs": [ "min", "max", "lastNotNull" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": true, "expr": "sum by (pod)(jvm_threads_states_threads{namespace=\"$namespace\", container=\"keycloak\", state=\"waiting\"})", "format": "time_series", "hide": false, "instant": false, "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "JVM waiting threads", "type": "timeseries" }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 87 }, "id": 28, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 67 }, "id": 30, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "value_and_name" }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "avg by (pod) (sum_over_time(agroal_active_count{namespace=\"$namespace\"}[$__range:$__interval]) / sum_over_time((agroal_active_count{namespace=\"$namespace\"} + agroal_available_count{namespace=\"$namespace\"})[$__range:$__interval]))", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Connection pool utilization %", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 6, "y": 67 }, "id": 32, "options": { "legend": { "calcs": [ "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "agroal_awaiting_count{container=\"keycloak\", namespace=\"$namespace\"}", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Threads waiting for database connection", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 2, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 12, "y": 67 }, "id": 34, "options": { "legend": { "calcs": [ "max", "min", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "agroal_available_count{job=\"${namespace}/keycloak-metrics\",namespace=\"${namespace}\"}", "legendFormat": "{{pod}} - available connections", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "agroal_active_count{job=\"${namespace}/keycloak-metrics\",namespace=\"${namespace}\"}", "hide": false, "legendFormat": "{{pod}} - used connections", "range": true, "refId": "B" } ], "title": "Database connections pool", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 16, "w": 6, "x": 0, "y": 83 }, "id": 183, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(irate(agroal_acquire_count_total{container=\"keycloak\", namespace=\"$namespace\"}[5m]))", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Number of acquired connections in 5 minutes interval", "type": "timeseries" } ], "title": "Database Metrics", "type": "row" }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 88 }, "id": 36, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 68 }, "id": 38, "options": { "legend": { "calcs": [ "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (\n irate(\n http_server_requests_seconds_count{\n container=\"keycloak\",\n namespace=\"$namespace\"}\n [5m] \n )\n)", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Number of requests in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 12, "x": 6, "y": 68 }, "id": 42, "options": { "legend": { "calcs": [ "lastNotNull", "max" ], "displayMode": "table", "placement": "right", "showLegend": true, "sortBy": "Max", "sortDesc": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (uri,outcome) (http_server_requests_seconds_count{container=\"keycloak\", namespace=\"$namespace\"})", "format": "time_series", "legendFormat": "{{uri}} - {{outcome}}", "range": true, "refId": "A" } ], "title": "Total number of requests per URI and outcome", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "custom": { "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "scaleDistribution": { "type": "linear" } } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 83 }, "id": 40, "options": { "calculate": false, "cellGap": 1, "color": { "exponent": 0.5, "fill": "dark-orange", "mode": "scheme", "reverse": true, "scale": "exponential", "scheme": "Greens", "steps": 64 }, "exemplars": { "color": "rgba(255,0,255,0.7)" }, "filterValues": { "le": 1e-9 }, "legend": { "show": true }, "rowsFrame": { "layout": "auto" }, "tooltip": { "show": true, "yHistogram": false }, "yAxis": { "axisPlacement": "left", "decimals": 3, "reverse": false, "unit": "s" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "exemplar": true, "expr": "sum by (le) (idelta(http_server_requests_seconds_bucket{container=\"keycloak\", namespace=\"$namespace\"} [5m]))", "format": "heatmap", "legendFormat": "{{le}}", "range": true, "refId": "A" } ], "title": "All requests with processing time", "type": "heatmap" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 12, "x": 6, "y": 83 }, "id": 50, "options": { "legend": { "calcs": [ "max" ], "displayMode": "table", "placement": "right", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (uri,outcome) (irate(http_server_requests_seconds_count{container=\"keycloak\", namespace=\"$namespace\"}[5m]))", "format": "time_series", "legendFormat": "{{uri}} - {{outcome}}", "range": true, "refId": "A" } ], "title": "Total number of requests per URI and outcome in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The current number of active requests processed by each pod", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 98 }, "id": 44, "options": { "legend": { "calcs": [ "max" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (http_server_active_requests{container=\"keycloak\", namespace=\"$namespace\"})", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Active requests", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 12, "x": 6, "y": 98 }, "id": 264, "options": { "legend": { "calcs": [ "max" ], "displayMode": "table", "placement": "right", "showLegend": true, "sortBy": "Max", "sortDesc": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod,outcome) (irate(http_server_requests_seconds_count{container=\"keycloak\", namespace=\"$namespace\"}[5m]))", "format": "time_series", "legendFormat": "{{pod}} - {{outcome}}", "range": true, "refId": "A" } ], "title": "Changes in outcome types in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 113 }, "id": 49, "options": { "legend": { "calcs": [ "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(\n irate(\n http_server_bytes_written_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}\n [5m] \n )\n)\n/\nsum by (pod)(\n irate(\n http_server_bytes_written_count{\n container=\"keycloak\",\n namespace=\"$namespace\"}\n [5m] \n )\n)", "hide": false, "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Changes in response sizes in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 6, "y": 113 }, "id": 46, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(\n rate(\n http_server_bytes_written_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}\n [$__range] \n )\n)\n/\nsum by(pod)(\n rate(\n http_server_bytes_written_count{\n container=\"keycloak\",\n namespace=\"$namespace\"}\n [$__range] \n )\n)", "hide": false, "legendFormat": "__auto", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "http_server_bytes_written_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}", "hide": true, "legendFormat": "__auto", "range": true, "refId": "B" } ], "title": "Average response size", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 12, "y": 113 }, "id": 47, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(\n rate(\n http_server_bytes_read_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}\n [$__range] \n )\n)\n/\nsum by(pod)(\n rate(\n http_server_bytes_read_count{\n container=\"keycloak\",\n namespace=\"$namespace\"}\n [$__range] \n )\n)", "hide": false, "legendFormat": "__auto", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "http_server_bytes_written_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}", "hide": true, "legendFormat": "__auto", "range": true, "refId": "B" } ], "title": "Average request size", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 128 }, "id": 51, "options": { "legend": { "calcs": [ "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(\n irate(\n http_server_bytes_read_sum{\n container=\"keycloak\", \n namespace=\"$namespace\"}\n [5m] \n )\n)\n/\nsum by (pod)(\n irate(\n http_server_bytes_read_count{\n container=\"keycloak\",\n namespace=\"$namespace\"}\n [5m] \n )\n)", "hide": false, "legendFormat": "{{pod}} - requests", "range": true, "refId": "B" } ], "title": "Changes in request sizes in 5 minutes interval", "type": "timeseries" } ], "title": "HTTP Metrics", "type": "row" }, { "collapsed": true, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 89 }, "id": 58, "panels": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The approximate number of entries stored by the node, excluding backup copies.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 176 }, "id": 89, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(vendor_statistics_approximate_entries_unique{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"})", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Number of owned entries", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "The approximate number of entries stored by the node, including backup copies.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 6, "y": 176 }, "id": 90, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(vendor_statistics_approximate_entries_unique{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"})", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Number of all stored entries (including backups)", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "Eviction is the process to limit the cache size and, when full, an entry is removed to make room for a new entry to be cached. As Keycloak caches the database entities in the users, realms and authorization, database access always proceeds with an eviction event.\n\nA rapid increase of eviction and very high database CPU usage means the users or realms cache is too small for smooth Keycloak operation, as data needs to be re-loaded very often from the database which slows down responses. If enough memory is available, consider increasing the max cache size using the CLI options cache-embedded-users-max-count or cache-embedded-realms-max-count", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 12, "y": 176 }, "id": 99, "options": { "legend": { "calcs": [ "max", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(irate(vendor_statistics_evictions{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Number of evictions in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 191 }, "id": 66, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(irate(vendor_statistics_store_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Number of write operations in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 6, "y": 191 }, "id": 97, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "avg by (pod)((\nirate(vendor_statistics_hit_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))\n/\n(\n irate(vendor_statistics_hit_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m])\n + \n irate(vendor_statistics_remove_hit_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]) + irate(vendor_statistics_remove_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]) \n + \n irate(vendor_statistics_store_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m])\n))", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Read/Write ratio", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 12, "y": 191 }, "id": 91, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.7", "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "avg by (pod)(\n rate(\n vendor_statistics_store_times_seconds_sum{\n container=\"keycloak\", \n namespace=\"$namespace\",\n cache=\"$jdbc_cache_names\"}\n [$__range] \n )\n)\n/\nsum by(pod)(\n rate(\n vendor_statistics_store_times_seconds_count{\n container=\"keycloak\",\n namespace=\"$namespace\",\n cache=\"$jdbc_cache_names\"}\n [$__range] \n )\n)", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Average write operation time", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "A read operation reads a value from the cache. It divides into two groups, a hit if a value is found, and a miss if not found.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 206 }, "id": 94, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (irate(vendor_statistics_hit_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Number of read operations in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 41, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 6, "y": 206 }, "id": 70, "options": { "legend": { "calcs": [ "lastNotNull", "min", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "avg by (pod)(irate(vendor_statistics_hit_times_seconds_count{container=\"keycloak\", cache=\"$jdbc_cache_names\", namespace=\"$namespace\"}[5m]) / (irate(vendor_statistics_hit_times_seconds_count{cache=\"$jdbc_cache_names\", namespace=\"$namespace\", container=\"keycloak\"}[5m]) + irate(vendor_statistics_miss_times_seconds_count{cache=\"$jdbc_cache_names\", namespace=\"$namespace\", container=\"keycloak\"}[5m])))", "hide": false, "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Cache read hit/miss ratio", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "A read operation reads a value from the cache. It divides into two groups, a hit if a value is found, and a miss if not found.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 12, "y": 206 }, "id": 53, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (irate(vendor_statistics_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Number of cache read misses in 5 minute interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "A remove operation removes a value from the cache. It divides in two groups, a hit if a value exists, and a miss if the value does not exist.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 0, "y": 221 }, "id": 95, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod)(irate(vendor_statistics_remove_hit_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]) + irate(vendor_statistics_remove_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))", "legendFormat": "__auto", "range": true, "refId": "A" } ], "title": "Number of remove operations in 5 minutes interval", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 41, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 6, "y": 221 }, "id": 92, "options": { "legend": { "calcs": [ "lastNotNull", "min", "mean" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "avg by (pod)(irate(vendor_statistics_remove_hit_times_seconds_count{container=\"keycloak\", cache=\"$jdbc_cache_names\", namespace=\"$namespace\"}[5m]) / (irate(vendor_statistics_remove_hit_times_seconds_count{cache=\"$jdbc_cache_names\", namespace=\"$namespace\", container=\"keycloak\"}[5m]) + irate(vendor_statistics_remove_miss_times_seconds_count{cache=\"$jdbc_cache_names\", namespace=\"$namespace\", container=\"keycloak\"}[5m])))", "hide": false, "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Cache remove hit/miss ratio", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "description": "A remove operation removes a value from the cache. It divides in two groups, a hit if a value exists, and a miss if the value does not exist.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 14, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" } ] } }, "overrides": [] }, "gridPos": { "h": 15, "w": 6, "x": 12, "y": 221 }, "id": 87, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "editorMode": "code", "expr": "sum by (pod) (irate(vendor_statistics_remove_miss_times_seconds_count{container=\"keycloak\", namespace=\"$namespace\", cache=\"$jdbc_cache_names\"}[5m]))", "legendFormat": "{{pod}}", "range": true, "refId": "A" } ], "title": "Number of cache remove misses in 5 minutes interval", "type": "timeseries" } ], "repeat": "jdbc_cache_names", "repeatDirection": "h", "title": "JDBC caching - $jdbc_cache_names", "type": "row" } ], "refresh": "", "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [], "templating": { "list": [ { "current": {}, "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "definition": "label_values(namespace)", "hide": 0, "includeAll": false, "multi": false, "name": "namespace", "options": [], "query": { "query": "label_values(namespace)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query" }, { "allValue": "all", "current": { "selected": true, "text": [ "All" ], "value": [ "$__all" ] }, "hide": 0, "includeAll": true, "multi": true, "name": "jdbc_cache_names", "options": [ { "selected": true, "text": "All", "value": "$__all" }, { "selected": false, "text": "realms", "value": "realms" }, { "selected": false, "text": "users", "value": "users" }, { "selected": false, "text": "keys", "value": "keys" }, { "selected": false, "text": "authorization", "value": "authorization" } ], "query": "realms,users,keys,authorization", "queryValue": "", "skipUrlSync": false, "type": "custom" } ] }, "time": { "from": "now-1h", "to": "now" }, "timepicker": {}, "timezone": "", "title": "Keycloak troubleshooting dashboard", "uid": "Mh1Ly1ZNz", "version": 1, "weekStart": "" } ================================================ FILE: config/stage/dev/grafana/provisioning/datasources/datasources.yml ================================================ # config file version apiVersion: 1 # list of datasources that should be deleted from the database deleteDatasources: - name: Prometheus orgId: 1 # list of datasources to insert/update depending # whats available in the database datasources: # name of the datasource. Required - name: Prometheus # datasource type. Required type: prometheus # access mode. direct or proxy. Required access: proxy # org id. will default to orgId 1 if not specified orgId: 1 # url url: http://acme-prometheus:9090 # database password, if used password: # database user, if used user: # database name, if used database: # enable/disable basic auth basicAuth: false # basic auth username, if used basicAuthUser: # basic auth password, if used basicAuthPassword: # enable/disable with credentials headers withCredentials: # mark as default datasource. Max one per org isDefault: true # fields that will be converted to json and stored in json_data jsonData: graphiteVersion: "1.1" tlsAuth: false tlsAuthWithCACert: false # json object of data that will be encrypted. # secureJsonData: # tlsCACert: "..." # tlsClientCert: "..." # tlsClientKey: "..." version: 1 # allow users to edit datasources from the UI. editable: true ================================================ FILE: config/stage/dev/opa/iam/authzen/interop/access/policy.rego ================================================ package iam.authzen.interop.access import rego.v1 default decision := { "decision": false, "context": {"record": []}, } decision := result if { ids := [r.id | r := data.iam.authzen.interop.records[_]; can_access(r)] result := { "decision": count(ids) > 0, "context": {"record": ids}, } } can_access(r) if { r.owner == input.subject.id } ================================================ FILE: config/stage/dev/opa/iam/authzen/interop/access/v1/search/policy.rego ================================================ package iam.authzen.interop.access.v1.search import rego.v1 default resource := { "results": [] } resource := result if { result := { "results": [{"id":r.id, "type": "record"} | r := data.iam.authzen.interop.records[_]; can_access(r)] } } can_access(r) if { r.owner == input.subject.id } ================================================ FILE: config/stage/dev/opa/iam/authzen/interop/data.json ================================================ { "records": [ { "id": "101", "owner": "alice" }, { "id": "102", "owner": "bob" }, { "id": "103", "owner": "carol" }, { "id": "104", "owner": "dan" }, { "id": "105", "owner": "erin" }, { "id": "106", "owner": "felix" }, { "id": "107", "owner": "alice" }, { "id": "108", "owner": "bob" }, { "id": "109", "owner": "carol" }, { "id": "110", "owner": "dan" }, { "id": "111", "owner": "erin" }, { "id": "112", "owner": "felix" }, { "id": "113", "owner": "alice" }, { "id": "114", "owner": "bob" }, { "id": "115", "owner": "carol" }, { "id": "116", "owner": "dan" }, { "id": "117", "owner": "erin" }, { "id": "118", "owner": "felix" }, { "id": "119", "owner": "alice" }, { "id": "120", "owner": "bob" } ] } ================================================ FILE: config/stage/dev/opa/iam/keycloak/policy.rego ================================================ package iam.keycloak import rego.v1 # Map required client roles to client_id required_roles := {"app-minispa": "acme-user"} # "app-minispa": "acme-developer", # "app-keycloak-website": "acme-developer" default allow := { "decision": false, "context": { "message": "access-denied" } } # Users from acme-internal realm need the required roles to access allow := result if { is_realm("acme-internal") has_required_role_for_client(input.resource.properties.clientId) result = _allow(true, "acme-user can access") } # Users from other realms can access allow := result if { not is_realm("acme-internal") result = _allow(true, "every user can access") } # Helper function to return access decision with explanation _allow(allow, hint) := result if { result = { "decision": allow, "context": { "message": hint } } } is_realm(realm_name) := result if { result := input.resource.id == realm_name } has_required_role_for_client(client_id) := result if { # if no explicit required_role is configured just use one of the existing realm roles required_role := object.get(required_roles, client_id, input.subject.properties.realmRoles[0]) # check if user contains required role result = required_role in input.subject.properties.realmRoles } ================================================ FILE: config/stage/dev/opa/inputs/input.json ================================================ { "input" : { "subject" : { "id" : "4c3e26e3-77f4-4228-a20d-9383151b7224", "username" : "tester", "realmRoles" : [ "default-roles-opademo", "offline_access", "uma_authorization", "user" ], "clientRoles" : [ "app1:access", "account:view-profile", "account:manage-account", "account:manage-account-links" ], "attributes" : { "emailVerified" : false, "email" : "tester@local.test" } }, "resource" : { "realm" : "opademo", "clientId" : "app1" }, "context" : { "attributes" : { "remoteAddress" : "0:0:0:0:0:0:0:1" } }, "action" : "access" } } ================================================ FILE: config/stage/dev/opa/opa.md ================================================ OPA IAM Access Policies ---- # Setup Example Auth-Flow ![img.png](docs/auth-flow.png) Example Authenticator Config ![img.png](docs/auth-config.png) OPA URL: `http://localhost:8181/v1/policies/iam/keycloak/allow` Attributes: `acme_greeting` # Deploy policies ## Manually deploy policies ``` curl -v -X PUT --data-binary @config/stage/dev/opa/iam/keycloak/policy.rego localhost:18181/v1/policies/iam/keycloak ``` # Examples ![img.png](docs/auth-opa-output.png) ## Manually evaluate policy Access denied due to missing `acme-user` role. ``` curl -v POST -H "content-type: application/json" -d '{"input":{"subject":{"username":"tester"}}}' 127.0.0.1:18181/v1/data/iam/keycloak/allow ``` Access granted ``` curl -v POST -H "content-type: application/json" -d '{"input":{"subject":{"username":"tester", "realmRoles":["acme-user"]}}}' 127.0.0.1:18181/v1/data/iam/keycloak/allow ``` ================================================ FILE: config/stage/dev/opa/policies/keycloak/realms/opademo/access/policy.rego ================================================ package keycloak.realms.opademo.access import future.keywords.if import future.keywords.in import data.keycloak.utils.kc # default allow rule: deny all default allow := false # allow acess to client-id:account-console if realm-role:user allow if { kc.isClient("account-console") kc.hasRealmRole("user") } # allow acess to client-id:app1 if client-role:access allow if { kc.isClient("app1") kc.hasCurrentClientRole("access") } # allow acess to client-id:app2 if client-role:access allow if { kc.isClient("app2") kc.hasClientRole("app2", "access") } # allow acess to client-id:app3 if member of group allow if { kc.isClient("app3") kc.isGroupMember("mygroup") } # allow acess to "special clients" if member of group allow if { is_special_client(input.resource.clientId) kc.isGroupMember("foobargroup") } is_special_client(clientId) if startswith(clientId, "foo-") is_special_client(clientId) if startswith(clientId, "bar-") # https://www.styra.com/blog/how-to-express-or-in-rego/ ================================================ FILE: config/stage/dev/opa/policies/keycloak/realms/opademo/access/policy_test.rego ================================================ package keycloak.realms.opademo.access #https://www.openpolicyagent.org/docs/latest/policy-testing/ import future.keywords.if import future.keywords.in import data.keycloak.utils.kc test_access_account_console if { allow with input as { "subject": { "id": "c9d683de-4987-4e90-801e-81c6ac411d80", "username": "tester", "realmRoles": [ "default-roles-opademo", "offline_access", "uma_authorization", "user" ], "clientRoles": [ "account:view-profile", "account:manage-account", "account:manage-account-links" ], "attributes": { "emailVerified": true, "email": "tester@local.de" } }, "resource": { "realm": "opademo", "clientId": "account-console" }, "context": { "attributes": { "remoteAddress": "0:0:0:0:0:0:0:1" } }, "action": "access" } } test_access_app1 if { allow with input as { "subject": { "id": "c9d683de-4987-4e90-801e-81c6ac411d80", "username": "tester", "realmRoles": [ "default-roles-opademo", "offline_access", "uma_authorization", "user" ], "clientRoles": [ "account:view-profile", "account:manage-account", "account:manage-account-links", "app1:access" ], "attributes": { "emailVerified": true, "email": "tester@local.de" } }, "resource": { "realm": "opademo", "clientId": "app1" }, "context": { "attributes": { "remoteAddress": "0:0:0:0:0:0:0:1" } }, "action": "access" } } ================================================ FILE: config/stage/dev/opa/policies/keycloak/realms/opademo2/access/policy.rego ================================================ package keycloak.realms.opademo2.access import future.keywords.if import future.keywords.in import data.keycloak.utils.kc # default rule "allow" default allow := false # rule "allow" for client-id:account-console allow if { kc.isClient("account-console") } ================================================ FILE: config/stage/dev/opa/policies/keycloak/utils/kc/helpers.rego ================================================ package keycloak.utils.kc import future.keywords.if import future.keywords.in isRealm(realmName) if input.resource.realm == realmName isClient(clientId) if input.resource.clientId == clientId hasRealmRole(roleName) if roleName in input.subject.realmRoles hasClientRole(clientId, roleName) := result if { client_role := concat(":", [clientId, roleName]) result := client_role in input.subject.clientRoles } hasCurrentClientRole(roleName) := result if { client_role := concat(":", [input.resource.clientId, roleName]) result := client_role in input.subject.clientRoles } hasUserAttribute(attribute) if input.subject.attributes[attribute] hasUserAttributeValue(attribute, value) if input.subject.attributes[attribute] == value isGroupMember(group) if group in input.subject.groups ================================================ FILE: config/stage/dev/opa/watch-opa.sh ================================================ #!/usr/bin/env bash lint_rego() { docker run --rm -v $PWD/iam:/opa/iam:z openpolicyagent/opa:0.44.0-envoy-static check --bundle /opa/iam } update_opa() { curl -s -o /dev/null -X PUT --data-binary @iam/keycloak/policy.rego localhost:18181/v1/policies/iam/keycloak } publish_rego() { lint_rego && update_opa && echo "$(date +"%m-%d-%Y %T") OPA updated." } echo "Watching for changes in OPA policy files" inotifywait --monitor --event close_write --recursive $PWD/iam | while read do publish_rego done ================================================ FILE: config/stage/dev/openldap/demo.ldif ================================================ version: 1 # Add Keycloak bind user dn: cn=keycloak,dc=corp,dc=acme,dc=local changetype: add objectClass: person objectClass: top cn: keycloak sn: keycloak userPassword:: e1NTSEF9MW1pSWwrTUROUGlvMExTRUZPOGloejk1eldpTTN3ZGRIZDV6Z2c9P Q== # Add custom ACL for keycloak bind user dn: olcDatabase={1}mdb,cn=config changetype: modify add: olcAccess olcAccess: {2}to * by self read by dn="cn=admin,dc=corp,dc=acme,dc=local" by dn="cn=keycloak,dc=corp,dc=acme,dc=local" write by * none dn: ou=Accounting,dc=corp,dc=acme,dc=local changetype: add ou: Accounting objectClass: top objectClass: organizationalUnit dn: ou=Product Development,dc=corp,dc=acme,dc=local changetype: add ou: Product Development objectClass: top objectClass: organizationalUnit dn: ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add ou: Product Testing objectClass: top objectClass: organizationalUnit dn: ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add ou: Human Resources objectClass: top objectClass: organizationalUnit dn: ou=Payroll,dc=corp,dc=acme,dc=local changetype: add ou: Payroll objectClass: top objectClass: organizationalUnit dn: ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add ou: Janitorial objectClass: top objectClass: organizationalUnit dn: ou=Management,dc=corp,dc=acme,dc=local changetype: add ou: Management objectClass: top objectClass: organizationalUnit dn: ou=Administrative,dc=corp,dc=acme,dc=local changetype: add ou: Administrative objectClass: top objectClass: organizationalUnit dn: ou=Peons,dc=corp,dc=acme,dc=local changetype: add ou: Peons objectClass: top objectClass: organizationalUnit dn: ou=Planning,dc=corp,dc=acme,dc=local changetype: add ou: Planning objectClass: top objectClass: organizationalUnit dn: cn=Walter Laurentius,ou=Planning,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Walter Laurentius sn: Laurentius description: This is Walter Laurentius's description facsimileTelephoneNumber: +1 510 315-3162 l: Sunnyvale ou: Planning postalAddress: Accounting$Sunnyvale telephoneNumber: +1 510 408-1538 title: Head of Planning userPassword: Password1 uid: LaurentiusW givenName: Walter mail: LaurentiusW@ns-mail8.com carLicense: BDGU314 departmentNumber: 1224 employeeType: Manager homePhone: +1 510 464-1671 initials: A. L. mobile: +1 510 180-1671 pager: +1 510 699-1671 roomNumber: 7051 dn: cn=Anne Laurentius,ou=Accounting,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Anne Laurentius sn: Laurentius description: This is Anne Laurentius's description facsimileTelephoneNumber: +1 510 315-3162 l: Sunnyvale ou: Accounting postalAddress: Accounting$Sunnyvale telephoneNumber: +1 510 408-1538 title: Head of Accounting userPassword: Password1 uid: LaurentiusA givenName: Anne mail: LaurentiusA@ns-mail8.com carLicense: BDGU314 departmentNumber: 1224 employeeType: Manager homePhone: +1 510 464-1670 initials: A. L. mobile: +1 510 180-1670 pager: +1 510 699-1670 roomNumber: 7050 dn: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Redgie Fleugel sn: Fleugel description: This is Redgie Fleugel's description facsimileTelephoneNumber: +1 510 315-3162 l: Sunnyvale ou: Janitorial postalAddress: Janitorial$Sunnyvale telephoneNumber: +1 510 408-1538 title: Chief Janitorial Admin userPassword: Password1 uid: FleugelR givenName: Redgie mail: FleugelR@ns-mail8.com carLicense: BDGU31 departmentNumber: 9222 employeeType: Employee homePhone: +1 510 464-1671 initials: R. F. mobile: +1 510 180-1685 pager: +1 510 699-9122 roomNumber: 8640 dn: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Anica Kakuta sn: Kakuta description: This is Anica Kakuta's description facsimileTelephoneNumber: +1 818 449-2614 l: Armonk ou: Janitorial postalAddress: Janitorial$Armonk telephoneNumber: +1 818 748-7509 title: Master Janitorial Mascot userPassword: Password1 uid: KakutaA givenName: Anica mail: KakutaA@ns-mail4.com carLicense: VU6HIV departmentNumber: 3490 employeeType: Employee homePhone: +1 818 483-2264 initials: A. K. mobile: +1 818 787-1089 pager: +1 818 972-7280 roomNumber: 8483 manager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local dn: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Evvy Gattrell sn: Gattrell description: This is Evvy Gattrell's description facsimileTelephoneNumber: +1 818 219-8955 l: Palo Alto ou: Management postalAddress: Management$Palo Alto telephoneNumber: +1 818 156-4431 title: Associate Management Director userPassword: Password1 uid: GattrelE givenName: Evvy mail: GattrelE@ns-mail6.com carLicense: 39WWKF departmentNumber: 6424 employeeType: Employee homePhone: +1 818 284-8958 initials: E. G. mobile: +1 818 531-5583 pager: +1 818 813-3201 roomNumber: 9817 secretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local manager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local dn: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Tilly Pilote sn: Pilote description: This is Tilly Pilote's description facsimileTelephoneNumber: +1 804 640-1719 l: San Mateo ou: Peons postalAddress: Peons$San Mateo telephoneNumber: +1 804 399-6208 title: Associate Peons Admin userPassword: Password1 uid: PiloteT givenName: Tilly mail: PiloteT@ns-mail9.com carLicense: PPRWB5 departmentNumber: 7000 employeeType: Contract homePhone: +1 804 515-6885 initials: T. P. mobile: +1 804 112-4703 pager: +1 804 758-8153 roomNumber: 9573 secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local manager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local dn: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Nashir Shiffer sn: Shiffer description: This is Nashir Shiffer's description facsimileTelephoneNumber: +1 804 395-7794 l: Armonk ou: Management postalAddress: Management$Armonk telephoneNumber: +1 804 654-9736 title: Associate Management Warrior userPassword: Password1 uid: ShifferN givenName: Nashir mail: ShifferN@ns-mail7.com carLicense: D1MVLI departmentNumber: 1436 employeeType: Contract homePhone: +1 804 994-7078 initials: N. S. mobile: +1 804 897-9748 pager: +1 804 111-1942 roomNumber: 8672 secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local dn: cn=Joy Hilton,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Joy Hilton sn: Hilton description: This is Joy Hilton's description facsimileTelephoneNumber: +1 415 295-3430 l: San Mateo ou: Peons postalAddress: Peons$San Mateo telephoneNumber: +1 415 791-7686 title: Associate Peons President userPassword: Password1 uid: HiltonJ givenName: Joy mail: HiltonJ@ns-mail5.com carLicense: YV34JP departmentNumber: 7074 employeeType: Employee homePhone: +1 415 858-4150 initials: J. H. mobile: +1 415 695-7685 pager: +1 415 309-8363 roomNumber: 9755 secretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Christel Basmadjian,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Christel Basmadjian sn: Basmadjian description: This is Christel Basmadjian's description facsimileTelephoneNumber: +1 415 556-4046 l: Palo Alto ou: Product Testing postalAddress: Product Testing$Palo Alto telephoneNumber: +1 415 279-4634 title: Master Product Testing Visionary userPassword: Password1 uid: BasmadjC givenName: Christel mail: BasmadjC@ns-mail8.com carLicense: EYSLAE departmentNumber: 5127 employeeType: Normal homePhone: +1 415 198-1452 initials: C. B. mobile: +1 415 424-9664 pager: +1 415 186-2950 roomNumber: 9934 secretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Roselin Charney,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Roselin Charney sn: Charney description: This is Roselin Charney's description facsimileTelephoneNumber: +1 415 174-5083 l: Cupertino ou: Payroll postalAddress: Payroll$Cupertino telephoneNumber: +1 415 306-9466 title: Supreme Payroll Warrior userPassword: Password1 uid: CharneyR givenName: Roselin mail: CharneyR@ns-mail8.com carLicense: BTH7XQ departmentNumber: 8954 employeeType: Contract homePhone: +1 415 990-7114 initials: R. C. mobile: +1 415 276-4723 pager: +1 415 592-8003 roomNumber: 8791 manager: cn=Redgie Fleugel,ou=Janitorial,dc=corp,dc=acme,dc=local dn: cn=Lorita Bittenbender,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Lorita Bittenbender sn: Bittenbender description: This is Lorita Bittenbender's description facsimileTelephoneNumber: +1 510 750-5365 l: San Mateo ou: Payroll postalAddress: Payroll$San Mateo telephoneNumber: +1 510 101-8577 title: Junior Payroll Director userPassword: Password1 uid: BittenbL givenName: Lorita mail: BittenbL@ns-mail9.com carLicense: MR2Q3H departmentNumber: 8654 employeeType: Normal homePhone: +1 510 791-2621 initials: L. B. mobile: +1 510 802-3579 pager: +1 510 485-1258 roomNumber: 9594 secretary: cn=Anica Kakuta,ou=Janitorial,dc=corp,dc=acme,dc=local manager: cn=Roselin Charney,ou=Payroll,dc=corp,dc=acme,dc=local dn: cn=Kimihiko Fujiwara,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Kimihiko Fujiwara sn: Fujiwara description: This is Kimihiko Fujiwara's description facsimileTelephoneNumber: +1 804 721-1544 l: Fremont ou: Administrative postalAddress: Administrative$Fremont telephoneNumber: +1 804 802-2024 title: Associate Administrative Stooge userPassword: Password1 uid: FujiwarK givenName: Kimihiko mail: FujiwarK@ns-mail5.com carLicense: 9X8M99 departmentNumber: 8753 employeeType: Employee homePhone: +1 804 593-7870 initials: K. F. mobile: +1 804 556-6289 pager: +1 804 172-2546 roomNumber: 9634 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Pas Linder,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Pas Linder sn: Linder description: This is Pas Linder's description facsimileTelephoneNumber: +1 804 634-3333 l: San Francisco ou: Janitorial postalAddress: Janitorial$San Francisco telephoneNumber: +1 804 213-3375 title: Master Janitorial Consultant userPassword: Password1 uid: LinderP givenName: Pas mail: LinderP@ns-mail9.com carLicense: HVHA6S departmentNumber: 2743 employeeType: Contract homePhone: +1 804 384-9662 initials: P. L. mobile: +1 804 102-5670 pager: +1 804 197-1463 roomNumber: 8958 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Helge Blumenfeld,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Helge Blumenfeld sn: Blumenfeld description: This is Helge Blumenfeld's description facsimileTelephoneNumber: +1 804 169-5952 l: San Francisco ou: Product Testing postalAddress: Product Testing$San Francisco telephoneNumber: +1 804 351-9054 title: Associate Product Testing Stooge userPassword: Password1 uid: BlumenfH givenName: Helge mail: BlumenfH@ns-mail9.com carLicense: IOP72I departmentNumber: 5084 employeeType: Employee homePhone: +1 804 160-2700 initials: H. B. mobile: +1 804 532-3761 pager: +1 804 246-6956 roomNumber: 8401 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Joete Lough,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Joete Lough sn: Lough description: This is Joete Lough's description facsimileTelephoneNumber: +1 510 916-2579 l: Milpitas ou: Payroll postalAddress: Payroll$Milpitas telephoneNumber: +1 510 225-4178 title: Chief Payroll Director userPassword: Password1 uid: LoughJ givenName: Joete mail: LoughJ@ns-mail4.com carLicense: 7X8DSV departmentNumber: 5497 employeeType: Employee homePhone: +1 510 227-4121 initials: J. L. mobile: +1 510 269-9216 pager: +1 510 155-8420 roomNumber: 8072 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Pak-Jong Marouchos,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Pak-Jong Marouchos sn: Marouchos description: This is Pak-Jong Marouchos's description facsimileTelephoneNumber: +1 408 634-3839 l: Armonk ou: Administrative postalAddress: Administrative$Armonk telephoneNumber: +1 408 515-7189 title: Master Administrative Artist userPassword: Password1 uid: MarouchP givenName: Pak-Jong mail: MarouchP@ns-mail5.com carLicense: WTDP4S departmentNumber: 8014 employeeType: Employee homePhone: +1 408 781-3799 initials: P. M. mobile: +1 408 616-6286 pager: +1 408 124-2046 roomNumber: 9840 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Dat Mejia,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Dat Mejia sn: Mejia description: This is Dat Mejia's description facsimileTelephoneNumber: +1 804 240-2269 l: Santa Clara ou: Human Resources postalAddress: Human Resources$Santa Clara telephoneNumber: +1 804 418-3946 title: Supreme Human Resources Architect userPassword: Password1 uid: MejiaD givenName: Dat mail: MejiaD@ns-mail2.com carLicense: UDR1CH departmentNumber: 8839 employeeType: Contract homePhone: +1 804 501-1284 initials: D. M. mobile: +1 804 940-7166 pager: +1 804 777-9199 roomNumber: 9353 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Goldy Jankowski,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Goldy Jankowski sn: Jankowski description: This is Goldy Jankowski's description facsimileTelephoneNumber: +1 415 686-7962 l: Palo Alto ou: Janitorial postalAddress: Janitorial$Palo Alto telephoneNumber: +1 415 543-8028 title: Chief Janitorial Warrior userPassword: Password1 uid: JankowsG givenName: Goldy mail: JankowsG@ns-mail3.com carLicense: CLM0YH departmentNumber: 4684 employeeType: Employee homePhone: +1 415 764-6176 initials: G. J. mobile: +1 415 621-7063 pager: +1 415 411-2427 roomNumber: 9062 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Hiroki Morelli,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Hiroki Morelli sn: Morelli description: This is Hiroki Morelli's description facsimileTelephoneNumber: +1 213 217-2091 l: Cambridge ou: Human Resources postalAddress: Human Resources$Cambridge telephoneNumber: +1 213 182-4690 title: Master Human Resources Engineer userPassword: Password1 uid: MorelliH givenName: Hiroki mail: MorelliH@ns-mail7.com carLicense: KCVRCT departmentNumber: 4254 employeeType: Normal homePhone: +1 213 211-7773 initials: H. M. mobile: +1 213 408-9383 pager: +1 213 467-7822 roomNumber: 8730 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Hadria Adam,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Hadria Adam sn: Adam description: This is Hadria Adam's description facsimileTelephoneNumber: +1 804 832-1956 l: Cupertino ou: Management postalAddress: Management$Cupertino telephoneNumber: +1 804 954-5484 title: Chief Management Mascot userPassword: Password1 uid: AdamH givenName: Hadria mail: AdamH@ns-mail9.com carLicense: UE3K6W departmentNumber: 8391 employeeType: Employee homePhone: +1 804 362-3145 initials: H. A. mobile: +1 804 851-4844 pager: +1 804 633-9866 roomNumber: 9008 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Loella Tibi,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Loella Tibi sn: Tibi description: This is Loella Tibi's description facsimileTelephoneNumber: +1 804 993-8020 l: Orem ou: Janitorial postalAddress: Janitorial$Orem telephoneNumber: +1 804 370-7549 title: Associate Janitorial Sales Rep userPassword: Password1 uid: TibiL givenName: Loella mail: TibiL@ns-mail5.com carLicense: A4B7L4 departmentNumber: 8654 employeeType: Employee homePhone: +1 804 820-5702 initials: L. T. mobile: +1 804 780-6560 pager: +1 804 885-3920 roomNumber: 8674 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Sharri McElligott,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Sharri McElligott sn: McElligott description: This is Sharri McElligott's description facsimileTelephoneNumber: +1 206 824-3652 l: Palo Alto ou: Management postalAddress: Management$Palo Alto telephoneNumber: +1 206 552-6740 title: Associate Management Figurehead userPassword: Password1 uid: McElligS givenName: Sharri mail: McElligS@ns-mail8.com carLicense: VVG7V6 departmentNumber: 6125 employeeType: Normal homePhone: +1 206 922-3353 initials: S. M. mobile: +1 206 298-4265 pager: +1 206 629-7792 roomNumber: 8106 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Yovonnda Van Veen,ou=Product Development,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Yovonnda Van Veen sn: Van Veen description: This is Yovonnda Van Veen's description facsimileTelephoneNumber: +1 408 774-9961 l: Cambridge ou: Product Development postalAddress: Product Development$Cambridge telephoneNumber: +1 408 170-9652 title: Supreme Product Development Admin userPassword: Password1 uid: Van VeeY givenName: Yovonnda mail: Van VeeY@ns-mail2.com carLicense: IDQN19 departmentNumber: 3630 employeeType: Normal homePhone: +1 408 700-8967 initials: Y. V. mobile: +1 408 846-3931 pager: +1 408 645-3212 roomNumber: 9081 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Aura Quinn,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Aura Quinn sn: Quinn description: This is Aura Quinn's description facsimileTelephoneNumber: +1 206 116-6507 l: Redmond ou: Janitorial postalAddress: Janitorial$Redmond telephoneNumber: +1 206 821-1850 title: Junior Janitorial Manager userPassword: Password1 uid: QuinnA givenName: Aura mail: QuinnA@ns-mail4.com carLicense: MVTGED departmentNumber: 2308 employeeType: Normal homePhone: +1 206 154-5641 initials: A. Q. mobile: +1 206 747-4585 pager: +1 206 933-5036 roomNumber: 9706 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Faina Michalos,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Faina Michalos sn: Michalos description: This is Faina Michalos's description facsimileTelephoneNumber: +1 206 824-7047 l: Redmond ou: Management postalAddress: Management$Redmond telephoneNumber: +1 206 617-6467 title: Supreme Management Stooge userPassword: Password1 uid: MichaloF givenName: Faina mail: MichaloF@ns-mail8.com carLicense: N9NUVE departmentNumber: 5205 employeeType: Contract homePhone: +1 206 747-8261 initials: F. M. mobile: +1 206 370-6675 pager: +1 206 337-9183 roomNumber: 9679 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Weitzel Piasecki,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Weitzel Piasecki sn: Piasecki description: This is Weitzel Piasecki's description facsimileTelephoneNumber: +1 415 369-9640 l: Redwood Shores ou: Product Testing postalAddress: Product Testing$Redwood Shores telephoneNumber: +1 415 227-6210 title: Supreme Product Testing Czar userPassword: Password1 uid: PiaseckW givenName: Weitzel mail: PiaseckW@ns-mail7.com carLicense: EK7JWG departmentNumber: 5750 employeeType: Contract homePhone: +1 415 631-4536 initials: W. P. mobile: +1 415 969-6594 pager: +1 415 324-9946 roomNumber: 9567 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Roseanna Goh,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Roseanna Goh sn: Goh description: This is Roseanna Goh's description facsimileTelephoneNumber: +1 415 451-4260 l: Redmond ou: Human Resources postalAddress: Human Resources$Redmond telephoneNumber: +1 415 353-8360 title: Master Human Resources Punk userPassword: Password1 uid: GohR givenName: Roseanna mail: GohR@ns-mail3.com carLicense: VQYVKH departmentNumber: 8527 employeeType: Normal homePhone: +1 415 970-6225 initials: R. G. mobile: +1 415 649-3386 pager: +1 415 997-5556 roomNumber: 9852 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Jenifer Wortman,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Jenifer Wortman sn: Wortman description: This is Jenifer Wortman's description facsimileTelephoneNumber: +1 510 219-4967 l: San Jose ou: Payroll postalAddress: Payroll$San Jose telephoneNumber: +1 510 616-5746 title: Junior Payroll Engineer userPassword: Password1 uid: WortmanJ givenName: Jenifer mail: WortmanJ@ns-mail3.com carLicense: TUQTBU departmentNumber: 6294 employeeType: Normal homePhone: +1 510 584-1622 initials: J. W. mobile: +1 510 235-2091 pager: +1 510 202-3776 roomNumber: 9299 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Tania Everitt,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Tania Everitt sn: Everitt description: This is Tania Everitt's description facsimileTelephoneNumber: +1 818 126-1680 l: Cambridge ou: Management postalAddress: Management$Cambridge telephoneNumber: +1 818 969-1227 title: Master Management Vice President userPassword: Password1 uid: EverittT givenName: Tania mail: EverittT@ns-mail5.com carLicense: MEA23G departmentNumber: 8855 employeeType: Employee homePhone: +1 818 835-6339 initials: T. E. mobile: +1 818 568-6942 pager: +1 818 132-5780 roomNumber: 9993 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Arlina Erkel,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Arlina Erkel sn: Erkel description: This is Arlina Erkel's description facsimileTelephoneNumber: +1 818 969-5292 l: Redwood Shores ou: Peons postalAddress: Peons$Redwood Shores telephoneNumber: +1 818 833-2593 title: Junior Peons Warrior userPassword: Password1 uid: ErkelA givenName: Arlina mail: ErkelA@ns-mail3.com carLicense: XTF35K departmentNumber: 9345 employeeType: Contract homePhone: +1 818 103-1672 initials: A. E. mobile: +1 818 263-4930 pager: +1 818 558-8339 roomNumber: 9679 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Yvet ENG,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Yvet ENG sn: ENG description: This is Yvet ENG's description facsimileTelephoneNumber: +1 415 669-5280 l: Fremont ou: Product Testing postalAddress: Product Testing$Fremont telephoneNumber: +1 415 389-8020 title: Master Product Testing Czar userPassword: Password1 uid: ENGY givenName: Yvet mail: ENGY@ns-mail6.com carLicense: OLCDLR departmentNumber: 7534 employeeType: Normal homePhone: +1 415 519-1854 initials: Y. E. mobile: +1 415 773-2475 pager: +1 415 164-6343 roomNumber: 8398 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Sherrye Buttrey,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Sherrye Buttrey sn: Buttrey description: This is Sherrye Buttrey's description facsimileTelephoneNumber: +1 415 754-6746 l: Sunnyvale ou: Janitorial postalAddress: Janitorial$Sunnyvale telephoneNumber: +1 415 190-2648 title: Associate Janitorial Consultant userPassword: Password1 uid: ButtreyS givenName: Sherrye mail: ButtreyS@ns-mail9.com carLicense: QCVKJ2 departmentNumber: 7404 employeeType: Employee homePhone: +1 415 651-1536 initials: S. B. mobile: +1 415 304-1250 pager: +1 415 447-8041 roomNumber: 9797 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Alida Nahmias,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Alida Nahmias sn: Nahmias description: This is Alida Nahmias's description facsimileTelephoneNumber: +1 415 492-1662 l: Redwood Shores ou: Janitorial postalAddress: Janitorial$Redwood Shores telephoneNumber: +1 415 377-7650 title: Chief Janitorial Stooge userPassword: Password1 uid: NahmiasA givenName: Alida mail: NahmiasA@ns-mail3.com carLicense: GW837I departmentNumber: 4549 employeeType: Contract homePhone: +1 415 598-7345 initials: A. N. mobile: +1 415 244-3718 pager: +1 415 929-7865 roomNumber: 9919 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Jerrilyn Ruddle,ou=Product Development,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Jerrilyn Ruddle sn: Ruddle description: This is Jerrilyn Ruddle's description facsimileTelephoneNumber: +1 804 830-8298 l: Redwood Shores ou: Product Development postalAddress: Product Development$Redwood Shores telephoneNumber: +1 804 180-7934 title: Chief Product Development Madonna userPassword: Password1 uid: RuddleJ givenName: Jerrilyn mail: RuddleJ@ns-mail4.com carLicense: PHDM53 departmentNumber: 4570 employeeType: Contract homePhone: +1 804 474-7271 initials: J. R. mobile: +1 804 374-3735 pager: +1 804 386-8771 roomNumber: 8389 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Alev Boucouris,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Alev Boucouris sn: Boucouris description: This is Alev Boucouris's description facsimileTelephoneNumber: +1 408 146-7711 l: Cupertino ou: Management postalAddress: Management$Cupertino telephoneNumber: +1 408 759-2449 title: Junior Management Artist userPassword: Password1 uid: BoucourA givenName: Alev mail: BoucourA@ns-mail7.com carLicense: V8GELA departmentNumber: 2840 employeeType: Employee homePhone: +1 408 808-1395 initials: A. B. mobile: +1 408 308-6826 pager: +1 408 653-4290 roomNumber: 8194 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Hazem Hagan,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Hazem Hagan sn: Hagan description: This is Hazem Hagan's description facsimileTelephoneNumber: +1 818 360-8313 l: Cambridge ou: Management postalAddress: Management$Cambridge telephoneNumber: +1 818 255-1352 title: Associate Management Evangelist userPassword: Password1 uid: HaganH givenName: Hazem mail: HaganH@ns-mail8.com carLicense: BBN1Y6 departmentNumber: 8167 employeeType: Contract homePhone: +1 818 904-1676 initials: H. H. mobile: +1 818 264-6149 pager: +1 818 910-9009 roomNumber: 9320 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Romano IEM,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Romano IEM sn: IEM description: This is Romano IEM's description facsimileTelephoneNumber: +1 818 888-4612 l: Cambridge ou: Administrative postalAddress: Administrative$Cambridge telephoneNumber: +1 818 267-4274 title: Chief Administrative Technician userPassword: Password1 uid: IEMR givenName: Romano mail: IEMR@ns-mail3.com carLicense: QBXXAK departmentNumber: 1326 employeeType: Employee homePhone: +1 818 158-2704 initials: R. I. mobile: +1 818 477-8860 pager: +1 818 104-3298 roomNumber: 8784 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Floria Hoagland,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Floria Hoagland sn: Hoagland description: This is Floria Hoagland's description facsimileTelephoneNumber: +1 213 619-7948 l: San Jose ou: Product Testing postalAddress: Product Testing$San Jose telephoneNumber: +1 213 641-6481 title: Master Product Testing Madonna userPassword: Password1 uid: HoaglanF givenName: Floria mail: HoaglanF@ns-mail2.com carLicense: 63S8FA departmentNumber: 2429 employeeType: Normal homePhone: +1 213 754-1997 initials: F. H. mobile: +1 213 445-7805 pager: +1 213 586-2690 roomNumber: 8063 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Ray Preston,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Ray Preston sn: Preston description: This is Ray Preston's description facsimileTelephoneNumber: +1 213 838-9375 l: San Mateo ou: Product Testing postalAddress: Product Testing$San Mateo telephoneNumber: +1 213 125-3426 title: Supreme Product Testing Consultant userPassword: Password1 uid: PrestonR givenName: Ray mail: PrestonR@ns-mail3.com carLicense: RNFK4Y departmentNumber: 8204 employeeType: Employee homePhone: +1 213 446-4465 initials: R. P. mobile: +1 213 507-2929 pager: +1 213 373-6534 roomNumber: 8894 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Veronike Vodicka,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Veronike Vodicka sn: Vodicka description: This is Veronike Vodicka's description facsimileTelephoneNumber: +1 213 657-1189 l: Orem ou: Janitorial postalAddress: Janitorial$Orem telephoneNumber: +1 213 311-1925 title: Junior Janitorial Consultant userPassword: Password1 uid: VodickaV givenName: Veronike mail: VodickaV@ns-mail8.com carLicense: OFTAVH departmentNumber: 3879 employeeType: Contract homePhone: +1 213 516-5523 initials: V. V. mobile: +1 213 280-7666 pager: +1 213 976-8415 roomNumber: 8848 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Genovera Smulders,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Genovera Smulders sn: Smulders description: This is Genovera Smulders's description facsimileTelephoneNumber: +1 415 897-4784 l: Redmond ou: Payroll postalAddress: Payroll$Redmond telephoneNumber: +1 415 200-1690 title: Supreme Payroll Dictator userPassword: Password1 uid: SmulderG givenName: Genovera mail: SmulderG@ns-mail4.com carLicense: J1Q44M departmentNumber: 8659 employeeType: Employee homePhone: +1 415 400-1612 initials: G. S. mobile: +1 415 799-3144 pager: +1 415 910-6380 roomNumber: 9866 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Fritz Stachowiak,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Fritz Stachowiak sn: Stachowiak description: This is Fritz Stachowiak's description facsimileTelephoneNumber: +1 213 388-3488 l: Cambridge ou: Janitorial postalAddress: Janitorial$Cambridge telephoneNumber: +1 213 342-6961 title: Junior Janitorial Evangelist userPassword: Password1 uid: StachowF givenName: Fritz mail: StachowF@ns-mail7.com carLicense: QCL57L departmentNumber: 8997 employeeType: Normal homePhone: +1 213 409-7064 initials: F. S. mobile: +1 213 291-7561 pager: +1 213 395-9221 roomNumber: 8839 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Gordon Hitchcock,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Gordon Hitchcock sn: Hitchcock description: This is Gordon Hitchcock's description facsimileTelephoneNumber: +1 510 820-9805 l: Cupertino ou: Administrative postalAddress: Administrative$Cupertino telephoneNumber: +1 510 147-8695 title: Master Administrative Director userPassword: Password1 uid: HitchcoG givenName: Gordon mail: HitchcoG@ns-mail8.com carLicense: TXKDC4 departmentNumber: 9897 employeeType: Normal homePhone: +1 510 168-7227 initials: G. H. mobile: +1 510 546-4843 pager: +1 510 326-8101 roomNumber: 9854 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Piero MacLean,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Piero MacLean sn: MacLean description: This is Piero MacLean's description facsimileTelephoneNumber: +1 818 689-3101 l: Menlo Park ou: Janitorial postalAddress: Janitorial$Menlo Park telephoneNumber: +1 818 156-9471 title: Associate Janitorial Warrior userPassword: Password1 uid: MacLeanP givenName: Piero mail: MacLeanP@ns-mail8.com carLicense: 9SH30R departmentNumber: 7221 employeeType: Normal homePhone: +1 818 927-2826 initials: P. M. mobile: +1 818 702-4365 pager: +1 818 559-6404 roomNumber: 8204 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Kass Belcher,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Kass Belcher sn: Belcher description: This is Kass Belcher's description facsimileTelephoneNumber: +1 206 507-6876 l: Redwood Shores ou: Peons postalAddress: Peons$Redwood Shores telephoneNumber: +1 206 563-4349 title: Associate Peons President userPassword: Password1 uid: BelcherK givenName: Kass mail: BelcherK@ns-mail3.com carLicense: XJG74B departmentNumber: 3411 employeeType: Contract homePhone: +1 206 251-4208 initials: K. B. mobile: +1 206 517-9175 pager: +1 206 423-2595 roomNumber: 8891 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Geoff Petrick,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Geoff Petrick sn: Petrick description: This is Geoff Petrick's description facsimileTelephoneNumber: +1 206 931-4080 l: Menlo Park ou: Management postalAddress: Management$Menlo Park telephoneNumber: +1 206 408-8873 title: Junior Management Warrior userPassword: Password1 uid: PetrickG givenName: Geoff mail: PetrickG@ns-mail4.com carLicense: QRS2IV departmentNumber: 5946 employeeType: Employee homePhone: +1 206 606-3394 initials: G. P. mobile: +1 206 212-4321 pager: +1 206 520-1109 roomNumber: 8565 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Lynna Klebsch,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Lynna Klebsch sn: Klebsch description: This is Lynna Klebsch's description facsimileTelephoneNumber: +1 804 708-1163 l: San Jose ou: Human Resources postalAddress: Human Resources$San Jose telephoneNumber: +1 804 848-5944 title: Junior Human Resources Engineer userPassword: Password1 uid: KlebschL givenName: Lynna mail: KlebschL@ns-mail2.com carLicense: JPRMHP departmentNumber: 5330 employeeType: Employee homePhone: +1 804 885-1229 initials: L. K. mobile: +1 804 190-7759 pager: +1 804 825-4457 roomNumber: 8905 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Sibley Frederick,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Sibley Frederick sn: Frederick description: This is Sibley Frederick's description facsimileTelephoneNumber: +1 818 844-7037 l: San Mateo ou: Janitorial postalAddress: Janitorial$San Mateo telephoneNumber: +1 818 651-3340 title: Associate Janitorial Warrior userPassword: Password1 uid: FrederiS givenName: Sibley mail: FrederiS@ns-mail2.com carLicense: SC6Q2I departmentNumber: 4175 employeeType: Employee homePhone: +1 818 645-2860 initials: S. F. mobile: +1 818 684-3673 pager: +1 818 846-2402 roomNumber: 8013 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Coord Reddington,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Coord Reddington sn: Reddington description: This is Coord Reddington's description facsimileTelephoneNumber: +1 408 655-4629 l: Sunnyvale ou: Product Testing postalAddress: Product Testing$Sunnyvale telephoneNumber: +1 408 820-2555 title: Master Product Testing President userPassword: Password1 uid: ReddingC givenName: Coord mail: ReddingC@ns-mail8.com carLicense: AVBLSP departmentNumber: 2194 employeeType: Employee homePhone: +1 408 411-7466 initials: C. R. mobile: +1 408 965-4472 pager: +1 408 723-5928 roomNumber: 9944 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Graham Pereira,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Graham Pereira sn: Pereira description: This is Graham Pereira's description facsimileTelephoneNumber: +1 408 526-6291 l: Palo Alto ou: Payroll postalAddress: Payroll$Palo Alto telephoneNumber: +1 408 437-3512 title: Supreme Payroll Mascot userPassword: Password1 uid: PereiraG givenName: Graham mail: PereiraG@ns-mail7.com carLicense: 82L3RI departmentNumber: 7186 employeeType: Normal homePhone: +1 408 193-1306 initials: G. P. mobile: +1 408 452-6149 pager: +1 408 196-8079 roomNumber: 9882 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Angy Cuthbert,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Angy Cuthbert sn: Cuthbert description: This is Angy Cuthbert's description facsimileTelephoneNumber: +1 804 408-1630 l: Orem ou: Product Testing postalAddress: Product Testing$Orem telephoneNumber: +1 804 351-5903 title: Chief Product Testing Technician userPassword: Password1 uid: CuthberA givenName: Angy mail: CuthberA@ns-mail5.com carLicense: IOA2HP departmentNumber: 5746 employeeType: Employee homePhone: +1 804 165-7245 initials: A. C. mobile: +1 804 744-5515 pager: +1 804 705-5980 roomNumber: 8201 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Feodora Hoehn,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Feodora Hoehn sn: Hoehn description: This is Feodora Hoehn's description facsimileTelephoneNumber: +1 206 780-2856 l: Sunnyvale ou: Payroll postalAddress: Payroll$Sunnyvale telephoneNumber: +1 206 346-1250 title: Chief Payroll Manager userPassword: Password1 uid: HoehnF givenName: Feodora mail: HoehnF@ns-mail2.com carLicense: MSSAMG departmentNumber: 9626 employeeType: Normal homePhone: +1 206 141-7389 initials: F. H. mobile: +1 206 148-3534 pager: +1 206 869-5278 roomNumber: 9504 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Misti Sinanan,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Misti Sinanan sn: Sinanan description: This is Misti Sinanan's description facsimileTelephoneNumber: +1 206 327-3723 l: San Jose ou: Product Testing postalAddress: Product Testing$San Jose telephoneNumber: +1 206 731-8974 title: Junior Product Testing Artist userPassword: Password1 uid: SinananM givenName: Misti mail: SinananM@ns-mail2.com carLicense: GVY0L2 departmentNumber: 9439 employeeType: Normal homePhone: +1 206 678-8995 initials: M. S. mobile: +1 206 738-2238 pager: +1 206 176-7878 roomNumber: 9975 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Ilya Routhier,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Ilya Routhier sn: Routhier description: This is Ilya Routhier's description facsimileTelephoneNumber: +1 213 661-8820 l: Santa Clara ou: Peons postalAddress: Peons$Santa Clara telephoneNumber: +1 213 823-7498 title: Junior Peons Czar userPassword: Password1 uid: RouthieI givenName: Ilya mail: RouthieI@ns-mail3.com carLicense: K9SNHF departmentNumber: 7574 employeeType: Contract homePhone: +1 213 459-1487 initials: I. R. mobile: +1 213 397-2381 pager: +1 213 825-7583 roomNumber: 9542 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Megen Thibeault,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Megen Thibeault sn: Thibeault description: This is Megen Thibeault's description facsimileTelephoneNumber: +1 408 409-2882 l: San Francisco ou: Management postalAddress: Management$San Francisco telephoneNumber: +1 408 859-9279 title: Associate Management Fellow userPassword: Password1 uid: ThibeauM givenName: Megen mail: ThibeauM@ns-mail9.com carLicense: 9HH3AN departmentNumber: 8743 employeeType: Employee homePhone: +1 408 836-9082 initials: M. T. mobile: +1 408 309-6861 pager: +1 408 557-6378 roomNumber: 8929 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Wannell Regimbald,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Wannell Regimbald sn: Regimbald description: This is Wannell Regimbald's description facsimileTelephoneNumber: +1 818 521-3130 l: Milpitas ou: Human Resources postalAddress: Human Resources$Milpitas telephoneNumber: +1 818 113-7120 title: Associate Human Resources Technician userPassword: Password1 uid: RegimbaW givenName: Wannell mail: RegimbaW@ns-mail9.com carLicense: 2THDCT departmentNumber: 6395 employeeType: Employee homePhone: +1 818 776-4063 initials: W. R. mobile: +1 818 231-9431 pager: +1 818 171-8300 roomNumber: 9927 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Odele Rosser,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Odele Rosser sn: Rosser description: This is Odele Rosser's description facsimileTelephoneNumber: +1 213 277-4724 l: Cupertino ou: Product Testing postalAddress: Product Testing$Cupertino telephoneNumber: +1 213 467-1891 title: Supreme Product Testing Mascot userPassword: Password1 uid: RosserO givenName: Odele mail: RosserO@ns-mail4.com carLicense: SM50GM departmentNumber: 7376 employeeType: Employee homePhone: +1 213 759-1876 initials: O. R. mobile: +1 213 473-5144 pager: +1 213 233-3151 roomNumber: 8120 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Mariska Chauhan,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Mariska Chauhan sn: Chauhan description: This is Mariska Chauhan's description facsimileTelephoneNumber: +1 510 686-8071 l: Armonk ou: Human Resources postalAddress: Human Resources$Armonk telephoneNumber: +1 510 385-5509 title: Chief Human Resources Janitor userPassword: Password1 uid: ChauhanM givenName: Mariska mail: ChauhanM@ns-mail7.com carLicense: LJT2HS departmentNumber: 3740 employeeType: Employee homePhone: +1 510 470-1535 initials: M. C. mobile: +1 510 959-6649 pager: +1 510 690-3046 roomNumber: 9630 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Witold Blesi,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Witold Blesi sn: Blesi description: This is Witold Blesi's description facsimileTelephoneNumber: +1 818 360-1222 l: San Mateo ou: Product Testing postalAddress: Product Testing$San Mateo telephoneNumber: +1 818 283-2462 title: Master Product Testing Warrior userPassword: Password1 uid: BlesiW givenName: Witold mail: BlesiW@ns-mail4.com carLicense: CK96BA departmentNumber: 3674 employeeType: Employee homePhone: +1 818 341-8860 initials: W. B. mobile: +1 818 512-8782 pager: +1 818 164-4350 roomNumber: 8828 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Claudelle Oman,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Claudelle Oman sn: Oman description: This is Claudelle Oman's description facsimileTelephoneNumber: +1 415 217-2238 l: San Mateo ou: Management postalAddress: Management$San Mateo telephoneNumber: +1 415 141-7796 title: Associate Management Artist userPassword: Password1 uid: OmanC givenName: Claudelle mail: OmanC@ns-mail8.com carLicense: TG2R5U departmentNumber: 1027 employeeType: Contract homePhone: +1 415 496-8846 initials: C. O. mobile: +1 415 367-7841 pager: +1 415 926-2000 roomNumber: 8004 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Liz LeClair,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Liz LeClair sn: LeClair description: This is Liz LeClair's description facsimileTelephoneNumber: +1 818 436-9079 l: Alameda ou: Administrative postalAddress: Administrative$Alameda telephoneNumber: +1 818 890-8136 title: Associate Administrative Technician userPassword: Password1 uid: LeClairL givenName: Liz mail: LeClairL@ns-mail2.com carLicense: 5D50MG departmentNumber: 3922 employeeType: Employee homePhone: +1 818 895-3646 initials: L. L. mobile: +1 818 815-1759 pager: +1 818 737-8568 roomNumber: 8020 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Linette Kessler,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Linette Kessler sn: Kessler description: This is Linette Kessler's description facsimileTelephoneNumber: +1 804 135-2670 l: San Mateo ou: Administrative postalAddress: Administrative$San Mateo telephoneNumber: +1 804 609-6227 title: Master Administrative Writer userPassword: Password1 uid: KesslerL givenName: Linette mail: KesslerL@ns-mail7.com carLicense: HWNP8T departmentNumber: 8941 employeeType: Normal homePhone: +1 804 596-8015 initials: L. K. mobile: +1 804 910-8981 pager: +1 804 715-3786 roomNumber: 9285 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Cang Coverdale,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Cang Coverdale sn: Coverdale description: This is Cang Coverdale's description facsimileTelephoneNumber: +1 804 877-2698 l: Sunnyvale ou: Peons postalAddress: Peons$Sunnyvale telephoneNumber: +1 804 250-8301 title: Chief Peons Writer userPassword: Password1 uid: CoverdaC givenName: Cang mail: CoverdaC@ns-mail3.com carLicense: 0LT7LC departmentNumber: 2554 employeeType: Normal homePhone: +1 804 958-9840 initials: C. C. mobile: +1 804 271-5714 pager: +1 804 265-1282 roomNumber: 8314 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Luella Scheffler,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Luella Scheffler sn: Scheffler description: This is Luella Scheffler's description facsimileTelephoneNumber: +1 804 584-9027 l: Armonk ou: Janitorial postalAddress: Janitorial$Armonk telephoneNumber: +1 804 913-6978 title: Chief Janitorial Artist userPassword: Password1 uid: SchefflL givenName: Luella mail: SchefflL@ns-mail6.com carLicense: L426VC departmentNumber: 6083 employeeType: Normal homePhone: +1 804 456-5925 initials: L. S. mobile: +1 804 159-8096 pager: +1 804 810-4962 roomNumber: 9005 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Ginny Mattiussi,ou=Product Development,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Ginny Mattiussi sn: Mattiussi description: This is Ginny Mattiussi's description facsimileTelephoneNumber: +1 804 914-9991 l: San Francisco ou: Product Development postalAddress: Product Development$San Francisco telephoneNumber: +1 804 519-9515 title: Master Product Development Grunt userPassword: Password1 uid: MattiusG givenName: Ginny mail: MattiusG@ns-mail9.com carLicense: HW15SO departmentNumber: 3687 employeeType: Employee homePhone: +1 804 213-5427 initials: G. M. mobile: +1 804 160-1741 pager: +1 804 461-9969 roomNumber: 9539 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Aline Parham,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Aline Parham sn: Parham description: This is Aline Parham's description facsimileTelephoneNumber: +1 510 706-8345 l: Alameda ou: Administrative postalAddress: Administrative$Alameda telephoneNumber: +1 510 690-4531 title: Chief Administrative Mascot userPassword: Password1 uid: ParhamA givenName: Aline mail: ParhamA@ns-mail9.com carLicense: XXVJPS departmentNumber: 7990 employeeType: Contract homePhone: +1 510 255-3374 initials: A. P. mobile: +1 510 423-9519 pager: +1 510 352-3609 roomNumber: 9652 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Nahum Rozumna,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Nahum Rozumna sn: Rozumna description: This is Nahum Rozumna's description facsimileTelephoneNumber: +1 415 362-5910 l: Orem ou: Administrative postalAddress: Administrative$Orem telephoneNumber: +1 415 587-5000 title: Supreme Administrative Fellow userPassword: Password1 uid: RozumnaN givenName: Nahum mail: RozumnaN@ns-mail4.com carLicense: 9X3JU6 departmentNumber: 5771 employeeType: Contract homePhone: +1 415 332-4469 initials: N. R. mobile: +1 415 851-6778 pager: +1 415 744-7717 roomNumber: 9069 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Teresa Standel,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Teresa Standel sn: Standel description: This is Teresa Standel's description facsimileTelephoneNumber: +1 510 909-8353 l: Fremont ou: Human Resources postalAddress: Human Resources$Fremont telephoneNumber: +1 510 574-8221 title: Master Human Resources Writer userPassword: Password1 uid: StandelT givenName: Teresa mail: StandelT@ns-mail4.com carLicense: SO745H departmentNumber: 6921 employeeType: Employee homePhone: +1 510 224-9306 initials: T. S. mobile: +1 510 767-9421 pager: +1 510 480-4823 roomNumber: 8868 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Linn Chaintreuil,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Linn Chaintreuil sn: Chaintreuil description: This is Linn Chaintreuil's description facsimileTelephoneNumber: +1 408 158-6711 l: Redmond ou: Peons postalAddress: Peons$Redmond telephoneNumber: +1 408 385-8391 title: Junior Peons Janitor userPassword: Password1 uid: ChaintrL givenName: Linn mail: ChaintrL@ns-mail4.com carLicense: J8LFWI departmentNumber: 9011 employeeType: Normal homePhone: +1 408 691-2236 initials: L. C. mobile: +1 408 400-5967 pager: +1 408 572-7014 roomNumber: 9973 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Annmarie Howe-Patterson,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Annmarie Howe-Patterson sn: Howe-Patterson description: This is Annmarie Howe-Patterson's description facsimileTelephoneNumber: +1 206 563-7538 l: Fremont ou: Product Testing postalAddress: Product Testing$Fremont telephoneNumber: +1 206 870-8715 title: Junior Product Testing Madonna userPassword: Password1 uid: Howe-PaA givenName: Annmarie mail: Howe-PaA@ns-mail9.com carLicense: 8TG5CO departmentNumber: 6432 employeeType: Normal homePhone: +1 206 644-3679 initials: A. H. mobile: +1 206 252-3374 pager: +1 206 984-3849 roomNumber: 9630 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Idaline Sentner,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Idaline Sentner sn: Sentner description: This is Idaline Sentner's description facsimileTelephoneNumber: +1 213 603-9938 l: Redwood Shores ou: Peons postalAddress: Peons$Redwood Shores telephoneNumber: +1 213 227-4437 title: Junior Peons Manager userPassword: Password1 uid: SentnerI givenName: Idaline mail: SentnerI@ns-mail7.com carLicense: G1SYEB departmentNumber: 4382 employeeType: Contract homePhone: +1 213 447-1918 initials: I. S. mobile: +1 213 530-8298 pager: +1 213 507-7988 roomNumber: 9144 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Debbi Coriaty,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Debbi Coriaty sn: Coriaty description: This is Debbi Coriaty's description facsimileTelephoneNumber: +1 206 128-8898 l: Palo Alto ou: Peons postalAddress: Peons$Palo Alto telephoneNumber: +1 206 700-7016 title: Supreme Peons Madonna userPassword: Password1 uid: CoriatyD givenName: Debbi mail: CoriatyD@ns-mail3.com carLicense: HU3C3K departmentNumber: 1018 employeeType: Contract homePhone: +1 206 430-4177 initials: D. C. mobile: +1 206 967-3108 pager: +1 206 680-2262 roomNumber: 8654 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Freek Centeno,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Freek Centeno sn: Centeno description: This is Freek Centeno's description facsimileTelephoneNumber: +1 804 692-5003 l: San Francisco ou: Product Testing postalAddress: Product Testing$San Francisco telephoneNumber: +1 804 296-3073 title: Associate Product Testing Warrior userPassword: Password1 uid: CentenoF givenName: Freek mail: CentenoF@ns-mail7.com carLicense: KO19MC departmentNumber: 9433 employeeType: Normal homePhone: +1 804 934-4523 initials: F. C. mobile: +1 804 920-5665 pager: +1 804 162-5559 roomNumber: 9852 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Open O Karina,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Open O Karina sn: O Karina description: This is Open O Karina's description facsimileTelephoneNumber: +1 408 356-5158 l: Menlo Park ou: Management postalAddress: Management$Menlo Park telephoneNumber: +1 408 708-3880 title: Supreme Management Sales Rep userPassword: Password1 uid: O KarinO givenName: Open mail: O KarinO@ns-mail8.com carLicense: 9TYDV7 departmentNumber: 6655 employeeType: Employee homePhone: +1 408 789-7498 initials: O. O. mobile: +1 408 342-4421 pager: +1 408 159-1587 roomNumber: 8636 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Rajani Ciochon,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Rajani Ciochon sn: Ciochon description: This is Rajani Ciochon's description facsimileTelephoneNumber: +1 408 395-6771 l: Cupertino ou: Human Resources postalAddress: Human Resources$Cupertino telephoneNumber: +1 408 603-7449 title: Associate Human Resources President userPassword: Password1 uid: CiochonR givenName: Rajani mail: CiochonR@ns-mail4.com carLicense: 2V9ICI departmentNumber: 5209 employeeType: Employee homePhone: +1 408 686-6963 initials: R. C. mobile: +1 408 468-1043 pager: +1 408 806-7145 roomNumber: 9276 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Perla Chilton,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Perla Chilton sn: Chilton description: This is Perla Chilton's description facsimileTelephoneNumber: +1 408 360-5565 l: Fremont ou: Product Testing postalAddress: Product Testing$Fremont telephoneNumber: +1 408 236-1743 title: Associate Product Testing Consultant userPassword: Password1 uid: ChiltonP givenName: Perla mail: ChiltonP@ns-mail8.com carLicense: AN68BN departmentNumber: 3054 employeeType: Contract homePhone: +1 408 571-6456 initials: P. C. mobile: +1 408 574-9687 pager: +1 408 908-1256 roomNumber: 9168 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Viviene Caruth,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Viviene Caruth sn: Caruth description: This is Viviene Caruth's description facsimileTelephoneNumber: +1 408 285-5896 l: Sunnyvale ou: Janitorial postalAddress: Janitorial$Sunnyvale telephoneNumber: +1 408 430-7660 title: Chief Janitorial Figurehead userPassword: Password1 uid: CaruthV givenName: Viviene mail: CaruthV@ns-mail4.com carLicense: 2WJGYG departmentNumber: 9174 employeeType: Normal homePhone: +1 408 954-7152 initials: V. C. mobile: +1 408 795-6215 pager: +1 408 803-2110 roomNumber: 9951 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Quyen Witzman,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Quyen Witzman sn: Witzman description: This is Quyen Witzman's description facsimileTelephoneNumber: +1 408 968-6635 l: Menlo Park ou: Janitorial postalAddress: Janitorial$Menlo Park telephoneNumber: +1 408 506-5945 title: Chief Janitorial Mascot userPassword: Password1 uid: WitzmanQ givenName: Quyen mail: WitzmanQ@ns-mail5.com carLicense: 53VMR1 departmentNumber: 4066 employeeType: Employee homePhone: +1 408 602-4608 initials: Q. W. mobile: +1 408 541-6494 pager: +1 408 880-8573 roomNumber: 9964 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Bevvy Xmssupport,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Bevvy Xmssupport sn: Xmssupport description: This is Bevvy Xmssupport's description facsimileTelephoneNumber: +1 206 454-8695 l: Armonk ou: Management postalAddress: Management$Armonk telephoneNumber: +1 206 671-2556 title: Chief Management Grunt userPassword: Password1 uid: XmssuppB givenName: Bevvy mail: XmssuppB@ns-mail9.com carLicense: S4OOKY departmentNumber: 1644 employeeType: Normal homePhone: +1 206 231-8140 initials: B. X. mobile: +1 206 159-4420 pager: +1 206 947-5096 roomNumber: 8586 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Dulsea Norment,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Dulsea Norment sn: Norment description: This is Dulsea Norment's description facsimileTelephoneNumber: +1 213 576-4550 l: Sunnyvale ou: Peons postalAddress: Peons$Sunnyvale telephoneNumber: +1 213 729-4103 title: Master Peons Dictator userPassword: Password1 uid: NormentD givenName: Dulsea mail: NormentD@ns-mail3.com carLicense: VV4JV4 departmentNumber: 2104 employeeType: Contract homePhone: +1 213 852-3212 initials: D. N. mobile: +1 213 749-3759 pager: +1 213 934-6464 roomNumber: 9412 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Dinny Golshan,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Dinny Golshan sn: Golshan description: This is Dinny Golshan's description facsimileTelephoneNumber: +1 408 391-3392 l: San Jose ou: Human Resources postalAddress: Human Resources$San Jose telephoneNumber: +1 408 196-2264 title: Associate Human Resources Architect userPassword: Password1 uid: GolshanD givenName: Dinny mail: GolshanD@ns-mail2.com carLicense: N22U7C departmentNumber: 8809 employeeType: Normal homePhone: +1 408 690-7616 initials: D. G. mobile: +1 408 699-7377 pager: +1 408 657-3991 roomNumber: 9134 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Gnni Leone,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Gnni Leone sn: Leone description: This is Gnni Leone's description facsimileTelephoneNumber: +1 408 994-6226 l: San Francisco ou: Janitorial postalAddress: Janitorial$San Francisco telephoneNumber: +1 408 813-6244 title: Associate Janitorial Technician userPassword: Password1 uid: LeoneG givenName: Gnni mail: LeoneG@ns-mail2.com carLicense: LNVD7N departmentNumber: 8886 employeeType: Employee homePhone: +1 408 574-4642 initials: G. L. mobile: +1 408 499-5496 pager: +1 408 803-2909 roomNumber: 9374 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Kambhampati Hutson,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Kambhampati Hutson sn: Hutson description: This is Kambhampati Hutson's description facsimileTelephoneNumber: +1 408 621-9746 l: Santa Clara ou: Peons postalAddress: Peons$Santa Clara telephoneNumber: +1 408 181-1729 title: Junior Peons Artist userPassword: Password1 uid: HutsonK givenName: Kambhampati mail: HutsonK@ns-mail3.com carLicense: 924O92 departmentNumber: 8427 employeeType: Contract homePhone: +1 408 583-4543 initials: K. H. mobile: +1 408 657-3416 pager: +1 408 798-3429 roomNumber: 9495 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Vallier Jarchow,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Vallier Jarchow sn: Jarchow description: This is Vallier Jarchow's description facsimileTelephoneNumber: +1 818 662-8472 l: Fremont ou: Peons postalAddress: Peons$Fremont telephoneNumber: +1 818 511-8166 title: Chief Peons Fellow userPassword: Password1 uid: JarchowV givenName: Vallier mail: JarchowV@ns-mail6.com carLicense: SU0SYE departmentNumber: 9398 employeeType: Employee homePhone: +1 818 369-7976 initials: V. J. mobile: +1 818 983-6860 pager: +1 818 982-1894 roomNumber: 9228 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Dalila Acree,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Dalila Acree sn: Acree description: This is Dalila Acree's description facsimileTelephoneNumber: +1 510 124-6912 l: Cupertino ou: Management postalAddress: Management$Cupertino telephoneNumber: +1 510 891-1960 title: Supreme Management Architect userPassword: Password1 uid: AcreeD givenName: Dalila mail: AcreeD@ns-mail7.com carLicense: HV6DTG departmentNumber: 4869 employeeType: Normal homePhone: +1 510 560-5075 initials: D. A. mobile: +1 510 919-7847 pager: +1 510 886-9892 roomNumber: 9876 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Akemi Brosselard,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Akemi Brosselard sn: Brosselard description: This is Akemi Brosselard's description facsimileTelephoneNumber: +1 804 319-8418 l: Orem ou: Administrative postalAddress: Administrative$Orem telephoneNumber: +1 804 642-5991 title: Supreme Administrative Writer userPassword: Password1 uid: BrosselA givenName: Akemi mail: BrosselA@ns-mail3.com carLicense: 8G6K6F departmentNumber: 5657 employeeType: Employee homePhone: +1 804 435-1410 initials: A. B. mobile: +1 804 844-6239 pager: +1 804 983-8937 roomNumber: 8980 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Katsunori Soong,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Katsunori Soong sn: Soong description: This is Katsunori Soong's description facsimileTelephoneNumber: +1 804 155-1859 l: Armonk ou: Administrative postalAddress: Administrative$Armonk telephoneNumber: +1 804 786-3820 title: Junior Administrative Manager userPassword: Password1 uid: SoongK givenName: Katsunori mail: SoongK@ns-mail4.com carLicense: WL605T departmentNumber: 2495 employeeType: Normal homePhone: +1 804 425-3905 initials: K. S. mobile: +1 804 666-4430 pager: +1 804 562-1815 roomNumber: 8364 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Lai Vezina,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Lai Vezina sn: Vezina description: This is Lai Vezina's description facsimileTelephoneNumber: +1 818 329-2142 l: San Francisco ou: Product Testing postalAddress: Product Testing$San Francisco telephoneNumber: +1 818 250-4180 title: Chief Product Testing Engineer userPassword: Password1 uid: VezinaL givenName: Lai mail: VezinaL@ns-mail4.com carLicense: NTMMT2 departmentNumber: 2673 employeeType: Normal homePhone: +1 818 143-3010 initials: L. V. mobile: +1 818 779-1783 pager: +1 818 816-1789 roomNumber: 8787 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Erdem Kelleher,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Erdem Kelleher sn: Kelleher description: This is Erdem Kelleher's description facsimileTelephoneNumber: +1 804 221-1664 l: Palo Alto ou: Peons postalAddress: Peons$Palo Alto telephoneNumber: +1 804 131-4472 title: Junior Peons Admin userPassword: Password1 uid: KelleheE givenName: Erdem mail: KelleheE@ns-mail5.com carLicense: 4G7QOA departmentNumber: 8901 employeeType: Employee homePhone: +1 804 370-6448 initials: E. K. mobile: +1 804 758-6068 pager: +1 804 638-7576 roomNumber: 9797 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Huyen Gebhart,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Huyen Gebhart sn: Gebhart description: This is Huyen Gebhart's description facsimileTelephoneNumber: +1 818 535-4968 l: Sunnyvale ou: Human Resources postalAddress: Human Resources$Sunnyvale telephoneNumber: +1 818 512-1598 title: Associate Human Resources Sales Rep userPassword: Password1 uid: GebhartH givenName: Huyen mail: GebhartH@ns-mail7.com carLicense: 6T55R9 departmentNumber: 4076 employeeType: Employee homePhone: +1 818 457-6466 initials: H. G. mobile: +1 818 348-4167 pager: +1 818 722-2918 roomNumber: 9844 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Wendye Deligdisch,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Wendye Deligdisch sn: Deligdisch description: This is Wendye Deligdisch's description facsimileTelephoneNumber: +1 804 722-9859 l: Milpitas ou: Janitorial postalAddress: Janitorial$Milpitas telephoneNumber: +1 804 760-1413 title: Master Janitorial Visionary userPassword: Password1 uid: DeligdiW givenName: Wendye mail: DeligdiW@ns-mail4.com carLicense: S877EF departmentNumber: 9328 employeeType: Employee homePhone: +1 804 983-7072 initials: W. D. mobile: +1 804 619-9717 pager: +1 804 426-7794 roomNumber: 8975 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Gerrard Rosko,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Gerrard Rosko sn: Rosko description: This is Gerrard Rosko's description facsimileTelephoneNumber: +1 206 183-8561 l: San Mateo ou: Management postalAddress: Management$San Mateo telephoneNumber: +1 206 246-1483 title: Master Management Figurehead userPassword: Password1 uid: RoskoG givenName: Gerrard mail: RoskoG@ns-mail3.com carLicense: K96SWS departmentNumber: 5059 employeeType: Normal homePhone: +1 206 444-5411 initials: G. R. mobile: +1 206 248-5523 pager: +1 206 442-1788 roomNumber: 9685 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Alanah Thornber,ou=Product Development,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Alanah Thornber sn: Thornber description: This is Alanah Thornber's description facsimileTelephoneNumber: +1 206 643-4551 l: Armonk ou: Product Development postalAddress: Product Development$Armonk telephoneNumber: +1 206 420-8668 title: Associate Product Development Pinhead userPassword: Password1 uid: ThornbeA givenName: Alanah mail: ThornbeA@ns-mail5.com carLicense: 0N330J departmentNumber: 2333 employeeType: Employee homePhone: +1 206 864-8920 initials: A. T. mobile: +1 206 629-1327 pager: +1 206 959-6612 roomNumber: 8068 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Peggie Environment,ou=Product Development,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Peggie Environment sn: Environment description: This is Peggie Environment's description facsimileTelephoneNumber: +1 415 742-7965 l: Milpitas ou: Product Development postalAddress: Product Development$Milpitas telephoneNumber: +1 415 464-8729 title: Master Product Development Architect userPassword: Password1 uid: EnvironP givenName: Peggie mail: EnvironP@ns-mail8.com carLicense: E1X5Y4 departmentNumber: 7482 employeeType: Normal homePhone: +1 415 922-9682 initials: P. E. mobile: +1 415 361-2591 pager: +1 415 773-5529 roomNumber: 9573 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Darcey McMahon,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Darcey McMahon sn: McMahon description: This is Darcey McMahon's description facsimileTelephoneNumber: +1 510 767-7263 l: Palo Alto ou: Janitorial postalAddress: Janitorial$Palo Alto telephoneNumber: +1 510 403-4704 title: Master Janitorial Fellow userPassword: Password1 uid: McMahonD givenName: Darcey mail: McMahonD@ns-mail4.com carLicense: XKRJSW departmentNumber: 3382 employeeType: Employee homePhone: +1 510 944-1203 initials: D. M. mobile: +1 510 438-2676 pager: +1 510 375-9906 roomNumber: 8696 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Sallyanne Efland,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Sallyanne Efland sn: Efland description: This is Sallyanne Efland's description facsimileTelephoneNumber: +1 818 696-2230 l: San Jose ou: Administrative postalAddress: Administrative$San Jose telephoneNumber: +1 818 536-5370 title: Supreme Administrative Janitor userPassword: Password1 uid: EflandS givenName: Sallyanne mail: EflandS@ns-mail9.com carLicense: A3DRW4 departmentNumber: 2649 employeeType: Normal homePhone: +1 818 923-4868 initials: S. E. mobile: +1 818 963-2769 pager: +1 818 623-2612 roomNumber: 8464 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Ginevra Adamson,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Ginevra Adamson sn: Adamson description: This is Ginevra Adamson's description facsimileTelephoneNumber: +1 408 351-9104 l: Menlo Park ou: Management postalAddress: Management$Menlo Park telephoneNumber: +1 408 367-8946 title: Chief Management Dictator userPassword: Password1 uid: AdamsonG givenName: Ginevra mail: AdamsonG@ns-mail2.com carLicense: R9T589 departmentNumber: 1042 employeeType: Employee homePhone: +1 408 567-7901 initials: G. A. mobile: +1 408 269-6573 pager: +1 408 196-2973 roomNumber: 8699 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Oralia Winlow,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Oralia Winlow sn: Winlow description: This is Oralia Winlow's description facsimileTelephoneNumber: +1 818 498-6469 l: Milpitas ou: Peons postalAddress: Peons$Milpitas telephoneNumber: +1 818 363-5209 title: Chief Peons Madonna userPassword: Password1 uid: WinlowO givenName: Oralia mail: WinlowO@ns-mail4.com carLicense: X8N9Y4 departmentNumber: 1787 employeeType: Employee homePhone: +1 818 733-3917 initials: O. W. mobile: +1 818 472-3442 pager: +1 818 842-7463 roomNumber: 9394 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Syyed O Karina,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Syyed O Karina sn: O Karina description: This is Syyed O Karina's description facsimileTelephoneNumber: +1 206 685-4339 l: Redmond ou: Management postalAddress: Management$Redmond telephoneNumber: +1 206 165-3305 title: Junior Management Stooge userPassword: Password1 uid: O KarinS givenName: Syyed mail: O KarinS@ns-mail5.com carLicense: 79HD2M departmentNumber: 5833 employeeType: Contract homePhone: +1 206 359-4285 initials: S. O. mobile: +1 206 300-5317 pager: +1 206 776-7090 roomNumber: 8148 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Kazuo Rintoul,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Kazuo Rintoul sn: Rintoul description: This is Kazuo Rintoul's description facsimileTelephoneNumber: +1 510 457-4969 l: San Mateo ou: Administrative postalAddress: Administrative$San Mateo telephoneNumber: +1 510 687-8512 title: Supreme Administrative Engineer userPassword: Password1 uid: RintoulK givenName: Kazuo mail: RintoulK@ns-mail2.com carLicense: H4ALP9 departmentNumber: 4549 employeeType: Contract homePhone: +1 510 881-3861 initials: K. R. mobile: +1 510 771-5475 pager: +1 510 238-3485 roomNumber: 9965 manager: cn=Nashir Shiffer,ou=Management,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Kaile Klingsporn,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Kaile Klingsporn sn: Klingsporn description: This is Kaile Klingsporn's description facsimileTelephoneNumber: +1 415 324-2082 l: Fremont ou: Human Resources postalAddress: Human Resources$Fremont telephoneNumber: +1 415 639-4326 title: Supreme Human Resources Sales Rep userPassword: Password1 uid: KlingspK givenName: Kaile mail: KlingspK@ns-mail3.com carLicense: 3I7UOR departmentNumber: 7954 employeeType: Contract homePhone: +1 415 607-7030 initials: K. K. mobile: +1 415 947-1280 pager: +1 415 876-4511 roomNumber: 8027 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Ext Askins,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson cn: Ext Askins sn: Askins description: This is Ext Askins's description facsimileTelephoneNumber: +1 415 741-9451 l: San Mateo ou: Janitorial postalAddress: Janitorial$San Mateo telephoneNumber: +1 415 481-4962 title: Supreme Janitorial Figurehead userPassword: Password1 uid: AskinsE givenName: Ext mail: AskinsE@ns-mail4.com carLicense: YIJATP departmentNumber: 4834 employeeType: Normal homePhone: +1 415 820-1417 initials: E. A. mobile: +1 415 665-9922 pager: +1 415 381-9637 roomNumber: 9532 manager: cn=Tilly Pilote,ou=Peons,dc=corp,dc=acme,dc=local secretary: cn=Evvy Gattrell,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Accountants,ou=Accounting,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Accountants member: uid=LaurentiusA,ou=Accounting,dc=corp,dc=acme,dc=local dn: cn=Developers,ou=Product Development,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Developers member: uid=ThornbeA,ou=Product Development,dc=corp,dc=acme,dc=local member: uid=MattiusG,ou=Product Development,dc=corp,dc=acme,dc=local member: uid=RuddleJ,ou=Product Development,dc=corp,dc=acme,dc=local member: uid=EnvironP,ou=Product Development,dc=corp,dc=acme,dc=local member: uid=Van VeeY,ou=Product Development,dc=corp,dc=acme,dc=local dn: cn=Testers,ou=Product Testing,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Testers member: uid=BasmadjC,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=CuthberA,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=Howe-PaA,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=ReddingC,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=HoaglanF,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=CentenoF,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=BlumenfH,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=VezinaL,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=SinananM,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=RosserO,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=ChiltonP,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=PrestonR,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=PiaseckW,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=BlesiW,ou=Product Testing,dc=corp,dc=acme,dc=local member: uid=ENGY,ou=Product Testing,dc=corp,dc=acme,dc=local dn: cn=Human Resources,ou=Human Resources,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Human Resources member: uid=MejiaD,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=GolshanD,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=MorelliH,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=GebhartH,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=KlingspK,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=KlebschL,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=ChauhanM,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=CiochonR,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=GohR,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=StandelT,ou=Human Resources,dc=corp,dc=acme,dc=local member: uid=RegimbaW,ou=Human Resources,dc=corp,dc=acme,dc=local dn: cn=Payroll,ou=Payroll,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Payroll member: uid=CharneyR,ou=Payroll,dc=corp,dc=acme,dc=local member: uid=HoehnF,ou=Payroll,dc=corp,dc=acme,dc=local member: uid=SmulderG,ou=Payroll,dc=corp,dc=acme,dc=local member: uid=PereiraG,ou=Payroll,dc=corp,dc=acme,dc=local member: uid=WortmanJ,ou=Payroll,dc=corp,dc=acme,dc=local member: uid=LoughJ,ou=Payroll,dc=corp,dc=acme,dc=local member: uid=BittenbL,ou=Payroll,dc=corp,dc=acme,dc=local dn: cn=Janitorial,ou=Janitorial,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Janitorial member: uid=FleugelR,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=NahmiasA,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=KakutaA,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=QuinnA,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=McMahonD,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=AskinsE,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=StachowF,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=LeoneG,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=JankowsG,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=TibiL,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=SchefflL,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=LinderP,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=MacLeanP,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=WitzmanQ,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=ButtreyS,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=FrederiS,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=VodickaV,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=CaruthV,ou=Janitorial,dc=corp,dc=acme,dc=local member: uid=DeligdiW,ou=Janitorial,dc=corp,dc=acme,dc=local dn: cn=Management,ou=Management,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Management member: uid=GattrelE,ou=Management,dc=corp,dc=acme,dc=local member: uid=BoucourA,ou=Management,dc=corp,dc=acme,dc=local member: uid=XmssuppB,ou=Management,dc=corp,dc=acme,dc=local member: uid=OmanC,ou=Management,dc=corp,dc=acme,dc=local member: uid=AcreeD,ou=Management,dc=corp,dc=acme,dc=local member: uid=MichaloF,ou=Management,dc=corp,dc=acme,dc=local member: uid=PetrickG,ou=Management,dc=corp,dc=acme,dc=local member: uid=RoskoG,ou=Management,dc=corp,dc=acme,dc=local member: uid=AdamsonG,ou=Management,dc=corp,dc=acme,dc=local member: uid=AdamH,ou=Management,dc=corp,dc=acme,dc=local member: uid=HaganH,ou=Management,dc=corp,dc=acme,dc=local member: uid=ThibeauM,ou=Management,dc=corp,dc=acme,dc=local member: uid=ShifferN,ou=Management,dc=corp,dc=acme,dc=local member: uid=McElligS,ou=Management,dc=corp,dc=acme,dc=local member: uid=EverittT,ou=Management,dc=corp,dc=acme,dc=local dn: cn=Administrative,ou=Administrative,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Administrative member: uid=FujiwarK,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=BrosselA,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=ParhamA,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=HitchcoG,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=SoongK,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=RintoulK,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=KesslerL,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=LeClairL,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=RozumnaN,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=MarouchP,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=IEMR,ou=Administrative,dc=corp,dc=acme,dc=local member: uid=EflandS,ou=Administrative,dc=corp,dc=acme,dc=local dn: cn=Peons,ou=Peons,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Peons member: uid=PiloteT,ou=Peons,dc=corp,dc=acme,dc=local member: uid=ErkelA,ou=Peons,dc=corp,dc=acme,dc=local member: uid=CoverdaC,ou=Peons,dc=corp,dc=acme,dc=local member: uid=CoriatyD,ou=Peons,dc=corp,dc=acme,dc=local member: uid=NormentD,ou=Peons,dc=corp,dc=acme,dc=local member: uid=KelleheE,ou=Peons,dc=corp,dc=acme,dc=local member: uid=SentnerI,ou=Peons,dc=corp,dc=acme,dc=local member: uid=RouthieI,ou=Peons,dc=corp,dc=acme,dc=local member: uid=HiltonJ,ou=Peons,dc=corp,dc=acme,dc=local member: uid=HutsonK,ou=Peons,dc=corp,dc=acme,dc=local member: uid=BelcherK,ou=Peons,dc=corp,dc=acme,dc=local member: uid=ChaintrL,ou=Peons,dc=corp,dc=acme,dc=local member: uid=WinlowO,ou=Peons,dc=corp,dc=acme,dc=local member: uid=JarchowV,ou=Peons,dc=corp,dc=acme,dc=local dn: cn=Planning,ou=Planning,dc=corp,dc=acme,dc=local changetype: add objectclass: top objectclass: groupofnames cn: Planning member: uid=LaurentiusW,ou=Planning,dc=corp,dc=acme,dc=local ================================================ FILE: config/stage/dev/otel/otel-collector-config-tls.yaml ================================================ receivers: otlp: protocols: grpc: endpoint: acme-otel-collector:4317 tls: cert_file: /cert.pem key_file: /key.pem min_version: "1.2" max_version: "1.3" exporters: otlp: endpoint: ops.acme.test:4317 tls: min_version: "1.2" max_version: "1.3" ca_file: /rootca.pem cert_file: /cert.pem key_file: /key.pem debug: {} processors: batch: extensions: health_check: service: extensions: [health_check] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp] ================================================ FILE: config/stage/dev/otel/otel-collector-config.yaml ================================================ receivers: otlp: protocols: grpc: endpoint: acme-otel-collector:4317 exporters: otlp: endpoint: ops.acme.test:4317 tls: insecure: true processors: batch: extensions: health_check: service: extensions: [health_check] pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp] ================================================ FILE: config/stage/dev/prometheus/prometheus.yml ================================================ # my global config global: scrape_interval: 15s # By default, scrape targets every 15 seconds. evaluation_interval: 15s # By default, scrape targets every 15 seconds. # scrape_timeout is set to the global default (10s). # Attach these labels to any time series or alerts when communicating with # external systems (federation, remote storage, Alertmanager). external_labels: monitor: 'acme' # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: # The job name is added as a label `job=` to any timeseries scraped from this config. - job_name: 'keycloak' # Override the global default and scrape targets from this job every 5 seconds. scrape_interval: 5s metrics_path: /auth/metrics scheme: https tls_config: insecure_skip_verify: true static_configs: - targets: [ 'acme-keycloak:9000' ] labels: env: dev # - job_name: 'acme-keycloak-metrics-spi' # metrics_path: 'auth/realms/master/metrics' # static_configs: # - targets: ['acme-keycloak:8080'] # labels: # env: dev ================================================ FILE: config/stage/dev/realms/acme-api.yaml ================================================ # Example for modelling API clients realm: acme-api enabled: true displayName: Acme API loginWithEmailAllowed: true registrationAllowed: true registrationEmailAsUsername: true #loginTheme: apps #loginTheme: keycloak #accountTheme: keycloak.v3 #adminTheme: keycloak #emailTheme: keycloak internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) #browserFlow: "Browser Identity First with IdP Routing" #registrationFlow: "Custom Registration" # Custom realm attributes attributes: # for http variant: http://apps.acme.test:4000 "acme_site_url": "https://apps.acme.test:4443" smtpServer: port: 1025 host: mail from: "acme-api-sso@local" fromDisplayName: "Acme API Account" replyTo: "no-reply@acme.test" replyToDisplayName: "Acme API Support" clientScopes: # Custom OAuth2 client-scope to centrally configure mappers / role scopes - name: acme description: Acme Access protocol: openid-connect clientScopeMappings: # Expose api-user / api-admin client roles when referencing the scope "acme" "acme-api": - clientScope: "acme" roles: - "api-user" roles: # Client specific role definitions for the acme-api client client: "acme-api": - name: "api-default" description: "API default role" composite: true composites: client: "acme-api": - "api-user" - name: "api-user" description: "API User Role" clientRole: true clients: # The generic acme-api client to define roles (see roles above) - clientId: acme-api protocol: openid-connect name: Acme API description: "Acme API that represents API clients" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false fullScopeAllowed: false # Represents the acme-api instance for customer1 - clientId: acme-api-customer1 protocol: openid-connect name: Acme API for Customer 1 description: "Acme API for Customer 1 that can obtain tokens via grant_type=client_credentials" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true fullScopeAllowed: false # this secret would be individual for each customer secret: "$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)" defaultClientScopes: - "basic" - "email" - "profile" - "roles" - "acme" users: - username: service-account-acme-api-customer1 enabled: true serviceAccountClientId: acme-api-customer1 clientRoles: "acme-api": - "api-default" ================================================ FILE: config/stage/dev/realms/acme-apps.yaml ================================================ realm: acme-apps enabled: true displayName: Acme Apps displayNameHtml: Acme Apps loginWithEmailAllowed: true registrationAllowed: true registrationEmailAsUsername: false #loginTheme: apps loginTheme: internal-modern #accountTheme: keycloak #adminTheme: keycloak emailTheme: internal-modern internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) browserFlow: "browser" registrationFlow: "Custom Registration" # Custom realm attributes attributes: # for http variant: http://apps.acme.test:4000 "acme_site_url": "https://apps.acme.test:4443" "acme_terms_url": "https://apps.acme.test:4443/site/terms.html" "acme_imprint_url": "https://apps.acme.test:4443/site/imprint.html" "acme_privacy_url": "https://apps.acme.test:4443/site/privacy.html" #"acme_logo_url": "no example, should be taken from client or null" "acme_account_deleted_url": "https://apps.acme.test:4443/site/accountdeleted.html" friendlyCaptchaEnabled: "false" # register your own friendly captcha app at https://friendlycaptcha.com/ friendlyCaptchaSiteKey: $(env:FRIENDLY_CAPTCHA_SITE_KEY:-DUMMY) friendlyCaptchaSecret: $(env:FRIENDLY_CAPTCHA_SECRET:-DUMMY) friendlyCaptchaStart: "auto" friendlyCaptchaSolutionFieldName: "frc-captcha-solution" friendlyCaptchaSourceModule: "https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.10/widget.module.min.js" friendlyCaptchaSourceNoModule: "https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.10/widget.polyfilled.min.js" smtpServer: replyToDisplayName: "Acme APPS Support" port: 1025 host: mail replyTo: "no-reply@acme.test" from: "acme-apps-sso@local" fromDisplayName: "Acme APPS Account" clientScopes: - name: acme.profile description: Acme Profile Access protocol: openid-connect - name: acme.api description: Acme API Access protocol: openid-connect - name: acme.ageinfo description: Acme Profile AgeInfo protocol: openid-connect protocolMappers: - name: "Acme: Audience Resolve" protocol: openid-connect protocolMapper: oidc-audience-resolve-mapper consentRequired: false - name: "Acme: AgeInfo" protocol: openid-connect protocolMapper: oidc-acme-ageinfo-mapper consentRequired: false config: userinfo.token.claim: "true" id.token.claim: "true" access.token.claim: "false" - name: name description: Name Details protocol: openid-connect protocolMappers: - name: "Acme: Given Name" protocol: openid-connect protocolMapper: oidc-usermodel-property-mapper config: "user.attribute": "firstName" "claim.name": "given_name" "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" - name: "Acme: Family Name" protocol: openid-connect protocolMapper: oidc-usermodel-property-mapper config: "user.attribute": "lastName" "claim.name": "family_name" "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" - name: "Acme: Display Name" protocol: openid-connect protocolMapper: oidc-full-name-mapper config: "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" - name: "Acme: Username" protocol: openid-connect protocolMapper: oidc-usermodel-property-mapper config: "user.attribute": "username" "claim.name": "preferred_username" "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" clients: - clientId: app-minispa protocol: openid-connect name: Acme Account Console description: "Acme Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-apps&show=profile,settings,apps,logout" adminUrl: "" redirectUris: - "/*" - "http://localhost:4000/acme-account/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "roles" - "profile" optionalClientScopes: - "acme.profile" - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: app-greetme protocol: openid-connect name: Acme Greet Me description: "App Greet Me Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "$(env:APPS_FRONTEND_URL_GREETME)" baseUrl: "/?realm=acme-apps" adminUrl: "" redirectUris: - "/*" - "http://localhost:4000/acme-greetme/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" optionalClientScopes: - "phone" - "name" attributes: "post.logout.redirect.uris": "+" identityProviders: - alias: "idp-acme-internal" displayName: "Acme Internal Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "1000" issuer: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal" tokenUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/token" jwksUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/certs" userInfoUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/userinfo" authorizationUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/auth" logoutUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-internal/protocol/openid-connect/logout" clientId: "acme_internal_idp_broker" clientSecret: "$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceMethod: "S256" pkceEnabled: "true" - alias: idp-acme-saml displayName: Acme SAML Login providerId: saml enabled: true updateProfileFirstLoginMode: 'on' trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" config: validateSignature: 'true' hideOnLoginPage: true guiOrder: "2000" # Note this singing certificate must match the 'custom-rsa-generated' in acme-saml.yaml signingCertificate: "MIIClzCCAX8CBgF/0OmrYzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMDMyODE0MjIyOVoXDTMyMDMyODE0MjQwOVowDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOVGgrZfj96C5zNhlzLi8KWXoqVYq2ZWlH5mykT55FSvwC5m5/Px63VOzxuNWDAyGz8Uq9lUa5ED2D10W/e72AIbEC0w2F9z91cyElitsr/uQoI3snCJjLchXMez50u0J/g/78tfhv1ICo6EhPzupMBWwl+Liw1fiUv54pLPVM1r450fcQxaVX/jZszzZgLrtzbQz73uoUHJ6QJ7N2wz5c+sG3iy9OyVQl+uI0dIs9RFc57UUOURw2lOPgAPErKnckV5gEDQ16C07EvjVzzv1Q6SE2FIVN4F65qSRQ1iXU2uI0rdNTOkju5WNJylsmp8dfJE8HiOwjQ8ArZ/nTAgukCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAcDoujUldX1HiCjPjLrcUaY+LNCSsGWeN7g/lI7z18sxg3VlhsPz2Bg5m4zZCFVcrTPax1PuNqYIxetR9fEP8N+8GHLTnd4KrGvE6rH8xwDDk3GET5QKHnaUDUoxdOND85d65oL20NDIHaNDP+Kw/XIktV30mTKZerkDpxJSC9101RDwVhH3zpr0t4CYTnnR6NTBNkVRfDl19Nia98KpbSJizIw2y0zC8wubJzFnBoWbXv1AXOqTZUR2pyP742YJNA/9NFg4+EDbW/ZJVaajY+UVN8ImCj1T32f78189d3NFoCX81pBkmRv8YfXetZgDcofuKKTkUmFlP55x5S32Vmw==" postBindingLogout: 'true' nameIDPolicyFormat: "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" postBindingResponse: 'true' principalAttribute: "username" singleLogoutServiceUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml" entityId: acme_saml_idp_broker backchannelSupported: 'true' signatureAlgorithm: RSA_SHA256 xmlSigKeyInfoKeyNameTransformer: KEY_ID loginHint: 'true' authnContextComparisonType: exact postBindingAuthnRequest: 'true' syncMode: FORCE singleSignOnServiceUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml" wantAuthnRequestsSigned: 'true' addExtensionsElementWithKeyInfo: 'false' principalType: SUBJECT - alias: idp-mocksaml displayName: "Mock SAML Login" providerId: saml enabled: true updateProfileFirstLoginMode: 'on' trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" config: validateSignature: 'true' hideOnLoginPage: false guiOrder: "2100" signingCertificate: "MIIC4jCCAcoCCQC33wnybT5QZDANBgkqhkiG9w0BAQsFADAyMQswCQYDVQQGEwJV\nSzEPMA0GA1UECgwGQm94eUhRMRIwEAYDVQQDDAlNb2NrIFNBTUwwIBcNMjIwMjI4\nMjE0NjM4WhgPMzAyMTA3MDEyMTQ2MzhaMDIxCzAJBgNVBAYTAlVLMQ8wDQYDVQQK\nDAZCb3h5SFExEjAQBgNVBAMMCU1vY2sgU0FNTDCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBALGfYettMsct1T6tVUwTudNJH5Pnb9GGnkXi9Zw/e6x45DD0\nRuRONbFlJ2T4RjAE/uG+AjXxXQ8o2SZfb9+GgmCHuTJFNgHoZ1nFVXCmb/Hg8Hpd\n4vOAGXndixaReOiq3EH5XvpMjMkJ3+8+9VYMzMZOjkgQtAqO36eAFFfNKX7dTj3V\npwLkvz6/KFCq8OAwY+AUi4eZm5J57D31GzjHwfjH9WTeX0MyndmnNB1qV75qQR3b\n2/W5sGHRv+9AarggJkF+ptUkXoLtVA51wcfYm6hILptpde5FQC8RWY1YrswBWAEZ\nNfyrR4JeSweElNHg4NVOs4TwGjOPwWGqzTfgTlECAwEAATANBgkqhkiG9w0BAQsF\nAAOCAQEAAYRlYflSXAWoZpFfwNiCQVE5d9zZ0DPzNdWhAybXcTyMf0z5mDf6FWBW\n5Gyoi9u3EMEDnzLcJNkwJAAc39Apa4I2/tml+Jy29dk8bTyX6m93ngmCgdLh5Za4\nkhuU3AM3L63g7VexCuO7kwkjh/+LqdcIXsVGO6XDfu2QOs1Xpe9zIzLpwm/RNYeX\nUjbSj5ce/jekpAw7qyVVL4xOyh8AtUW1ek3wIw1MJvEgEPt0d16oshWJpoS1OT8L\nr/22SvYEo3EmSGdTVGgk3x3s+A0qWAqTcyjr7Q4s/GKYRFfomGwz0TZ4Iw1ZN99M\nm0eo2USlSRTVl7QHRTuiuSThHpLKQQ==" idpEntityId: "https://saml.example.com/entityid" postBindingLogout: 'true' nameIDPolicyFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" postBindingResponse: 'true' principalAttribute: "username" metadataDescriptorUrl: "https://mocksaml.com/api/saml/metadata" entityId: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps" backchannelSupported: 'true' signatureAlgorithm: RSA_SHA256 xmlSigKeyInfoKeyNameTransformer: KEY_ID loginHint: 'true' authnContextComparisonType: exact postBindingAuthnRequest: 'true' syncMode: FORCE singleSignOnServiceUrl: "https://mocksaml.com/api/saml/sso" wantAuthnRequestsSigned: 'true' addExtensionsElementWithKeyInfo: 'false' principalType: SUBJECT - alias: idp-simplesaml displayName: "Simple SAML Login" providerId: saml enabled: true updateProfileFirstLoginMode: 'on' trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" config: # TODO fixme validateSignature: 'false' hideOnLoginPage: false guiOrder: "2200" # signingCertificate: "MIICmj....qGw==" # encryptionPublicKey: "MIICmj....qHqGw==" idpEntityId: "http://samlidp.acme.test:18380/simplesaml/saml2/idp/metadata.php" postBindingLogout: 'true' nameIDPolicyFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" postBindingResponse: 'true' principalAttribute: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" principalType: ATTRIBUTE metadataDescriptorUrl: "http://samlidp.acme.test:18380/simplesaml/saml2/idp/metadata.php" entityId: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps" backchannelSupported: 'true' signatureAlgorithm: RSA_SHA256 xmlSigKeyInfoKeyNameTransformer: KEY_ID loginHint: 'true' authnContextComparisonType: exact postBindingAuthnRequest: 'true' syncMode: FORCE singleSignOnServiceUrl: "http://samlidp.acme.test:18380/simplesaml/saml2/idp/SSOService.php" singleLogoutServiceUrl: "http://samlidp.acme.test:18380/simplesaml/saml2/idp/SingleLogoutService.php" wantAuthnRequestsSigned: 'true' addExtensionsElementWithKeyInfo: 'false' - alias: "idp-acme-ldap" displayName: "Acme LDAP Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: hideOnLoginPage: true guiOrder: "3000" issuer: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap" tokenUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/token" jwksUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/certs" userInfoUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/userinfo" authorizationUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/auth" logoutUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-ldap/protocol/openid-connect/logout" clientId: "acme_ldap_idp_broker" clientSecret: "$(env:ACME_APPS_LDAP_IDP_BROKER_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceMethod: "S256" pkceEnabled: "true" - alias: "idp-acme-azuread" displayName: "Acme EntraID Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: true addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "4000" issuer: "$(env:ACME_AZURE_AAD_TENANT_URL)/v2.0" tokenUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/token" jwksUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/discovery/v2.0/keys" userInfoUrl: "https://graph.microsoft.com/oidc/userinfo" authorizationUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/authorize" logoutUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/logout" clientId: "$(env:ACME_AZURE_AAD_TENANT_CLIENT_ID:-dummy)" clientSecret: "$(env:ACME_AZURE_AAD_TENANT_CLIENT_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid profile email" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceMethod: "S256" pkceEnabled: "true" - alias: "Google" displayName: "Acme Google Login" providerId: "google" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "5000" syncMode: IMPORT userIp: true clientSecret: dummysecret clientId: dummyclientid useJwksUrl: true - alias: "auth0" displayName: "Acme Auth0 Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false # firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "1000" issuer: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/" tokenUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/oauth/token" jwksUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/.well-known/jwks.json" userInfoUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/userinfo" authorizationUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/authorize" logoutUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/v2/logout" clientId: "$(env:ACME_AUTH0_CLIENT_ID:-dummy)" clientSecret: "$(env:ACME_AUTH0_CLIENT_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid profile email" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceEnabled: "true" pkceMethod: "S256" - alias: "Okta" displayName: "Acme Okta Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: # https://mydomain.okta.com/.well-known/openid-configuration guiOrder: "2000" issuer: "https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com" tokenUrl: "https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/token" jwksUrl: "https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/keys" userInfoUrl: "https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/userinfo" authorizationUrl: "https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/authorize" logoutUrl: "https://$(env:ACME_OKTA_DOMAIN:-dummy).$(env:OKTA_DOMAIN:-okta).com/oauth2/v1/logout" clientId: "$(env:ACME_OKTA_CLIENT_ID:-dummy)" clientSecret: "$(env:ACME_OKTA_CLIENT_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid profile email" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceEnabled: "true" pkceMethod: "S256" - alias: "linkedin" providerId: "linkedin-openid-connect" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # also uses acme-idp-mapper-linkedin-user-importer, see identityProviderMappers config: guiOrder: "6000" syncMode: "FORCE" clientId: "$(env:ACME_LINKEDIN_IDP_CLIENT_ID:-dummy)" clientSecret: "$(env:ACME_LINKEDIN_IDP_CLIENT_SECRET:-dummysecret)" # see https://learn.microsoft.com/en-us/linkedin/shared/references/v2/profile/lite-profile defaultScope: "openid email profile" acceptsPromptNoneForwardFromClient: "false" disableUserInfo: "false" filteredByClaim: "false" hideOnLoginPage: "false" # see also https://www.linkedin.com/oauth/.well-known/openid-configuration # see https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api # profileProjection not supported by new linkedin-openid-connect provider # profileProjection: "(id,firstName,lastName,profilePicture(displayImage~digitalmediaAsset:playableStreams))" identityProviderMappers: - name: lastname-importer identityProviderAlias: idp-acme-saml identityProviderMapper: saml-user-attribute-idp-mapper config: syncMode: FORCE user.attribute: lastName attributes: "[]" attribute.friendly.name: surname - name: firstname-importer identityProviderAlias: idp-acme-saml identityProviderMapper: saml-user-attribute-idp-mapper config: syncMode: FORCE user.attribute: firstName attributes: "[]" attribute.friendly.name: givenName - name: "linkedin-profile-importer" identityProviderAlias: "linkedin" identityProviderMapper: "acme-idp-mapper-linkedin-user-importer" config: syncMode: "FORCE" - name: simplesaml-email-importer identityProviderAlias: idp-simplesaml identityProviderMapper: saml-user-attribute-idp-mapper config: syncMode: FORCE user.attribute: email attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" - name: simplesaml-firstname-importer identityProviderAlias: idp-simplesaml identityProviderMapper: saml-user-attribute-idp-mapper config: syncMode: FORCE user.attribute: firstName attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" - name: simplesaml-lastname-importer identityProviderAlias: idp-simplesaml identityProviderMapper: saml-user-attribute-idp-mapper config: syncMode: FORCE user.attribute: lastName attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" authenticationFlows: ## Custom User Registration Flow - alias: "Custom Registration" description: "Custom User Registration" providerId: "basic-flow" topLevel: true builtIn: false authenticationExecutions: - authenticator: "registration-page-form" requirement: REQUIRED flowAlias: "Custom Registration Forms" autheticatorFlow: true - alias: "Custom Registration Forms" description: "registration form" providerId: "form-flow" topLevel: false builtIn: false authenticationExecutions: - authenticator: "custom-registration-user-creation" requirement: "REQUIRED" userSetupAllowed: false autheticatorFlow: false - authenticator: "acme-friendly-captcha-form-action" requirement: "DISABLED" userSetupAllowed: false autheticatorFlow: false - authenticator: "registration-password-action" requirement: "REQUIRED" userSetupAllowed: false autheticatorFlow: false - alias: "Custom Post Broker Login" description: "Custom Post Broker Login with logging" providerId: "basic-flow" topLevel: true builtIn: false authenticationExecutions: - authenticator: "acme-debug-auth" requirement: "REQUIRED" authenticatorConfig: "acme-debug-auth-post-broker" userSetupAllowed: true authenticatorConfig: - alias: "acme-debug-auth-post-broker" config: {} # deploy123 ================================================ FILE: config/stage/dev/realms/acme-auth0.yaml ================================================ realm: acme-auth0 enabled: true displayName: Acme Auth0 displayNameHtml: Acme Auth0 loginWithEmailAllowed: true loginTheme: internal internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" resetPasswordAllowed: true #accountTheme: keycloak.v2 #adminTheme: keycloak #emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) identityProviders: - alias: "auth0" displayName: "Acme Auth0 Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "1000" issuer: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/" tokenUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/oauth/token" jwksUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/.well-known/jwks.json" userInfoUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/userinfo" authorizationUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/authorize" logoutUrl: "https://$(env:ACME_AUTH0_DOMAIN:-dummy.eu).auth0.com/v2/logout" clientId: "$(env:ACME_AUTH0_CLIENT_ID:-dummy)" clientSecret: "$(env:ACME_AUTH0_CLIENT_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid profile email" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceEnabled: "true" pkceMethod: "S256" browserFlow: "browser-auth0" authenticationFlows: - alias: "browser-auth0" description: "Custom browser flow with Auth0 as default IdP" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - authenticator: identity-provider-redirector requirement: ALTERNATIVE authenticatorConfig: "default-to-auth0" authenticatorConfig: - alias: "default-to-auth0" config: defaultProvider: "auth0" ================================================ FILE: config/stage/dev/realms/acme-client-examples.yaml ================================================ realm: acme-client-examples enabled: true clients: - clientId: acme-client-spa-app protocol: openid-connect name: Acme SPA Frontend App description: "JavaScript based Single-Page App as Public Client that uses Authorization Code Grant Flow" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false fullScopeAllowed: false rootUrl: "https://www.keycloak.org/app" baseUrl: "/#url=https://id.acme.test:8443/auth&realm=acme-client-examples&client=acme-client-spa-app" adminUrl: "" redirectUris: - "/*" - "https://flowsimulator.pragmaticwebsecurity.com" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: acme-client-cli-app protocol: openid-connect name: Acme CLI App description: "Command-line interface app that can obtain tokens with Username + Password and ClientId via grant_type=password" enabled: true publicClient: true standardFlowEnabled: false directAccessGrantsEnabled: true serviceAccountsEnabled: false defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "use.refresh.tokens": "false" - clientId: acme-client-classic-web-app protocol: openid-connect name: Acme Classic Server-side Web Application description: "Classic Server-side Web Application that uses Authorization Code Grant Flow" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false secret: "$(env:ACME_APPS_APP_WEB_SPRINGBOOT_SECRET:-secret)" fullScopeAllowed: false rootUrl: "https://apps.acme.test:4633/webapp" baseUrl: "/" adminUrl: "" redirectUris: - "/*" - "https://flowsimulator.pragmaticwebsecurity.com" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: acme-client-legacy-app protocol: openid-connect name: Acme Legacy App description: "Legacy App that can obtain tokens with Username + Password and ClientId+Secret via grant_type=password" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: true serviceAccountsEnabled: false secret: "$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: acme-client-api-resource-server protocol: openid-connect name: Acme API Resource Server description: "OAuth2 Resource Server that can be called with an AccessToken, can be used to provide Role configuration for an API" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false - clientId: acme-client-api-resource-server2 protocol: openid-connect name: Acme API Resource Server 2 description: "OAuth2 Resource Server that can be called with an AccessToken, can be used to provide Role configuration for an API" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false - clientId: acme-client-service-app protocol: openid-connect name: Acme Service App description: "Acme Service App that can obtain tokens via grant_type=client_credentials" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true secret: "$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: acme-client-mobile-app protocol: openid-connect name: Acme Mobile App description: "Acme Mobile App with Authorization Code Flow" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false redirectUris: # App URL - "acme://app/callback/*" # Claimed URL - "https://mobile.acme.test/*" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - "offline_access" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: acme-client-desktop-app protocol: openid-connect name: Acme Desktop App description: "Acme Desktop App with Authorization Code Flow" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false fullScopeAllowed: false redirectUris: - "http://localhost/*" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - "offline_access" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: acme-client-idp-broker-oidc protocol: openid-connect name: Acme OIDC IDP Broker description: "Client for OpenID Connect Identity Provider" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false fullScopeAllowed: false secret: "$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)" redirectUris: - "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-internal/endpoint/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: acme-client-saml-webapp name: "Acme Classical Web App SAML" description: "Classical Web App which use SAML for SSO" rootUrl: 'https://apps.acme.test:4723' adminUrl: "https://apps.acme.test:4723/saml" baseUrl: "/" surrogateAuthRequired: false enabled: true alwaysDisplayInConsole: false clientAuthenticatorType: client-secret redirectUris: - "/saml/consume" notBefore: 0 bearerOnly: false consentRequired: false standardFlowEnabled: true implicitFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false publicClient: false frontchannelLogout: true protocol: saml attributes: "saml.force.post.binding": "true" "saml.multivalued.roles": "false" "frontchannel.logout.session.required": "false" "saml.server.signature.keyinfo.ext": "false" # TODO externalize saml certificate saml.signing.certificate: "MIIEFTCCAn2gAwIBAgIQCSD0cM/czR4Rb3t/5MesJTANBgkqhkiG9w0BAQsFADBTMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFDASBgNVBAsMC3RvbUBuZXVtYW5uMRswGQYDVQQDDBJta2NlcnQgdG9tQG5ldW1hbm4wHhcNMjIwMTE5MTA1NDIyWhcNMzIwMTE5MTA1NDIyWjA/MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxFDASBgNVBAsMC3RvbUBuZXVtYW5uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtWoSr1BEpMy64BsaVGXyKCpz1qtoqX4lC0LPghE2xz+N88HJIAB60//pmkoYi++Yg7EvaN6/R6saFWQ4l3KTrAKdqZnBc5ylFwMDk5aeNdqnycnMq4tYIIZ5Kd5ATHexsQ5BI5MgDrGa65uR8yU/D8FSTEhEXBMUOCPTmaz/eDccHnmx2wa17zDoBabeCpUnRGzuvX+YtyYi7VlAq9cf1PHjr7YQeXpocwZSifOJn0FrNItHfTeGrYZbcCe9IUY07T2g3Jd2kbgra8V5XFau0cxGqQYq4IwqpuIiVKas7xs6Y5mmLMz1slfSavmGwJOXg+U9ZqpquI4kyc2jHN6kvQIDAQABo3kwdzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBTvbKPnxrx5ICZChpVluUL4O+etgDAhBgNVHREEGjAYgglhY21lLnRlc3SCCyouYWNtZS50ZXN0MA0GCSqGSIb3DQEBCwUAA4IBgQBjYKjLhBfbovY1bJrn7KJ1cirOUQ7PZpHBCxWUy4pkQYveGYLNJbEMdFV7zfMPlNrusz5Roqporz09sX3L6T1PAYkXBEDLIYJArB1nUjrAt36oNvY5wO9wgnJUJlMO3rxVsk3vivG1zf8PPXAq/U6fF+lHbm9VNxhGn9HMQb7ahNVW7S980ioRvZta1vrBX56ItY5WLOTnb9hcrXmN+Y8c9sh6vUbxGcXpXhMC5l55aCgLCxOS/qJAT/G7sBHoPKNk4h33T0wehkCCLjO19QvBQI2eCUAWJnAE7Hv8L22IuoSpLUDfPoN9nq4gMcqyyAq7WqS4MAMwZjLAM7IMS17wQX/B0pzBZPgE5/ankTEhDH710Ct5of3bizIEd/oOSwBqotIVOVx1XX5NzEI3HCIeYj8TAIlp1k32yYqqJTOkD7dFgKIY8VvZqJXU8iQ/dVSbaHodmuyAlabjeXcXPABFCr/5Vc+TpngiZT8tDfY1hPUKJmKSl+gp71Y1WYDWKH0=" "backchannel.logout.session.required": "false" "saml.signature.algorithm": "RSA_SHA256" "saml.client.signature": "true" "saml.allow.ecp.flow": "false" "saml.assertion.signature": "false" "saml.encrypt": "false" "saml.server.signature": "true" "saml.artifact.binding.identifier": "HC5ftXuLr0FUnJUosZarVSvzZh0=" "saml.artifact.binding": "false" "saml_force_name_id_format": "false" "saml.authnstatement": "true" "display.on.consent.screen": "false" "saml_name_id_format": "username" "saml.onetimeuse.condition": "false" "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#" authenticationFlowBindingOverrides: {} fullScopeAllowed: false nodeReRegistrationTimeout: -1 protocolMappers: - name: "acme-saml-email" protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: email friendly.name: email attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" - name: "acme-saml-lastname" protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: lastName friendly.name: surname attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" - name: "acme-saml-firstname" protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: firstName friendly.name: givenName attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" defaultClientScopes: [] optionalClientScopes: [] - clientId: acme-api-gateway protocol: openid-connect name: Acme API Gateway description: "Acme API Gateway can translate API keys to access tokens via token-exchange" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true secret: "$(env:ACME_CLIENT_EXAMPLES_CLIENT_SECRET:-secret)" defaultClientScopes: - "basic" - "email" - "profile" - "roles" optionalClientScopes: - "phone" users: - username: employee email: employee@local firstName: Erik lastName: Employee enabled: true attributes: locale: ["en"] credentials: - type: password userLabel: initial value: test temporary: false - username: tester email: tester@local firstName: Theo lastName: Tester enabled: true attributes: locale: ["en"] phoneNumber: ["+49178111222333"] phoneNumberVerified: ["true"] credentials: - type: password userLabel: initial value: test temporary: false - username: api-user-42 enabled: true realmRoles: [] clientRoles: account: [] credentials: - type: password userLabel: initial value: "7FTt1Q0PG5yv3YqaZGhEB19KIollpNFurA" temporary: false ================================================ FILE: config/stage/dev/realms/acme-demo.yaml ================================================ realm: acme-demo enabled: true displayName: Acme Demo displayNameHtml: Acme Demo loginWithEmailAllowed: true loginTheme: internal resetPasswordAllowed: true #accountTheme: keycloak.v2 #adminTheme: keycloak #emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) clients: - clientId: app-minispa protocol: openid-connect name: Acme Account Console description: "Acme Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-demo&show=profile,settings,apps,security,logout,token,idToken,revoke" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "roles" - "profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - id: acme-standard-client-1 clientId: acme-standard-client protocol: openid-connect name: Client with Standard Flow description: "Standard Client Description" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false secret: acme-standard-client-1-secret fullScopeAllowed: false redirectUris: - "http://localhost/acme-standard-client/login*" - "https://flowsimulator.pragmaticwebsecurity.com" webOrigins: - "+" attributes: # "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - id: acme-public-client-1 clientId: acme-public-client-1 protocol: openid-connect name: Client with Standard Flow description: "Public Client Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false fullScopeAllowed: false redirectUris: - "http://localhost/acme-public-client/login*" - "https://flowsimulator.pragmaticwebsecurity.com" webOrigins: - "+" attributes: # "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - id: acme-implicit-client-1 clientId: acme-implicit-client protocol: openid-connect name: Client with Implicit Grant description: "Implicit Client Description v2" enabled: true publicClient: true standardFlowEnabled: true implicitFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false fullScopeAllowed: false redirectUris: - "http://localhost/acme-implcit-client/oauth/callback" - id: acme-direct-access-client-1 clientId: acme-direct-access-client protocol: openid-connect name: Standard Client with ROPC Grant description: "Direct Access Grant Client Description" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: true serviceAccountsEnabled: false secret: acme-direct-access-client-1-secret fullScopeAllowed: false redirectUris: - "http://localhost/acme-direct-access-client-1/login*" - "https://flowsimulator.pragmaticwebsecurity.com/*" - id: acme-service-client-1 clientId: acme-service-client protocol: openid-connect name: Standard Client with Client Credentials Grant description: "Service Client Description" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true secret: acme-service-client-1-secret fullScopeAllowed: false - id: acme-device-client clientId: acme-device-client protocol: openid-connect name: Client with Device Flow Grant description: "Device Flow Grant Description" enabled: true publicClient: true standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false fullScopeAllowed: false attributes: #"use.refresh.tokens": "false" "use.refresh.tokens": "true" "oauth2.device.authorization.grant.enabled": "true" redirectUris: [] users: - username: tester email: test@local.test firstName: Theo lastName: Tester enabled: true attributes: locale: [ "de" ] credentials: - type: password userLabel: initial value: test temporary: false ================================================ FILE: config/stage/dev/realms/acme-internal.yaml ================================================ realm: acme-internal enabled: true displayName: Acme Internal displayNameHtml: "Acme Internal" loginWithEmailAllowed: true internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" resetPasswordAllowed: true loginTheme: "internal-modern" accountTheme: "internal-modern" #adminTheme: keycloak emailTheme: "internal-modern" sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) browserFlow: "Browser Identity First" browserSecurityHeaders: contentSecurityPolicyReportOnly: "" xContentTypeOptions: "nosniff" xRobotsTag: "none" xFrameOptions: "SAMEORIGIN" contentSecurityPolicy: "frame-src 'self'; frame-ancestors 'self'; object-src 'none';" xXSSProtection: "1; mode=block" strictTransportSecurity: "max-age=31536000; includeSubDomains" # update 123 # Custom realm attributes attributes: # for http variant: http://apps.acme.test:4000 "acme_site_url": "https://apps.acme.test:4443" "acme_terms_url": "https://apps.acme.test:4443/site/terms.html" "acme_imprint_url": "https://apps.acme.test:4443/site/imprint.html" "acme_privacy_url": "https://apps.acme.test:4443/site/privacy.html" #"acme_logo_url": "no example, should be taken from client or null" "acme_account_deleted_url": "https://apps.acme.test:4443/site/accountdeleted.html" "acme_greeting": "Hello" "acme_opa_chk_contextAttributes": "remoteAddress" # Bruteforce Protection bruteForceProtected: true permanentLockout: false maxFailureWaitSeconds: 900 minimumQuickLoginWaitSeconds: 60 waitIncrementSeconds: 60 quickLoginCheckMilliSeconds: 1000 maxDeltaTimeSeconds: 43200 failureFactor: 10 eventsListeners: - "jboss-logging" - "email" - "acme-audit-listener" eventsEnabled: true # 120 days eventsExpiration: 10368000 # enabledEventTypes: [ "SEND_RESET_PASSWORD", "UPDATE_CONSENT_ERROR", "GRANT_CONSENT", ... ] adminEventsEnabled: false adminEventsDetailsEnabled: false # Note adminEventsExpired is stored as realm attribute # adminEventsExpiration: 3667 smtpServer: replyToDisplayName: "Acme Employee Support" port: 1025 host: mail replyTo: "no-reply@acme.test" from: "acme-internal-sso@acme.test" fromDisplayName: "Acme Employee Account" clientScopes: - name: email description: 'OpenID Connect built-in scope: email' protocol: openid-connect attributes: consent.screen.text: "${emailScopeConsentText}" include.in.token.scope: 'true' display.on.consent.screen: 'true' gui.order: '2000' - name: phone description: 'OpenID Connect built-in scope: phone' protocol: openid-connect attributes: consent.screen.text: "${phoneScopeConsentText}" include.in.token.scope: 'true' display.on.consent.screen: 'true' gui.order: '3000' - name: acme.profile description: Acme Profile Access protocol: openid-connect - name: acme.ageinfo description: Acme Profile AgeInfo protocol: openid-connect attributes: "include.in.token.scope": "true" "display.on.consent.screen": "true" "gui.order": "4000" protocolMappers: - name: "Acme: Audience Resolve" protocol: openid-connect protocolMapper: oidc-audience-resolve-mapper consentRequired: false - name: "Acme: AgeInfo" protocol: openid-connect protocolMapper: oidc-acme-ageinfo-mapper consentRequired: false config: userinfo.token.claim: "true" id.token.claim: "true" access.token.claim: "false" - name: acme.api description: Acme API Access protocol: openid-connect - name: name description: Name Details protocol: openid-connect attributes: "include.in.token.scope": "true" "display.on.consent.screen": "true" "gui.order": "1000" protocolMappers: - name: "Acme: Given Name" protocol: openid-connect protocolMapper: oidc-usermodel-property-mapper config: "user.attribute": "firstName" "claim.name": "given_name" "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" - name: "Acme: Family Name" protocol: openid-connect protocolMapper: oidc-usermodel-property-mapper config: "user.attribute": "lastName" "claim.name": "family_name" "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" - name: "Acme: Display Name" protocol: openid-connect protocolMapper: oidc-full-name-mapper config: "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" - name: "Acme: Username" protocol: openid-connect protocolMapper: oidc-usermodel-property-mapper config: "user.attribute": "username" "claim.name": "preferred_username" "userinfo.token.claim": "true" "id.token.claim": "true" "access.token.claim": "true" requiredActions: - alias: acme-update-phonenumber name: 'Acme: Update Mobile Phonenumber' providerId: acme-update-phonenumber enabled: true defaultAction: false priority: 1100 - alias: acme-manage-trusted-device name: 'Acme: Manage Trusted Device' providerId: acme-manage-trusted-device enabled: true defaultAction: false priority: 1200 - alias: acme-register-email-code name: 'Acme: Register MFA via E-Mail code' providerId: acme-register-email-code enabled: true defaultAction: false priority: 1300 - alias: acme-update-email name: 'Acme: Update Email' providerId: acme-update-email enabled: true defaultAction: false priority: 1400 - alias: CONFIGURE_RECOVERY_AUTHN_CODES name: 'Recovery Authentication Codes' providerId: CONFIGURE_RECOVERY_AUTHN_CODES enabled: true defaultAction: false priority: 1500 - alias: acme-context-selection-action name: 'Acme: User Context Selection' providerId: acme-context-selection-action enabled: false defaultAction: false priority: 1600 clients: - clientId: app-minispa protocol: openid-connect name: Acme Account Console description: "Acme Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-internal&show=profile,settings,apps,security,logout" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "roles" - "profile" - "acme.profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: app-greetme protocol: openid-connect name: Acme Greet Me description: "App Greet Me Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "$(env:APPS_FRONTEND_URL_GREETME)" baseUrl: "/?realm=acme-internal&scope=openid" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" optionalClientScopes: - "phone" - "name" - "acme.api" - "address" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: app-consent-demo protocol: openid-connect name: Acme Consent Demo description: "App Consent Demo Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "$(env:APPS_FRONTEND_URL_GREETME)" baseUrl: "/?realm=acme-internal&client_id=app-consent-demo&scope=openid" adminUrl: "" redirectUris: - "/*" - "http://localhost:4000/acme-greetme/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" optionalClientScopes: - "phone" - "name" - "acme.api" - "address" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" - clientId: app-mobile protocol: openid-connect name: App Mobile description: "App Mobile Description" enabled: true publicClient: true standardFlowEnabled: true # using directAccessGrantsEnabled just for demo purposes directAccessGrantsEnabled: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false redirectUris: - "acme://app/callback/*" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - "offline_access" - clientId: acme_internal_idp_broker protocol: openid-connect name: Acme Internal Broker description: "Acme Internal IdP Broker Client" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false fullScopeAllowed: false secret: "$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)" redirectUris: - "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-internal/endpoint/*" - "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-apps/broker/idp-acme-internal/endpoint/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "post.logout.redirect.uris": "+" - clientId: app-oauth2-proxy protocol: openid-connect name: Acme OAuth2 Proxy App description: "Acme App behind Oauth2 Proxy" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false fullScopeAllowed: true secret: "secret" rootUrl: "https://apps.acme.test:6443" baseUrl: "/" redirectUris: - "/oauth2/callback/*" - "/" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" - "roles" optionalClientScopes: - "phone" attributes: "post.logout.redirect.uris": "+" - clientId: test-client protocol: openid-connect name: Test Client description: "Test Client Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-internal&client_id=test-client" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - "acme.profile" - "acme.ageinfo" - clientId: test-client-ropc protocol: openid-connect name: Test Client ROPC description: "Test Client ROPC Description" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: true serviceAccountsEnabled: true secret: "$(env:ACME_APPS_INTERNAL_IDP_BROKER_SECRET:-secret)" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - "acme.profile" - "acme.ageinfo" - clientId: app-keycloak-website protocol: openid-connect name: Keycloak Website App description: "Keycloak Website App Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "https://www.keycloak.org/app" baseUrl: "/?url=https://id.acme.test:8443/auth&realm=acme-internal&client=app-keycloak-website" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "post.logout.redirect.uris": "+" - clientId: app-demo-service protocol: openid-connect name: Demo Service description: "Demo Service Description" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true secret: "$(env:ACME_APPS_DEMO_SERVICE_SECRET:-secret)" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: frontend-webapp-springboot protocol: openid-connect name: Acme Web App Spring Boot description: "Web App Spring Boot Description" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false secret: "$(env:ACME_APPS_APP_WEB_SPRINGBOOT_SECRET:-secret)" fullScopeAllowed: false rootUrl: "https://apps.acme.test:4633/webapp" baseUrl: "/" adminUrl: "" redirectUris: - "/" - "/login/oauth2/code/keycloak" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "post.logout.redirect.uris": "+" - clientId: acme-bff-springboot protocol: openid-connect name: Acme BFF App Spring Boot description: "Acme BFF Spring Boot Description" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false secret: "$(env:ACME_APPS_APP_BFF_SPRINGBOOT_SECRET:-secret)" fullScopeAllowed: false rootUrl: "https://apps.acme.test:4693/bff" baseUrl: "/" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: acme-service-client-jwt-auth protocol: openid-connect name: Acme Service JWT Auth Client description: "Acme Service JWT Auth Client Description" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true clientAuthenticatorType: "client-jwt" attributes: "use.refresh.tokens": "false" # Note that this certificate must match apps/jwt-client-authentication/client_cert.pem "jwt.credential.certificate": "MIIFLzCCAxegAwIBAgIUDYnNAzTFZ6FNHbRn4evtZFluiuAwDQYJKoZIhvcNAQELBQAwJzElMCMGA1UEAwwcYWNtZS1zZXJ2aWNlLWNsaWVudC1qd3QtYXV0aDAeFw0yMjA0MTAxMTU5MjdaFw0yMzA0MTAxMTU5MjdaMCcxJTAjBgNVBAMMHGFjbWUtc2VydmljZS1jbGllbnQtand0LWF1dGgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCu91jwgkO/ZJoeIzclL+x7oBxsgjxGDLdCYs4QKEhitB8FFOobxtA67Yh48Bprd9Qji9IHvdxVLPa+SkgMvF/5MhDKUW/wLBpDL0sgvLT56FV9gl/hqtUvVKVNTQuEBk0QpTJH4uSg5jNIZARVcOnPRvBtQ0vpQ/1FsEJkSLXq7OAUQTgguXltR+/u2hH4wpOIRzpjeN7i/OHkiZS0K5otAVr1kCoMeZXo0qUfzG6FngVQhqZ3haMf3C+MSjLagvFzYK6NlJJDp7WIFvzkCriZlIyrSbO4vpHjmKuo31boIAbwn1EimHWGG1ExcXWLeqLfJt43tQIwzPe6PhnQ+2kw7zb6O6SrJhnaYNTVclAuj3TRtF0bxM9B9kEpHyypxGKwF8UAyqQkMnG1tJzvafruDLHrHpOAnV4bncWjbIP5vsS+mEALIs+CpRujdNut1NrJT7rMkNcXdhOfiLVA5FY91c5WC1uoJPmyFscsj2WmBe5AVQFpOiS0V5dIm1Gc3ctebX7Nqa8rxnEPVQhRWyacadlti+VNHgUUZIbU67vHx64w+Gh3XlxG2fBqf1YHHOOUlFHUAcJoANqyk0lD6eqn8RSpI0xB/VdQNnCmDblx94W0iTCxgEC3vrUsHzC3r37BX6dHo3aF7xaT6wwRCM8/FHj1eH/JBq+1Xt4y1P3aNQIDAQABo1MwUTAdBgNVHQ4EFgQUt+sggUSV+8cCNAvMMlBH1SBRAA0wHwYDVR0jBBgwFoAUt+sggUSV+8cCNAvMMlBH1SBRAA0wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAPsthqWnfJFEjh7BV/Vg8hAgLme/cVDr/O1Ff0WVX+Mmtsu+vZyRGNXL67PXYqar9b+fZr5mGhbiZS4mBk/5N9sGRpDh2Keyy9j/CTaYUpszqur5NfYBvmtbk+u4BCPkMMme+R2RnZ3GnzOoGKlZ2jCfBvm/Jr+Ta05VVGb41LZfb19m8uYnMzLTZd1VSZQ4+vHa6OrPn1zaAPobHWLysLkUH6ngteZjVu2bPcf1UYmYRyMs/a4cIgsC3A5PIFu9DN87o0q8LN3qITt70/3FgKLUuPmvCZSnNZ+oLsUDs3RM5qyJth91UAaJL3LXglkSCzwmSqimel+Ga3e4ltmsUMB2HKc81NXfMmAV3gRYyfwLQSA1HyuMGOBRLbmsXr6Y6C4l3Wfsbv/MLL/BgDev6A6Hkq2Zc0Q4VJUhBsm75GMAy5vA6Pn+TSp8MeWJMAXJwcY/3ESyodfn29m0mGiHwef/ByQcK/8M9fRFIvIsKF/NPziWHiIV3QXB2qDIZF595/De4siUtaSW6H9ywZpVfTBoc/AEHKY4QZ1Pr8xj+YRQar7qmuVsSug6RWgXplZWOn9vx25FPwAXcOv9z/gP7zA8IYMiEUZ0MKFLetQ6ietCNPlvqgFC59+hjsh/MyNTePitxNPN1+MYUb7Xr/Ul+BREnNCVJM/ZCEbIhsVKMiCk=" "token.endpoint.auth.signing.alg": "RS256" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: acme-webapp-saml-node-express name: Acme Web App SAML Node Express rootUrl: 'https://apps.acme.test:4723' # Replace with relative URL once https://github.com/keycloak/keycloak/issues/28832 is fixed adminUrl: "https://apps.acme.test:4723/saml" baseUrl: "/app" surrogateAuthRequired: false enabled: true alwaysDisplayInConsole: false clientAuthenticatorType: client-secret redirectUris: - "/saml/consume" notBefore: 0 bearerOnly: false consentRequired: false standardFlowEnabled: true implicitFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false publicClient: false frontchannelLogout: true protocol: saml attributes: "saml.force.post.binding": "true" "saml.multivalued.roles": "false" "frontchannel.logout.session.required": "false" "saml.server.signature.keyinfo.ext": "false" # TODO externalize saml private key "saml.signing.private.key": "MIIEpAIBAAKCAQEApYRVMirLeyzT5XW/A9jjr3Vn2yCJw2wnOzs3c/CFJ94LuZa95I09stWPh21FT2NuFQsPxKwcbAr5efkk1E4Ym5FXbFOqM6oE3J6+N609Af7wQam8eBGjs+jhq+l7tsZVvLL0xlXC5ek6NIAJqlCaAbT17Y1cSS5owfd9kAq779+z6jGkTKRJGA6bgvpcBYKz8WrIRwdhCxupbGiDOm10MfDWKBbvI+z5VL/PC20lc0+EWz+Wg9hUEWV67kjyvYKITWn+0n3UF/EjiGY7xJzH02CjAG5IHCYMQ31xEX7k9BkmCZywDjL/HCIwRgnSZTKE58yK7u6O9W3++zWEFEvtPwIDAQABAoIBAASgRLm3H+KZV6WXqDDFPJcR7hOq5Quc6u6V+QN/PiSyaSh7ccl0BLy9VedTYtCiHX8IEdCnSmoSPab7k1xw5fFfKA7c7cP6ftIXVVPjPliSbzuel1s4JWR1xoaFnl525gmOnnz2nUP/KnaYlFL5/e8jR1mODf2O08241/3d8IldPAlb0aA8IGOjqX0gtzLI6CxWwXLuEM0XMgqQidrR6cilS4qogvzSBpHwyxyuVAuDflfXpewN7kszWxvjVUGWxn3ia+Vtzu0tmfQ9NZRjSiJLMAalXPImRGtrHfnLRDLuRWXROdPtj4wHPvBuWoAavHSob2cmnilCAyh+TTXIVQkCgYEA0xZhwdq0ndL0wIQo3yuEpMJtJBrFVESERduaL+K0YUffzzo85Fei+66LdbQF0SNxQIzV+U3bu3p7dgE5s4WSTIf9qAdMNQa0Snsc78LfYhYP13zh2wgkxyemk7zVBIGUmPPeSkDXUgoG7E0nNWJxhMBEkljTAAkFksSxy4aiElcCgYEAyLvLG1y2ksZNgdRRAHpv6hWpfjOtYcU8dL9ybhlErjVYPKx20cyNfyR0obL/TTv1/QMsavPYLn4dFicDNRUwf4v+XSjnIpH7RH6RUCTk3ONT2l2iCMUfQMOhj4Kjlqdr5LBNT5Karwr6zblH8xTVd5hKnxlzt8BC2k1CXkUiu1kCgYAdapgS+NSEzfo3vfMoLptcjo/BIU3wkV/RkGnrVG+Iwwhoi5gixie7ZTagH4dT/tlwgm/rPzNo7Ae6iS8uWmXp7mWl/eZb2WRUoNWGgCS7OZHZmNisunTNoDPxkLYq25gGvK10sZaQIz+VvKbDJMXnFxg3QNOexKMXMfwI/ekmHwKBgQCQbNWAtV9DEVyYydsR/gXhpX5Sp/nae5+43DoHzzRkJ0t6NBg1cPhpfyBPa0tXFYoyZYMi3JkxMlnZI26iVcGUM3RrMM/ERsZDjNEembz01LbzSSUZLEMFRPxMFhF/hqwRWWv2kaOrx7mWJPYIhnfkWXVvLU/d6H3xNV9IFnQb2QKBgQCFahKUCj+aBSsIPX0e6Ew0/6qmrBfAY4XHLG2WTXiU6KGKrY5zwJMLz3GkKrnrnjCarf6S2fbw5Sb6ozn7Fe9NGqKZpIex2HRmBZTL5BDqEVe+0q5BBx2YMOnohrI56saJYUOvYNPNSx5Ytf9cjHIXZgv8ITc6oM3IOrQzOq649A==" # TODO externalize saml certificate "saml.signing.certificate": "MIICyTCCAbECBgGGmOH16jANBgkqhkiG9w0BAQsFADAoMSYwJAYDVQQDDB1hY21lLXdlYmFwcC1zYW1sLW5vZGUtZXhwcmVzczAeFw0yMzAyMjgxNjM0NTBaFw0zMzAyMjgxNjM2MzBaMCgxJjAkBgNVBAMMHWFjbWUtd2ViYXBwLXNhbWwtbm9kZS1leHByZXNzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApYRVMirLeyzT5XW/A9jjr3Vn2yCJw2wnOzs3c/CFJ94LuZa95I09stWPh21FT2NuFQsPxKwcbAr5efkk1E4Ym5FXbFOqM6oE3J6+N609Af7wQam8eBGjs+jhq+l7tsZVvLL0xlXC5ek6NIAJqlCaAbT17Y1cSS5owfd9kAq779+z6jGkTKRJGA6bgvpcBYKz8WrIRwdhCxupbGiDOm10MfDWKBbvI+z5VL/PC20lc0+EWz+Wg9hUEWV67kjyvYKITWn+0n3UF/EjiGY7xJzH02CjAG5IHCYMQ31xEX7k9BkmCZywDjL/HCIwRgnSZTKE58yK7u6O9W3++zWEFEvtPwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCiRJBOAtO+D/QCgFksKqV9C71y7VM5njyJ2KSRvWYf7krBsyyceN5tNtnbgaGJEcF5gVJ7SJPEUGIydBN8Umt3udNYTnJwBed22zHI4JYAyUP7bZ3bp1wHzkqye3TK0b2/drv4HgsHl1rQbokcGtRMZeBi4pRzoeJbn1E9G1W/JRB+yiCVlquLC6dYs5MrynFC2bOKsDKRgMtd9n9Dlqb+YcwqwYL/6UluxmbSbhWauAXHOWNwEHlEBiAkiJYkG1wBxPLo6oeQ1pfv7X+YDJaWysD/JivLuv+0m3NO6n8RWOgXhdRkTecqC8LlEUUigLJ0Qxbjc+1/wMjd3guV8ToL" "backchannel.logout.session.required": "false" "saml.signature.algorithm": "RSA_SHA256" "saml.client.signature": "true" "saml.allow.ecp.flow": "false" "saml.assertion.signature": "true" "saml.encrypt": "false" "saml.server.signature": "true" "saml.artifact.binding.identifier": "HC5ftXuLr0FUnJUosZarVSvzZh0=" "saml.artifact.binding": "false" "saml_force_name_id_format": "false" "saml.authnstatement": "true" "display.on.consent.screen": "false" "saml_name_id_format": "username" "saml.onetimeuse.condition": "false" "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#" authenticationFlowBindingOverrides: {} fullScopeAllowed: false nodeReRegistrationTimeout: -1 protocolMappers: - name: "acme-saml-email" protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: email friendly.name: email attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" - name: "acme-saml-lastname" protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: lastName friendly.name: surname attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" - name: "acme-saml-firstname" protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: firstName friendly.name: givenName attribute.name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" defaultClientScopes: [] optionalClientScopes: [] authenticationFlows: ## Identity First Browser Login Flow - alias: "Browser Identity First" description: "This flow implements the Identity First pattern" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - authenticator: identity-provider-redirector requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: auth-username-form requirement: REQUIRED - authenticator: auth-password-form requirement: REQUIRED - flowAlias: "2FA Forms" requirement: CONDITIONAL autheticatorFlow: true - authenticator: acme-opa-authenticator #requirement: REQUIRED requirement: DISABLED authenticatorConfig: "acme-opa-auth-default" - alias: "2FA Forms" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: # Only execute 2FA flow if no passkey was used - authenticator: conditional-credential requirement: REQUIRED authenticatorConfig: "no-passkey" - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: acme-auth-trusted-device requirement: ALTERNATIVE - authenticator: acme-auth-otp-form requirement: ALTERNATIVE - authenticator: acme-sms-authenticator requirement: ALTERNATIVE authenticatorConfig: "acme-sms-auth-default" - authenticator: acme-email-code-form requirement: ALTERNATIVE - authenticator: auth-recovery-authn-code-form requirement: ALTERNATIVE authenticatorConfig: - alias: "no-passkey" config: credentials: "webauthn-passwordless" - alias: "acme-sms-auth-default" config: phoneNumberPattern: "\\+49.*" sender: "$realmDisplayName" length: "6" client: "mock" ttl: "300" attempts: "5" useWebOtp: true - alias: "acme-opa-auth-default" config: # realmAttributes: "acme_greeting" authzUrl: "http://acme-opa:8181/v1/data/iam/keycloak/allow" useClientRoles: "true" useRealmRoles: "true" # configure here until https://github.com/keycloak/keycloak/issues/28020 is resolved webAuthnPolicyPasswordlessRpEntityName: "keycloak" webAuthnPolicyPasswordlessSignatureAlgorithms: - "ES256" - "RS256" webAuthnPolicyRpEntityName: "keycloak" webAuthnPolicySignatureAlgorithms: - "ES256" - "RS256" components: "org.keycloak.userprofile.UserProfileProvider": - providerId: "declarative-user-profile" subComponents: { } config: "kc.user.profile.config": - "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" roles: # Realm specific roles realm: - name: "acme-user" description: "Acme User" - name: "acme-admin" description: "Acme Admin" composite: true composites: client: "realm-management": - realm-admin - name: "acme-developer" description: "Acme Developer" - name: "acme-user-support" description: "Acme Support User" composite: true composites: client: "realm-management": - query-groups - view-users - name: "default-roles-acme-internal" description: "${role_default-roles}" composite: true composites: realm: - "offline_access" - "acme-user" client: "account": - "manage-account" - "view-profile" # Client specific roles client: "test-client-ropc": - name: "role4" description: "Role 4 Description" clientRole: true groups: - "name": "Users" # "path": "/Users" "attributes": groupAttribute1: ["groupAttributeValue1"] "realmRoles": - "acme-user" # "clientRoles": {} "subGroups": [] users: - username: employee email: employee@local firstName: Erik lastName: Employee enabled: true attributes: locale: ["de"] title: [""] salutation: ["mr"] credentials: - type: password userLabel: initial value: test temporary: false groups: - "Users" - username: tester email: tester@local firstName: Theo lastName: Tester enabled: true emailVerified: true attributes: locale: ["en"] phoneNumber: ["+49178111222333"] phoneNumberVerified: ["true"] title: [""] salutation: ["mr"] # Thomas picture: ["https://en.gravatar.com/userimage/52342809/a957ac868585f91edf7eb9b7463328b9.jpeg?size=64"] credentials: - type: password userLabel: initial value: test temporary: false groups: - "Users" - username: support email: support@local firstName: Stefan lastName: Support enabled: true attributes: locale: ["de"] title: [""] salutation: ["mr"] credentials: - type: password userLabel: initial value: test temporary: false realmRoles: - "acme-user-support" groups: - "Users" - username: admin email: admin@local firstName: Arno lastName: Admin enabled: true attributes: locale: ["de"] title: [""] salutation: ["mr"] credentials: - type: password userLabel: initial value: test temporary: false realmRoles: - "acme-admin" - username: service-account-app-demo-service enabled: true serviceAccountClientId: app-demo-service clientRoles: realm-management: - view-identity-providers - view-users - username: demo@local email: demo@local firstName: Dora lastName: Demo enabled: true emailVerified: true attributes: locale: ["en"] phoneNumber: ["+49178111222333"] phoneNumberVerified: ["true"] title: [""] salutation: ["ms"] # Thomas picture: ["https://en.gravatar.com/userimage/52342809/a957ac868585f91edf7eb9b7463328b9.jpeg?size=64"] credentials: - type: password userLabel: initial value: test temporary: false groups: - "Users" ================================================ FILE: config/stage/dev/realms/acme-ldap.yaml ================================================ realm: acme-ldap enabled: true displayName: Acme LDAP displayNameHtml: Acme LDAP loginWithEmailAllowed: true loginTheme: internal resetPasswordAllowed: true #accountTheme: keycloak.v2 #adminTheme: keycloak #emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) browserFlow: "Browser Identity First" smtpServer: replyToDisplayName: "Acme Employee Support" port: 1025 host: mail replyTo: "no-reply@acme.test" from: "acme-internal-sso@acme.test" fromDisplayName: "Acme Employee Account" clientScopes: - name: acme.profile description: Acme Profile Access protocol: openid-connect - name: acme.ageinfo description: Acme Profile AgeInfo protocol: openid-connect protocolMappers: - name: "Acme: AgeInfo" protocol: openid-connect protocolMapper: oidc-acme-ageinfo-mapper consentRequired: false config: userinfo.token.claim: "true" id.token.claim: "true" access.token.claim: "false" - name: acme.api description: Acme API Access protocol: openid-connect clients: - clientId: app-minispa protocol: openid-connect name: Acme Account Console description: "Acme Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-ldap" adminUrl: "" redirectUris: - "/*" - "http://localhost:4000/acme-account/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: app-greetme protocol: openid-connect name: Acme Greet Me description: "App Greet Me Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "$(env:APPS_FRONTEND_URL_GREETME)" baseUrl: "/?realm=acme-ldap" adminUrl: "" redirectUris: - "/*" - "http://localhost:4000/acme-greetme/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" - clientId: acme_ldap_idp_broker protocol: openid-connect name: Acme Internal Broker description: "Acme LDAP IdP Broker Client" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false fullScopeAllowed: false secret: "$(env:ACME_APPS_LDAP_IDP_BROKER_SECRET:-secret)" redirectUris: - "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-ldap/endpoint/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" authenticationFlows: ## Identity First Browser Login Flow - alias: "Browser Identity First" description: "This flow implements the Identity First pattern" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - authenticator: identity-provider-redirector requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - requirement: REQUIRED authenticator: auth-username-form - requirement: REQUIRED authenticator: auth-password-form - flowAlias: "2FA Forms" requirement: CONDITIONAL autheticatorFlow: true - alias: "2FA Forms" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: auth-otp-form requirement: REQUIRED components: org.keycloak.storage.UserStorageProvider: - name: Acme LDAP providerId: ldap subComponents: org.keycloak.storage.ldap.mappers.LDAPStorageMapper: - name: "creation date" providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: - createTimestamp is.mandatory.in.ldap: ["false"] read.only: ["true"] always.read.value.from.ldap: ["true"] user.model.attribute: [createTimestamp] - name: "modify date" providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: [modifyTimestamp] is.mandatory.in.ldap: ["false"] read.only: ["true"] always.read.value.from.ldap: ["true"] user.model.attribute: [modifyTimestamp] - name: username providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: [uid] is.mandatory.in.ldap: ["true"] always.read.value.from.ldap: ["false"] read.only: ["true"] user.model.attribute: [username] - name: "first name" providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: [givenName] is.mandatory.in.ldap: ["true"] always.read.value.from.ldap: ["true"] read.only: ["true"] user.model.attribute: ["firstName"] - name: email providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: [ "mail" ] is.mandatory.in.ldap: [ "false" ] read.only: [ "true" ] always.read.value.from.ldap: [ "false" ] user.model.attribute: [ "email" ] - name: "last name" providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: [ "sn" ] is.mandatory.in.ldap: ["true"] always.read.value.from.ldap: ["true"] read.only: ["true"] user.model.attribute: ["lastName"] - name: "phone number" providerId: user-attribute-ldap-mapper subComponents: {} config: ldap.attribute: ["mobile"] is.mandatory.in.ldap: ["false"] attribute.default.value: ["0"] always.read.value.from.ldap: ["true"] read.only: ["true"] user.model.attribute: ["phone_number"] - name: "LDAP Group Mapper" providerId: group-ldap-mapper subComponents: {} config: membership.attribute.type: ["DN"] "group.name.ldap.attribute": ["cn"] "preserve.group.inheritance": ["false"] "membership.user.ldap.attribute": ["uid"] "groups.dn": ["$(env:ACME_LDAP_GROUP_DN:-dc=corp,dc=acme,dc=local)"] mode: ["READ_ONLY"] "user.roles.retrieve.strategy": ["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"] "ignore.missing.groups": ["true"] "membership.ldap.attribute": ["member"] "group.object.classes": ["groupOfNames"] "memberof.ldap.attribute": ["memberOf"] "groups.path": ["/"] "drop.non.existing.groups.during.sync": ["false"] config: enabled: ["true"] pagination: ["true"] fullSyncPeriod: ["-1"] searchScope: ["2"] useTruststoreSpi: ["ldapsOnly"] usersDn: ["$(env:ACME_LDAP_USERS_DN:-dc=corp,dc=acme,dc=local)"] maxLifespan: ["3600000"] connectionPooling: ["true"] cachePolicy: ["NO_CACHE"] priority: ["0"] importEnabled: ["true"] useKerberosForPasswordAuthentication: ["false"] usePasswordModifyExtendedOp: ["true"] "trustEmail": ["false"] userObjectClasses: ["inetOrgPerson, organizationalPerson"] bindDn: ["$(env:LDAP_USER:-ldap_user)"] usernameLDAPAttribute: ["uid"] changedSyncPeriod: ["-1"] bindCredential: ["$(env:LDAP_PASSWORD:-ldap_password)"] rdnLDAPAttribute: ["uid"] vendor: ["other"] editMode: ["READ_ONLY"] uuidLDAPAttribute: ["entryUUID"] connectionUrl: ["$(env:LDAP_URL:-ldap://localhost:389)"] syncRegistrations: ["false"] authType: ["simple"] batchSizeForSync: ["1000"] changedSyncEnabled: ["false"] validatePasswordPolicy: ["false"] ================================================ FILE: config/stage/dev/realms/acme-offline-test.yaml ================================================ realm: acme-offline-test displayName: "Acme Offline" browserFlow: "Custom Browser" authenticationFlows: ## Identity First Browser Login Flow - alias: "Custom Browser" description: "This flow implements a custom browser pattern" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - authenticator: identity-provider-redirector requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: auth-username-password-form requirement: REQUIRED - flowAlias: "2FA Forms" requirement: CONDITIONAL autheticatorFlow: true - alias: "2FA Forms" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: acme-auth-trusted-device requirement: ALTERNATIVE - authenticator: acme-auth-otp-form requirement: ALTERNATIVE - authenticator: acme-email-code-form requirement: ALTERNATIVE - authenticator: auth-recovery-authn-code-form requirement: ALTERNATIVE ================================================ FILE: config/stage/dev/realms/acme-ops.yaml ================================================ realm: acme-ops enabled: true displayName: Acme Operations displayNameHtml: Acme Operations loginWithEmailAllowed: true loginTheme: internal internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" resetPasswordAllowed: true #accountTheme: keycloak.v2 #adminTheme: keycloak #emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) roles: client: acme-ops-grafana: - name: "Viewer" description: "Allowed to read grafana" composite: false clientRole: true - name: "Editor" description: "Allowed to edit grafana" composite: false clientRole: true - name: "Admin" description: "Allowed to administrate grafana" composite: false clientRole: true clients: - clientId: acme-ops-grafana protocol: openid-connect name: Client for grafana description: "Secure grafana" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false secret: acme-ops-grafana-secret fullScopeAllowed: false defaultClientScopes: - "basic" - "email" rootUrl: "https://ops.acme.test:3000/grafana" baseUrl: "/" adminUrl: "" redirectUris: - "/login*" # grafana uses jmespath to access the role resource. it cannot handle '-' as part of the client id protocolMappers: - name: client roles protocol: openid-connect protocolMapper: oidc-usermodel-client-role-mapper consentRequired: false config: access.token.claim: 'true' id.token.claim: 'false' userinfo.token.claim: 'true' claim.name: resource_access.grafana.roles jsonType.label: String multivalued: 'true' users: - username: devops email: devops@acme.test firstName: Adele lastName: Admina enabled: true credentials: - type: password userLabel: initial value: test temporary: false clientRoles: acme-ops-grafana: - "Admin" ================================================ FILE: config/stage/dev/realms/acme-passwordless.yaml ================================================ realm: acme-passwordless enabled: true displayName: Acme Passwordless displayNameHtml: Acme Passwordless loginWithEmailAllowed: true internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" resetPasswordAllowed: true loginTheme: "internal-modern" accountTheme: "internal-modern" #adminTheme: keycloak emailTheme: "internal-modern" sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) browserFlow: "Browser ID 1st Passwordless" # Bruteforce Protection bruteForceProtected: true permanentLockout: false maxFailureWaitSeconds: 900 minimumQuickLoginWaitSeconds: 60 waitIncrementSeconds: 60 quickLoginCheckMilliSeconds: 1000 maxDeltaTimeSeconds: 43200 failureFactor: 10 eventsListeners: - "jboss-logging" eventsEnabled: true # 120 days eventsExpiration: 10368000 # enabledEventTypes: [ "SEND_RESET_PASSWORD", "UPDATE_CONSENT_ERROR", "GRANT_CONSENT", ... ] adminEventsEnabled: false adminEventsDetailsEnabled: false smtpServer: replyToDisplayName: "Acme Employee Support" port: 1025 host: mail replyTo: "no-reply@acme.test" from: "acme-internal-sso@acme.test" fromDisplayName: "Acme Employee Account" requiredActions: - alias: CONFIGURE_RECOVERY_AUTHN_CODES name: 'Recovery Authentication Codes' providerId: CONFIGURE_RECOVERY_AUTHN_CODES enabled: true defaultAction: false priority: 1500 - alias: webauthn-register-passwordless name: 'Webauthn Register Passwordless' providerId: webauthn-register-passwordless enabled: true defaultAction: false priority: 1501 clients: - clientId: app-minispa protocol: openid-connect name: Acme Account Console description: "Acme Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-passwordless&show=profile,settings,apps,security,logout&scope=openid+profile+email+roles" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "roles" - "profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" - clientId: app-keycloak-website protocol: openid-connect name: Keycloak Demo App description: "Keycloak Demo App Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: false rootUrl: "https://www.keycloak.org/app" baseUrl: "/#url=https://id.acme.test:8443/auth&realm=acme-passwordless&client=app-keycloak-website" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" authenticationFlows: ## Identity First Browser Login Flow - alias: "Browser ID 1st Passwordless" description: "This flow implements the Identity First pattern with Passwordless Auth" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: auth-username-form requirement: REQUIRED - flowAlias: "Passwordless or 2FA Forms" requirement: REQUIRED autheticatorFlow: true - alias: "Passwordless or 2FA Forms" description: "Sub-Flow to ask user for Passwordless Auth or Password with 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: webauthn-authenticator-passwordless requirement: ALTERNATIVE - flowAlias: "Password with 2FA" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Password with 2FA" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: auth-password-form requirement: REQUIRED - flowAlias: "2FA Forms" requirement: CONDITIONAL autheticatorFlow: true - alias: "2FA Forms" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: webauthn-authenticator requirement: ALTERNATIVE - authenticator: acme-auth-otp-form requirement: ALTERNATIVE - authenticator: acme-sms-authenticator requirement: ALTERNATIVE - authenticator: auth-recovery-authn-code-form requirement: ALTERNATIVE roles: # Realm specific roles realm: - name: "acme-user" description: "Acme User" - name: "acme-user-support" description: "Acme Support User" composite: true composites: client: "realm-management": - query-groups - view-users - name: "default-roles-acme-internal" description: "${role_default-roles}" composite: true composites: realm: - "offline_access" - "acme-user" client: "account": - "manage-account" - "view-profile" groups: - "name": "Users" # "path": "/Users" "attributes": groupAttribute1: ["groupAttributeValue1"] "realmRoles": - "acme-user" # "clientRoles": {} "subGroups": [] users: - username: tester email: tester@local firstName: Theo lastName: Tester enabled: true attributes: locale: ["en"] phoneNumber: ["+49178111222333"] phoneNumberVerified: ["true"] credentials: - type: password userLabel: initial value: test temporary: false groups: - "Users" ================================================ FILE: config/stage/dev/realms/acme-saml.yaml ================================================ realm: acme-saml enabled: true displayName: Acme SAML displayNameHtml: Acme SAML loginWithEmailAllowed: true loginTheme: internal #accountTheme: keycloak #adminTheme: keycloak #emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) components: # TODO externalize cert and keys "org.keycloak.keys.KeyProvider": - name: custom-aes-generated providerId: aes-generated config: kid: ["b4ae2780-ddcc-4199-b579-d2715340cb0b"] secret: ["AK9MESP3eN1lI7Ukk4hHeQ"] priority: ['100'] - name: custom-hmac-generated providerId: hmac-generated config: kid: ["1d0e6d18-d947-44ef-b5ab-1cae5b4e2d68"] secret: ["heM9Q45VuQ6V-OqJ4HTmDBgQ53duz_5YgeZKZaJiddbe3FyT3vOi9Tv12iPX3rt6eIzRD1nPQY3T6NOPawBLmQ"] priority: ['100'] algorithm: ["HS256"] - name: custom-rsa-generated providerId: rsa-generated config: privateKey: ["MIIEowIBAAKCAQEAw5UaCtl+P3oLnM2GXMuLwpZeipVirZlaUfmbKRPnkVK/ALmbn8/HrdU7PG41YMDIbPxSr2VRrkQPYPXRb97vYAhsQLTDYX3P3VzISWK2yv+5CgjeycImMtyFcx7PnS7Qn+D/vy1+G/UgKjoSE/O6kwFbCX4uLDV+JS/niks9UzWvjnR9xDFpVf+NmzPNmAuu3NtDPve6hQcnpAns3bDPlz6wbeLL07JVCX64jR0iz1EVzntRQ5RHDaU4+AA8SsqdyRXmAQNDXoLTsS+NXPO/VDpITYUhU3gXrmpJFDWJdTa4jSt01M6SO7lY0nKWyanx18kTweI7CNDwCtn+dMCC6QIDAQABAoIBAAGD6XZ9jmKYA7iEiB62bgAFMbjRpPjS2BYMAMV987yWv0eWaIwBrFqerQ2QDgZQoAzmHI7i0lHvEY5vAR8kg0bDcfFDZUMfWimtIxkcdG2YsxqOjIlUIX8h4b/NVI7zcqbWc6zLwa8eRFBHcGXqrL6gU0/8xAdQJ8jKePkDkbdQDvMSHSuIRRFCWHUTwOykXjuB4fzyzwNyjXQbmcSDeYm8LKtULji+fwpjcd34+aM7eqXnAMkzkPvfLNuULcXRP+Pu7DPGIXrw5/P+LmbN7LE+JM8B+hiYn62GDOttTCnKpjU58125p/Pvjec8bpYorhJP7O1iG02u4dTEA9Vpv2ECgYEA5JudCkEzY2HLn02N0ybQZhVODLWxktvDFxj6NXh0wBIu1uVzOpG9GgwRtyS1i+6KrnpKz7eqWHLAT0o3doPKVbxFJ7aUgYUL7IQqbgD1sdV+Cq7n1xVdyGnOaaBXpKhftSKqjEpup7UKN5xrQ+W5nYHcuHzjZLlLc5xkRSkrLjkCgYEA2wR0wvkgwwI85aUk5/dSysvHct8Wh1PF8TPvoyuiouNdCc6X4HPjcL5o+B9AVVr7JroVsHVtDYSA9+uHTag8DHbJ3lVbWA94N4SuNCoKJ6DZtapRnCLEFPXXRO5me4GvTxtEO8S5ES0PwykAu5bKRfhl/xmeUU/OVWidAAsh+jECgYEAhMovail9ZBkGYj52R1SgcOunLpLL1vZ4WA5WKIETsA3fz0vwpvDI2zxvfeaA3gtt2vOGSSnydPYS5vvBQ8JB4ZM+yFax5JoX1wbebo94KBhO4n2+hZ0PoL50+737qtVy4pCEaIFDzX7HtI3TcNkb/HXWdAN3QqavQTRyugmz32ECgYAqnn5eJn6ClB/njDBXV2BsCCWCq/jFUr71Bec++FHIorfLHcGeMs7ydIsWpXYuZerziUiJMwCKnds+4z1MFk3BGyiDNFb3FuOM4ivICNo7Bej3mfIRkQ5ZCdHfHwkgRYcovKSVgN2Ggx9LGeKDnn80CHdIoeKV7hK3ugi7Jm9xMQKBgCcyKs3Odw5u4RpTAChAmdvgnkuXoCtQAsi998NPWyu1Y8aEfaAQJKbWdXDen9/PUu+ZUtwkyn7goAX0TtRgcTrkc6qyXQI/3gXPWpm0o++Mxdb7+l9wLZhosnoeeUtkF+3+wD1WlxKDOvw0UNEAYodBpdyYGfVZiky1ntcBelb/"] keyUse: ["SIG"] certificate: ["MIIClzCCAX8CBgF/0OmrYzANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMDMyODE0MjIyOVoXDTMyMDMyODE0MjQwOVowDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOVGgrZfj96C5zNhlzLi8KWXoqVYq2ZWlH5mykT55FSvwC5m5/Px63VOzxuNWDAyGz8Uq9lUa5ED2D10W/e72AIbEC0w2F9z91cyElitsr/uQoI3snCJjLchXMez50u0J/g/78tfhv1ICo6EhPzupMBWwl+Liw1fiUv54pLPVM1r450fcQxaVX/jZszzZgLrtzbQz73uoUHJ6QJ7N2wz5c+sG3iy9OyVQl+uI0dIs9RFc57UUOURw2lOPgAPErKnckV5gEDQ16C07EvjVzzv1Q6SE2FIVN4F65qSRQ1iXU2uI0rdNTOkju5WNJylsmp8dfJE8HiOwjQ8ArZ/nTAgukCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAcDoujUldX1HiCjPjLrcUaY+LNCSsGWeN7g/lI7z18sxg3VlhsPz2Bg5m4zZCFVcrTPax1PuNqYIxetR9fEP8N+8GHLTnd4KrGvE6rH8xwDDk3GET5QKHnaUDUoxdOND85d65oL20NDIHaNDP+Kw/XIktV30mTKZerkDpxJSC9101RDwVhH3zpr0t4CYTnnR6NTBNkVRfDl19Nia98KpbSJizIw2y0zC8wubJzFnBoWbXv1AXOqTZUR2pyP742YJNA/9NFg4+EDbW/ZJVaajY+UVN8ImCj1T32f78189d3NFoCX81pBkmRv8YfXetZgDcofuKKTkUmFlP55x5S32Vmw=="] priority: ['100'] - name: custom-rsa-enc-generated providerId: rsa-enc-generated config: privateKey: ["MIIEowIBAAKCAQEAvMLRCPK7e4w+JKoGvYsSHxh7xRZRqB24QUaFs2CxV1LK37rFKFmKx236LYjl5dCWEEceDgmCO9lLNrbt5pvYsGVcU2Uzvv0r0sikDI/LUogNfIactKZkT+U1HcvOsBpDdCTsNhsR9d0wUXQXpUjPJYKyIyMX/WpiaNPzFd9qUUB6angq1SLlSTG3n3dFCedEvWIDRMY7FM5eHi78YrIpsCfgVQmKjjEonYqIBx6BG/lI/89hm7u++IyzHv32XgQPNpzY0ltsLaALcedLcv9auNkCxwLbGIVzaIf4dDO3VHJsNYHrJAypUBPta4sBYVnwajDjq+eSNFwUFNFxcN0/5wIDAQABAoIBAC6qsXhXXm+YiAKTgJAa0lOav3rF3lFEa7nDoCltVdrDbsGqULT9kjhk4a2hQ0kybO9ATddllWuLeLNhvWY+kG9n19AMXKMyv0Ng9GHgqQFR/peTRinJW1J/Vcb0jLhv/c44lKd5wNJ6qUfx/iiQXBonejgCpJsz0nmdMONu9T951tJZc8jIV+SuldWOBlH5DY4rGO+8wmxCzbuOKkb4mBNy511rVLn5csePZooSWHJPU647MT8+/xO//UYGPnlK1FOyaLNlWpDnebXFXDftl274fgR27AAaGbVGGIv7NDkidLYR/TvG6ifEgxtUJB1vk8n8Id1EwA5eZsVPgVcpW+kCgYEA73MxEWD1WFHIhTM+285mpMdwwLpd3eFyJBG4VzKRsiJUAVapPnNrVB0Jknzyo/yhL6Yv9TUmmj7zyOh7KYIMDjoBfOZym463WYW4WmKmCJPjZBvV9f0ZPkOO9bjMRvz0zQRFB1D50ebtja2nPKzMUG1iNPrv49qMzqJbOBTJGh8CgYEAyc69CWGohP6z9mNn+qHbZ0pYAYCf1d9/fhX6CHcgVPudTPr/8EERvMEffr1fh5IadJIWDu2/fYZxlJGS+r/3YLwJTBvYip9d06TUThDxxQiHSdK7USW5A7lSgT/kKTp2ldqmiDQtoaf54Axlapq5m9K/uN45keismI1LNsgZsTkCgYEAzCfVrTCS3sOUCOWBcZ2QbGvTWa9MevJOBCzLlCT8jfmw0BdYY3O7DdNYJvq7UlACCgNSnmm7yQVli2WUJPbJWpPgUuKU0sai0wQtA3tafrPAy8jj60DpdenaCO2P1fK0sdwzEqLa7TlMT2DA1v2pkeVBN1TAle/v3/oTdkRalhcCgYBdIIVFpgZhUTR0+AyMsVKRSNJx5wxbYubvpW6bp3WJIg/F7XJcSXrI8wn4r6U856RDtQJu4zHh2D/jwoXkJuAeiMd1ksgLuF1RBJhgahtXxIbB/3gni1Pkrwmu0XAVwn/kyWDeK83+8ogx5yaJ2lra2JdW1V4VwhybzWAvKIoKqQKBgFCzeiA47tVF18lx9h9qc86HrrS+OtwrAyNlQlt8sHGHV3ev3Ip361U6B50bv9NTpfgtcBL4Ml57lyO0RljQxV+9TLYSfOJo5iYwVmIP1LhmKWf+4WSvybhCIzCXp9czao5nHXFsvBtyy3+ay76RtAxmVn2lE/3zy45cwEIHErnr"] keyUse: ["ENC"] certificate: ["MIIClzCCAX8CBgF/0OmsGjANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARkZW1vMB4XDTIyMDMyODE0MjIyOVoXDTMyMDMyODE0MjQwOVowDzENMAsGA1UEAwwEZGVtbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzC0Qjyu3uMPiSqBr2LEh8Ye8UWUagduEFGhbNgsVdSyt+6xShZisdt+i2I5eXQlhBHHg4JgjvZSza27eab2LBlXFNlM779K9LIpAyPy1KIDXyGnLSmZE/lNR3LzrAaQ3Qk7DYbEfXdMFF0F6VIzyWCsiMjF/1qYmjT8xXfalFAemp4KtUi5Ukxt593RQnnRL1iA0TGOxTOXh4u/GKyKbAn4FUJio4xKJ2KiAcegRv5SP/PYZu7vviMsx799l4EDzac2NJbbC2gC3HnS3L/WrjZAscC2xiFc2iH+HQzt1RybDWB6yQMqVAT7WuLAWFZ8Gow46vnkjRcFBTRcXDdP+cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAWjAlB7qYrXY2XbriG0S+H+PKJsZ3GZFYZAsFGOYvEmL2BT4e53GzQLqDiwW2VUnhGXFwN438+IptYPZXuXiqjidjTyqLpApZzm66er9ZLs5Ii8E1wJn0j/uRpmsQ3arsZ7FAVYaVbt5txyJSh0mDOng351HsCye7EDWhseaZLTQ8YIGZxoPZYe8abceG3lxF8iI2Wnmvhudzhli9ZCRbYeNeVGObNLiBd33gYEYo3UZc+j0/tIoYmVLG5R8CeKK62M5ow8/ul4xc9BmX7QFB/GLCQnhlEMeFAunhtLZBAwmJA9lG0JXp3c4K22cGXyyLG15PBSYWULJcUQ8lxKW+lQ=="] priority: ['100'] algorithm: ["RSA-OAEP"] clients: - clientId: acme_saml_idp_broker name: Acme SAML Broker Client rootUrl: '' adminUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml" surrogateAuthRequired: false enabled: true alwaysDisplayInConsole: false clientAuthenticatorType: client-secret redirectUris: - "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-saml/endpoint/*" notBefore: 0 bearerOnly: false consentRequired: false standardFlowEnabled: true implicitFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false publicClient: false frontchannelLogout: true protocol: saml attributes: saml.assertion.signature: 'true' saml.force.post.binding: 'true' saml.multivalued.roles: 'false' saml.encrypt: 'false' backchannel.logout.revoke.offline.tokens: 'false' saml.server.signature: 'true' saml.server.signature.keyinfo.ext: 'false' exclude.session.state.from.auth.response: 'false' # TODO externalize saml certificate saml.signing.certificate: "MIICtzCCAZ8CBgF5PmO+MTANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRhY21lX3NhbWxfaWRwX2Jyb2tlcjAeFw0yMTA1MDUyMTE0NTRaFw0zMTA1MDUyMTE2MzRaMB8xHTAbBgNVBAMMFGFjbWVfc2FtbF9pZHBfYnJva2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQABeL4DpJIymxZ5UWFWwFC5NXLJ0Q8+UdrWPrqazebtsGSrYUwpsO4gObEHuo497UMcXMcDd9cJiPLeo9TyvfNFkC/17riGC5gd8eBIHTAEECnyJZGtuAuWQtRIkoLYJ260zlgC6dBy86m9OSd6UgJRmkXihWcE/dGplWw5FYQ0U3CrE9LXup0d0PEYH+b1RUUtIxjQDZxxVoO2BjivfbbmILbOikthMfjfO3BviIb9U/8MrerLftZ+wssSUxsCr41pakIZn5uTttiwwlUXlFTWQ5vsvDLNLprINgTlzZOXZYQ9Az08PcQR5EMpb0LDoQlTGf9BZJNtMFmssLKeNi9V" backchannel.logout.session.required: 'false' client_credentials.use_refresh_token: 'false' saml.signature.algorithm: RSA_SHA256 saml_force_name_id_format: 'false' saml.client.signature: 'false' tls.client.certificate.bound.access.tokens: 'false' saml.authnstatement: 'true' display.on.consent.screen: 'false' # TODO externalize saml key saml.signing.private.key: "MIIEpAIBAAKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABAoIBABIOrS79ZCSkG2D3rKi6ran6K+4QeyxykmM3a0MDdz4x0tpGL5C2SHAKS6tSKCRFthnaU7BUMUzk7UROWJBxeT3BrZFrhgGEUJHT2UF8aNekdQ8Yolo3RqZAHdmLKDwG9jIHmAdkPQqaq5T3ztFXgsSQJrHI9Eh2cALYQqq40YK+5VF+sYrEwBvT4wZtgsFd+NXjQuaLH2PuQAG9gdAH0jhzN+NRmbC8JEHtb6/i0tKiOBcYuEAcQ+BE6V4EpGDEWlIDoLMI7EGZsQHuvn6Aqs7IpIBNhJiTFl1rGCssDVzjgfFKaa/jTfDS8xUfbusT5vqLTecUQRzenrPeyAgRoBECgYEA01+d1X4OvmIqZ5nW9CjJvs4y9qKvtpv2Xvrqe2/qdhejfmg9XMUwpBAOfaH8Y/5RoJzqq0iyfpaDnt0REJC7+x2LOZ8XOzRH1ow7M9swBYZDuz6Wa0h2uFcPHW+3SDKulm+TyNczltLvKA7v/KyS8Bn1UkjDL/QIlQbCEPLLtb8CgYEAv2JPgzLTV+DA3ybKmF+1sTpsRnHGOqiKzb5GIf8yq0zi6t7pjK5QiRbZBvlH5aC8BFY52k7BcGBiQsnc1kDpg/ms6Mg9TRXaTVZIzqlRYSDsFcaDGvXxzLdc6WwJGPOV/VXrC3DzgHt/Rb6ED6CXPrxlrgGAc2nkpt9waQac4vkCgYEAg77FEZxQdDmbVJd+cxA5LsQ236LnAlqTZP/fxrAq4xA4x0ERfhEqEBgx7/xW47xQBFvJqJjXKC+IOixvxnNvt0Ti0jdms3ASlpcxD1E+zTKyZLLN7nBsDtm0ghRvmIB+cSV6Z2Q6s3cluUIWMtcdfqmvTmorvmfMMZbUvtuWPOECgYEAl26im51LvO0Jr4hyJb8VdPZVVigQQbm6mrFDrQLQhNqBcnaPNdF3yAFcGDiGuxtDqerQO/y08sZQ+afgJWeXXeXg+w/18VipMyhi06MF0WTLaS957YtNmD4+NjRVvnh+5cVmBdeJ1M/jFLx6oiLfibRogBaQHMJdOezydSfWW4ECgYBSN/CBHUKZn16UaOrjZReLGHtAHqA6KsLPQSDv+kUkaiZZr782d3DMDxIRyU+eXFtvDqYWzvRYnoaV/4sL2CLd2XTnMpFdlrDELzsD4xzr6sSHRAcuWD6T0lURfGmRt3/Qo7GZh312WtezrD0fRaz2OZpzA/txDsz0gQojC7JUwQ==" saml_name_id_format: username saml.onetimeuse.condition: 'false' saml_signature_canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#" authenticationFlowBindingOverrides: {} fullScopeAllowed: false nodeReRegistrationTimeout: -1 protocolMappers: - name: X500 email protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: email friendly.name: email attribute.name: urn:oid:1.2.840.113549.1.9.1 - name: X500 surname protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: lastName friendly.name: surname attribute.name: urn:oid:2.5.4.4 - name: X500 givenName protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: firstName friendly.name: givenName attribute.name: urn:oid:2.5.4.42 defaultClientScopes: [] optionalClientScopes: [] users: - username: acmesaml email: acmesaml@local firstName: Anne lastName: SAML enabled: true attributes: locale: ["de"] credentials: - type: password userLabel: initial value: test temporary: false ================================================ FILE: config/stage/dev/realms/acme-stepup.yaml ================================================ realm: acme-stepup displayName: "Acme Step-up" enabled: true browserFlow: "Browser Step-Up" attributes: "acr.loa.map": "{\"cookie\":\"0\",\"pw\":\"1\",\"2fa\":\"2\"}" clients: - clientId: app-minispa protocol: openid-connect name: Acme Account Console description: "Acme Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=acme-stepup&show=profile,apps,security,token,idToken,stepup,reauth,logout" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "roles" - "profile" - "acr" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" authenticationFlows: - alias: "Browser Step-Up" description: "This flow implements a custom browser pattern" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - flowAlias: "Password Condition" requirement: CONDITIONAL autheticatorFlow: true - flowAlias: "2FA Condition" requirement: CONDITIONAL autheticatorFlow: true - alias: "Password Condition" description: "Sub-Flow to ask user for username / password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-level-of-authentication requirement: REQUIRED authenticatorConfig: "username-password" - authenticator: auth-username-password-form requirement: REQUIRED - alias: "2FA Condition" description: "Sub-Flow to ask user for 2FA during stepup" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: conditional-level-of-authentication requirement: REQUIRED authenticatorConfig: "2fa-stepup" - authenticator: acme-auth-otp-form requirement: ALTERNATIVE - authenticator: auth-recovery-authn-code-form requirement: ALTERNATIVE authenticatorConfig: - alias: "username-password" config: "loa-condition-level": "1" "loa-max-age": "36000" - alias: "2fa-stepup" config: "loa-condition-level": "2" "loa-max-age": "300" users: - username: tester email: tester@local firstName: Theo lastName: Tester enabled: true emailVerified: true attributes: locale: [ "en" ] phoneNumber: [ "+49178111222333" ] phoneNumberVerified: [ "true" ] title: [ "" ] salutation: [ "mr" ] # Thomas picture: [ "https://en.gravatar.com/userimage/52342809/a957ac868585f91edf7eb9b7463328b9.jpeg?size=64" ] credentials: - type: password userLabel: initial value: test temporary: false ================================================ FILE: config/stage/dev/realms/company-apps.yaml ================================================ realm: company-apps enabled: true displayName: Company Apps displayNameHtml: Company Apps loginWithEmailAllowed: true registrationAllowed: false registrationEmailAsUsername: true #loginTheme: apps loginTheme: internal-modern #accountTheme: keycloak.v3 #adminTheme: keycloak #emailTheme: keycloak internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) browserFlow: "Browser Identity First with IdP Routing" #registrationFlow: "Custom Registration" # Custom realm attributes attributes: # for http variant: http://apps.acme.test:4000 "acme_site_url": "https://apps.acme.test:4443" "acme_terms_url": "https://apps.acme.test:4443/site/terms.html" "acme_imprint_url": "https://apps.acme.test:4443/site/imprint.html" "acme_privacy_url": "https://apps.acme.test:4443/site/privacy.html" #"acme_logo_url": "no example, should be taken from client or null" "acme_account_deleted_url": "https://apps.acme.test:4443/site/accountdeleted.html" smtpServer: replyToDisplayName: "Company APPS Support" port: 1025 host: mail replyTo: "no-reply@acme.test" from: "company-apps-sso@local" fromDisplayName: "Company APPS Account" clientScopes: - name: company description: Company Access protocol: openid-connect identityProviders: - alias: "idp-company-users" displayName: "Company Users Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "1000" issuer: "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users" tokenUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/token" jwksUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/certs" userInfoUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/userinfo" authorizationUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/auth" logoutUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-users/protocol/openid-connect/logout" clientId: "acme-company-apps-broker" clientSecret: "secret" clientAuthMethod: "client_secret_post" defaultScope: "openid" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceMethod: "S256" pkceEnabled: "true" acmeEmailDomainRegex: "(company\\.com)" - alias: "idp-acme-azuread" displayName: "Company Partner EntraID Login" providerId: "oidc" enabled: true updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "4000" issuer: "$(env:ACME_AZURE_AAD_TENANT_URL)/v2.0" tokenUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/token" jwksUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/discovery/v2.0/keys" userInfoUrl: "https://graph.microsoft.com/oidc/userinfo" authorizationUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/authorize" logoutUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/logout" clientId: "$(env:ACME_AZURE_AAD_TENANT_CLIENT_ID:-dummy)" clientSecret: "$(env:ACME_AZURE_AAD_TENANT_CLIENT_SECRET:-secret)" clientAuthMethod: "client_secret_post" defaultScope: "openid profile email" loginHint: "true" backchannelSupported: "true" validateSignature: "true" useJwksUrl: "true" syncMode: "FORCE" pkceMethod: "S256" pkceEnabled: "true" hideOnLoginPage: true acmeEmailDomainRegex: "(partner\\.com)" authenticationFlows: ## Identity First Browser Login Flow - alias: "Browser Identity First with IdP Routing" description: "This flow implements the Identity First pattern" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: acme-auth-username-idp-select requirement: REQUIRED authenticatorConfig: "acme-auth-username-idp-select" - flowAlias: "2FA Forms" requirement: CONDITIONAL autheticatorFlow: true - alias: "2FA Forms" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: acme-auth-trusted-device requirement: ALTERNATIVE - authenticator: acme-auth-otp-form requirement: ALTERNATIVE - authenticator: acme-email-code-form requirement: ALTERNATIVE - authenticator: auth-recovery-authn-code-form requirement: ALTERNATIVE authenticatorConfig: - alias: "acme-auth-username-idp-select" config: lookupRealmName: "company-users" lookupRealmIdpAlias: "idp-company-users" clients: - clientId: app-minispa protocol: openid-connect name: Company Account Console description: "Company Account Console Description" enabled: true publicClient: true standardFlowEnabled: true directAccessGrantsEnabled: false # Show client in account-console alwaysDisplayInConsole: true serviceAccountsEnabled: false # attributes: { } fullScopeAllowed: true rootUrl: "$(env:APPS_FRONTEND_URL_MINISPA)" baseUrl: "/?realm=company-apps&show=profile,settings,apps,security,logout" adminUrl: "" redirectUris: - "/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "roles" - "profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" ================================================ FILE: config/stage/dev/realms/company-users.yaml ================================================ realm: company-users enabled: true displayName: Company Users displayNameHtml: Company Users loginWithEmailAllowed: true registrationAllowed: true registrationEmailAsUsername: true #loginTheme: apps loginTheme: internal-modern #accountTheme: keycloak.v3 #adminTheme: keycloak #emailTheme: keycloak internationalizationEnabled: true supportedLocales: ["en","de"] defaultLocale: "en" sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) browserFlow: "Browser Identity First with IdP Routing" #registrationFlow: "Custom Registration" # Custom realm attributes attributes: # for http variant: http://apps.acme.test:4000 "acme_site_url": "https://apps.acme.test:4443" "acme_terms_url": "https://apps.acme.test:4443/site/terms.html" "acme_imprint_url": "https://apps.acme.test:4443/site/imprint.html" "acme_privacy_url": "https://apps.acme.test:4443/site/privacy.html" #"acme_logo_url": "no example, should be taken from client or null" "acme_account_deleted_url": "https://apps.acme.test:4443/site/accountdeleted.html" smtpServer: replyToDisplayName: "Company Users Support" port: 1025 host: mail replyTo: "no-reply@acme.test" from: "company-apps-sso@local" fromDisplayName: "Company Users Account" authenticationFlows: ## Identity First Browser Login Flow - alias: "Browser Identity First with IdP Routing" description: "This flow implements the Identity First pattern" providerId: basic-flow builtIn: false topLevel: true authenticationExecutions: - authenticator: auth-cookie requirement: ALTERNATIVE - flowAlias: "Identity Forms" requirement: ALTERNATIVE autheticatorFlow: true - alias: "Identity Forms" description: "Sub-Flow to ask user for username an password" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: auth-username-form requirement: REQUIRED - authenticator: auth-password-form requirement: REQUIRED - flowAlias: "2FA Forms" requirement: CONDITIONAL autheticatorFlow: true - alias: "2FA Forms" description: "Sub-Flow to ask user for 2FA" providerId: basic-flow topLevel: false builtIn: false authenticationExecutions: - authenticator: conditional-user-configured requirement: REQUIRED - authenticator: acme-auth-trusted-device requirement: ALTERNATIVE - authenticator: acme-auth-otp-form requirement: ALTERNATIVE - authenticator: acme-email-code-form requirement: ALTERNATIVE - authenticator: auth-recovery-authn-code-form requirement: ALTERNATIVE clients: - clientId: acme-company-apps-broker protocol: openid-connect name: "Company Apps" description: "Company IdP Broker Client" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false fullScopeAllowed: false secret: "secret" redirectUris: - "$(env:KEYCLOAK_FRONTEND_URL)/realms/company-apps/broker/idp-company-users/endpoint/*" webOrigins: - "+" defaultClientScopes: - "basic" - "email" - "profile" optionalClientScopes: - "phone" attributes: "pkce.code.challenge.method": "S256" "post.logout.redirect.uris": "+" ================================================ FILE: config/stage/dev/realms/master.yaml ================================================ realm: master enabled: true ================================================ FILE: config/stage/dev/realms/other/acme-internal-custom.yaml ================================================ realm: acme-internal #identityProviders: # - alias: "idp-acme-azuread" # displayName: "Acme EntraID Login" # providerId: "oidc" # enabled: true # updateProfileFirstLoginMode: on # trustEmail: true # storeToken: false # addReadTokenRoleOnCreate: false # authenticateByDefault: false # linkOnly: false # firstBrokerLoginFlowAlias: "first broker login" # # postBrokerLoginFlowAlias: "Custom Post Broker Login" # config: # guiOrder: "4000" # issuer: "$(env:ACME_AZURE_AAD_TENANT_URL)/v2.0" # tokenUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/token" # jwksUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/discovery/v2.0/keys" # userInfoUrl: "https://graph.microsoft.com/oidc/userinfo" # authorizationUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/authorize" # logoutUrl: "$(env:ACME_AZURE_AAD_TENANT_URL)/oauth2/v2.0/logout" # clientId: "$(env:ACME_AZURE_AAD_TENANT_CLIENT_ID:-dummy)" # clientSecret: "$(env:ACME_AZURE_AAD_TENANT_CLIENT_SECRET:-secret)" # clientAuthMethod: "client_secret_post" # defaultScope: "openid profile email" # loginHint: "true" # backchannelSupported: "true" # validateSignature: "true" # useJwksUrl: "true" # syncMode: "FORCE" ================================================ FILE: config/stage/dev/realms/other/acme-saml.yaml ================================================ realm: acme-saml enabled: true displayName: Acme SAML displayNameHtml: Acme SAML loginWithEmailAllowed: true loginTheme: internal accountTheme: keycloak adminTheme: keycloak emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) clients: - clientId: acme_saml_idp_broker name: Acme SAML Broker Client rootUrl: '' adminUrl: "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-saml/protocol/saml" surrogateAuthRequired: false enabled: true alwaysDisplayInConsole: false clientAuthenticatorType: client-secret redirectUris: - "$(env:KEYCLOAK_FRONTEND_URL)/realms/acme-apps/broker/idp-acme-saml/endpoint/*" notBefore: 0 bearerOnly: false consentRequired: false standardFlowEnabled: true implicitFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: false publicClient: false frontchannelLogout: true protocol: saml attributes: saml.assertion.signature: 'true' saml.force.post.binding: 'true' saml.multivalued.roles: 'false' saml.encrypt: 'false' backchannel.logout.revoke.offline.tokens: 'false' saml.server.signature: 'true' saml.server.signature.keyinfo.ext: 'false' exclude.session.state.from.auth.response: 'false' # TODO externalize saml certificate saml.signing.certificate: "MIICtzCCAZ8CBgF5PmO+MTANBgkqhkiG9w0BAQsFADAfMR0wGwYDVQQDDBRhY21lX3NhbWxfaWRwX2Jyb2tlcjAeFw0yMTA1MDUyMTE0NTRaFw0zMTA1MDUyMTE2MzRaMB8xHTAbBgNVBAMMFGFjbWVfc2FtbF9pZHBfYnJva2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQABeL4DpJIymxZ5UWFWwFC5NXLJ0Q8+UdrWPrqazebtsGSrYUwpsO4gObEHuo497UMcXMcDd9cJiPLeo9TyvfNFkC/17riGC5gd8eBIHTAEECnyJZGtuAuWQtRIkoLYJ260zlgC6dBy86m9OSd6UgJRmkXihWcE/dGplWw5FYQ0U3CrE9LXup0d0PEYH+b1RUUtIxjQDZxxVoO2BjivfbbmILbOikthMfjfO3BviIb9U/8MrerLftZ+wssSUxsCr41pakIZn5uTttiwwlUXlFTWQ5vsvDLNLprINgTlzZOXZYQ9Az08PcQR5EMpb0LDoQlTGf9BZJNtMFmssLKeNi9V" backchannel.logout.session.required: 'false' client_credentials.use_refresh_token: 'false' saml.signature.algorithm: RSA_SHA256 saml_force_name_id_format: 'false' saml.client.signature: 'false' tls.client.certificate.bound.access.tokens: 'false' saml.authnstatement: 'true' display.on.consent.screen: 'false' # TODO externalize saml key saml.signing.private.key: "MIIEpAIBAAKCAQEAngWDA4phFQgIpqVvau/7PJoHDMO1YAM95iaTRvSgLBCUohCtEUHpLS5yBd+k4aya55LNZY4Wh6XUWw0wvQkvP9oaBFgseVy+IiEgatiZAcmsGTeHf4acIkcsZIiobaISSbE+SCLhxsRbMGrIJjp1HXagHPm/Kw/GV5ZbPC3zVlVTMIuVPuQq/xCQxTreOj4V9JgExehrrjAFYL2bQ8GywAPXiblR7WWojoEF1L8iDW7jCam/Jpi/o3upNndMbRqLK4XBWGENsP1YaXaon55UsB/CjvZYnKzrNr1wDM/zhWVjOVr4Uk4N6QoY5K54ELAmYAWgSCZzuClzXI6QOPpkxwIDAQABAoIBABIOrS79ZCSkG2D3rKi6ran6K+4QeyxykmM3a0MDdz4x0tpGL5C2SHAKS6tSKCRFthnaU7BUMUzk7UROWJBxeT3BrZFrhgGEUJHT2UF8aNekdQ8Yolo3RqZAHdmLKDwG9jIHmAdkPQqaq5T3ztFXgsSQJrHI9Eh2cALYQqq40YK+5VF+sYrEwBvT4wZtgsFd+NXjQuaLH2PuQAG9gdAH0jhzN+NRmbC8JEHtb6/i0tKiOBcYuEAcQ+BE6V4EpGDEWlIDoLMI7EGZsQHuvn6Aqs7IpIBNhJiTFl1rGCssDVzjgfFKaa/jTfDS8xUfbusT5vqLTecUQRzenrPeyAgRoBECgYEA01+d1X4OvmIqZ5nW9CjJvs4y9qKvtpv2Xvrqe2/qdhejfmg9XMUwpBAOfaH8Y/5RoJzqq0iyfpaDnt0REJC7+x2LOZ8XOzRH1ow7M9swBYZDuz6Wa0h2uFcPHW+3SDKulm+TyNczltLvKA7v/KyS8Bn1UkjDL/QIlQbCEPLLtb8CgYEAv2JPgzLTV+DA3ybKmF+1sTpsRnHGOqiKzb5GIf8yq0zi6t7pjK5QiRbZBvlH5aC8BFY52k7BcGBiQsnc1kDpg/ms6Mg9TRXaTVZIzqlRYSDsFcaDGvXxzLdc6WwJGPOV/VXrC3DzgHt/Rb6ED6CXPrxlrgGAc2nkpt9waQac4vkCgYEAg77FEZxQdDmbVJd+cxA5LsQ236LnAlqTZP/fxrAq4xA4x0ERfhEqEBgx7/xW47xQBFvJqJjXKC+IOixvxnNvt0Ti0jdms3ASlpcxD1E+zTKyZLLN7nBsDtm0ghRvmIB+cSV6Z2Q6s3cluUIWMtcdfqmvTmorvmfMMZbUvtuWPOECgYEAl26im51LvO0Jr4hyJb8VdPZVVigQQbm6mrFDrQLQhNqBcnaPNdF3yAFcGDiGuxtDqerQO/y08sZQ+afgJWeXXeXg+w/18VipMyhi06MF0WTLaS957YtNmD4+NjRVvnh+5cVmBdeJ1M/jFLx6oiLfibRogBaQHMJdOezydSfWW4ECgYBSN/CBHUKZn16UaOrjZReLGHtAHqA6KsLPQSDv+kUkaiZZr782d3DMDxIRyU+eXFtvDqYWzvRYnoaV/4sL2CLd2XTnMpFdlrDELzsD4xzr6sSHRAcuWD6T0lURfGmRt3/Qo7GZh312WtezrD0fRaz2OZpzA/txDsz0gQojC7JUwQ==" saml_name_id_format: username saml.onetimeuse.condition: 'false' saml_signature_canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#" authenticationFlowBindingOverrides: {} fullScopeAllowed: false nodeReRegistrationTimeout: -1 protocolMappers: - name: X500 email protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: email friendly.name: email attribute.name: urn:oid:1.2.840.113549.1.9.1 - name: X500 surname protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: lastName friendly.name: surname attribute.name: urn:oid:2.5.4.4 - name: X500 givenName protocol: saml protocolMapper: saml-user-property-mapper consentRequired: false config: attribute.nameformat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri user.attribute: firstName friendly.name: givenName attribute.name: urn:oid:2.5.4.42 defaultClientScopes: [] optionalClientScopes: [] users: - username: acmesaml email: acmesaml@local firstName: Anne lastName: SAML enabled: true attributes: locale: ["de"] credentials: - type: password userLabel: initial value: test temporary: false ================================================ FILE: config/stage/dev/realms/other/acme-user-profile.yaml ================================================ realm: acme-user-profile enabled: true attributes: userProfileEnabled: true userProfile: attributes: - name: username displayName: "${username}" validations: length: min: 3 max: 255 - name: email displayName: "${email}" validations: length: max: 255 - name: firstName displayName: "${firstName}" required: roles: - user permissions: view: - admin - user edit: - admin - user validations: length: max: 255 - name: lastName displayName: "${lastName}" required: roles: - user permissions: view: - admin - user edit: - admin - user validations: length: max: 255 - name: phoneNumber displayName: "${phoneNumber}" annotations: inputType: "html5-tel" validations: length: min: 6 max: 64 required: roles: - user scopes: - "phone" selector: scopes: [ "phone" ] permissions: view: - user - admin edit: - user - admin ================================================ FILE: config/stage/dev/realms/other/acme-vci.yaml ================================================ realm: acme-vci enabled: true displayName: Acme VCI displayNameHtml: Acme VCI loginWithEmailAllowed: true loginTheme: internal resetPasswordAllowed: true accountTheme: keycloak.v2 adminTheme: keycloak emailTheme: keycloak sslRequired: $(env:SSL_REQUIRED:-EXTERNAL) clients: - clientId: acme-siop-manager protocol: openid-connect name: Acme SIOP Manager description: "Client for registering SIOP Clients" enabled: true publicClient: false standardFlowEnabled: false directAccessGrantsEnabled: false serviceAccountsEnabled: true secret: secret fullScopeAllowed: false users: - username: tester email: test@local.test firstName: Theo lastName: Tester enabled: true attributes: locale: [ "de" ] credentials: - type: password userLabel: initial value: test temporary: false - username: service-account-acme-siop-manager enabled: true serviceAccountClientId: acme-siop-manager clientRoles: realm-management: - create-client ================================================ FILE: config/stage/dev/realms/other/acme-workshop-clients.yaml ================================================ realm: acme-workshop clients: - clientId: acme-standard-client protocol: openid-connect name: Standard Client description: "Standard Client Description v2" enabled: true publicClient: false standardFlowEnabled: true directAccessGrantsEnabled: false serviceAccountsEnabled: false secret: acme-standard-client-1-secret fullScopeAllowed: false redirectUris: - "http://localhost/acme-standard-client/login*" - clientId: client1 # protocol: openid-connect # name: Client 1 description: "Client1 Description v2" # enabled: true # publicClient: true # standardFlowEnabled: true # directAccessGrantsEnabled: false # serviceAccountsEnabled: false # fullScopeAllowed: false # rootUrl: "http://localhost:20002/webapp" redirectUris: - "http://localhost:20002/webapp/*" - "http://localhost/acme-standard-client/login*" ================================================ FILE: config/stage/dev/realms/other/acme-workshop-idp.yaml ================================================ realm: acme-workshop identityProviders: - alias: "Google" displayName: "Acme Google Login" providerId: "google" enabled: false updateProfileFirstLoginMode: on trustEmail: true storeToken: false addReadTokenRoleOnCreate: false authenticateByDefault: false linkOnly: false firstBrokerLoginFlowAlias: "first broker login" # postBrokerLoginFlowAlias: "Custom Post Broker Login" config: guiOrder: "5000" syncMode: IMPORT userIp: true clientSecret: dummysecret clientId: dummyclientid useJwksUrl: true ================================================ FILE: config/stage/dev/realms/other/acme-workshop.yaml ================================================ realm: acme-workshop displayName: Acme Workshop ================================================ FILE: config/stage/dev/realms/workshop.yaml ================================================ realm: workshop enabled: true displayName: "Acme Workshop" # Custom realm attributes attributes: "custom.branding.backgroundColor": "orange" ================================================ FILE: config/stage/dev/tls/.gitkeep ================================================ ================================================ FILE: deployments/local/cluster/apache/docker-compose-apache.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:3443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:3443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-apache-lb: image: httpd:2.4.48-alpine # logging: # driver: none volumes: # relative paths needs to be relative to the docker-compose cwd. - ./id.acme.test.conf:/etc/apache2/sites-enabled/id.acme.test.conf:z - ../../../../config/stage/dev/tls/acme.test+1.pem:/usr/local/apache2/conf/server.crt:z - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/usr/local/apache2/conf/server.key:z command: > sh -c "sed -i -e 's/^#\(Include .*httpd-ssl.conf\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_slotmem_shm.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_watchdog.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_proxy_http.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_proxy_balancer.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_proxy_hcheck.so\)/\1/' conf/httpd.conf && sed -i -e 's/^#\(LoadModule .*mod_lbmethod_byrequests.so\)/\1/' conf/httpd.conf && sed -i 's/#*[Cc]ustom[Ll]og/#CustomLog/g' conf/httpd.conf && echo 'Include /etc/apache2/sites-enabled/id.acme.test.conf' >> conf/httpd.conf && exec httpd-foreground" ports: - "3443:443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/apache/id.acme.test.conf ================================================ ServerName id.acme.test ServerAdmin admin@id.acme.test # See https://ubiq.co/tech-blog/remove-server-name-apache-response-header/ ServerSignature Off ServerTokens Prod ProxyHCExpr found_issuer {hc('body') =~ /issuer/} ProxyStatus Full BalancerMember http://acme-keycloak-1:8080 route=1 connectiontimeout=2 hcmethod=GET hcexpr=found_issuer hcuri=/auth/realms/master/.well-known/openid-configuration BalancerMember http://acme-keycloak-2:8080 route=2 connectiontimeout=2 hcmethod=GET hcexpr=found_issuer hcuri=/auth/realms/master/.well-known/openid-configuration ProxySet stickysession=ROUTEID Header add Set-Cookie "KC_ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED ProxyPreserveHost on ProxyPass "balancer://keycloak/" stickysession=KC_ROUTEID|kc_routeid scolonpathdelim=On ProxyPassReverse "balancer://keycloak/" ProxyPass ! SetHandler server-status # THIS SHOULD BE PROTECTED ================================================ FILE: deployments/local/cluster/caddy/caddy.json ================================================ { "apps": { "http": { "servers": { "srv0": { "listen": [ ":443" ], "routes": [ { "match": [ { "host": [ "id.acme.test" ] } ], "handle": [ { "handler": "reverse_proxy", "transport": { "protocol": "http" }, "upstreams": [ { "dial": "acme-keycloak-1:8080" }, { "dial": "acme-keycloak-2:8080" } ], "load_balancing": { "selection_policy": { "policy": "ip_hash" }, "try_duration": "1s", "try_interval": "250ms" }, "health_checks": { "active": { "path": "/auth", "port": 8080, "interval": "3s", "timeout": "2s", "expect_status": 200 } } } ], "terminal": true } ] } } }, "tls": { "certificates": { "load_files": [ { "certificate": "/etc/caddy/server.crt", "key": "/etc/caddy/server.key", "tags": [ "selfsigned" ] } ] } } } } ================================================ FILE: deployments/local/cluster/caddy/docker-compose-caddy.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:5443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:5443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-caddy-lb: image: caddy:2.4.2-alpine volumes: # relative paths needs to be relative to the docker-compose cwd. - ./caddy.json:/etc/caddy/caddy.json:z - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/caddy/server.crt:z - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/caddy/server.key:z command: [ "caddy", "run", "-config" , "/etc/caddy/caddy.json"] ports: - "5443:443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/cli/0001-onstart-init.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin Keycloak custom configuration... ### Event Listeners SPI Configuration ### echo SETUP: Event Listeners configuration # Add dedicated eventsListener config element to allow configuring elements. if (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/:read-resource echo SETUP: Add missing eventsListener SPI /subsystem=keycloak-server/spi=eventsListener:add() echo end-if echo SETUP: Configure built-in "jboss-logging" event listener if (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging/:read-resource echo SETUP: Add missing "jboss-logging" event listener /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:add(enabled=true) echo end-if # Propagate success events to INFO instead of DEBUG # This allows to track successful logins in log analysis /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.success-level,value=info) /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.error-level,value=warn) ### Hostname SPI Configuration ### echo SETUP: Hostname configuration # Configure Keycloak to use the frontend-URL as the base URL for backend endpoints /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=${env.KEYCLOAK_FORCE_FRONTEND_TO_BACKEND_URL:true}) ### Sticky Session SPI Configuration ### echo SETUP: Stick Session configuration # The Keycloak Book recommends to always rely on the session affinity provided by the reverse proxy /subsystem=keycloak-server/spi=stickySessionEncoder:add /subsystem=keycloak-server/spi=stickySessionEncoder/provider=infinispan:add(enabled=true,properties={shouldAttachRoute=false}) echo SETUP: Finished Keycloak custom configuration. stop-embedded-server ================================================ FILE: deployments/local/cluster/cli/0010-add-jmx-user.sh ================================================ #!/usr/bin/env bash echo Add JMX user /opt/jboss/keycloak/bin/add-user.sh jmxuser password ================================================ FILE: deployments/local/cluster/cli/0100-onstart-setup-remote-caches.cli ================================================ ================================================ FILE: deployments/local/cluster/cli/0200-onstart-setup-jgroups-encryption.cli ================================================ ================================================ FILE: deployments/local/cluster/cli/0300-onstart-setup-ispn-jdbc-store.cli ================================================ ================================================ FILE: deployments/local/cluster/docker-compose.yml ================================================ services: # Keycloak service definition will be inherited from concrete clustering configurations. acme-keycloak: #image: quay.io/keycloak/keycloak:$KEYCLOAK_VERSION # "quay.io/keycloak/keycloak:16.1.1" -> wildfly # "quay.io/keycloak/keycloak:17.0.1" -> quarkus # "quay.io/keycloak/keycloak:17.0.1-legacy" -> wildfly image: quay.io/keycloak/keycloak:17.0.1-legacy environment: KEYCLOAK_USER: "admin" KEYCLOAK_PASSWORD: "admin" KEYCLOAK_THEME_CACHING: "false" KEYCLOAK_THEME_TEMPLATE_CACHING: "false" PROXY_ADDRESS_FORWARDING: "true" DB_VENDOR: POSTGRES DB_ADDR: acme-keycloak-db DB_DATABASE: keycloak DB_USER: keycloak DB_PASSWORD: keycloak DB_SCHEMA: public # Triggers Truststore generation and dynamic TlS certificate import X509_CA_BUNDLE: "/etc/x509/ca/*.crt" CACHE_OWNERS_COUNT: 2 CACHE_OWNERS_AUTH_SESSIONS_COUNT: 2 JAVA_OPTS: "-XX:MaxRAMPercentage=80 -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -Djava.net.preferIPv4Stack=true" # depends_on: # acme-keycloak-db: # condition: service_healthy volumes: - ./cli:/opt/jboss/startup-scripts:z # This configures the key and certificate for HTTPS. - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/https/tls.crt:z - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/x509/https/tls.key:z # Allow TLS connection to ourselves, this is necessary for cross realm Identity Brokering - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z command: - "-b" - "0.0.0.0" - "-bmanagement" - "0.0.0.0" - "-Dwildfly.statistics-enabled=true" ports: - "8080" - "9990" - "8443" - "8787" acme-keycloak-db: image: postgres:11.12 environment: POSTGRES_USER: keycloak POSTGRES_PASSWORD: keycloak POSTGRES_DB: keycloak ports: - "5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U keycloak"] interval: 10s timeout: 5s retries: 5 volumes: - ./run/postgres/data:/var/lib/postgresql/data:z ================================================ FILE: deployments/local/cluster/haproxy/Dockerfile ================================================ FROM haproxy:2.4.10-alpine COPY --chown=haproxy:haproxy "./acme.test+1.pem" "/etc/haproxy/haproxy.crt.pem" COPY --chown=haproxy:haproxy "./acme.test+1-key.pem" "/etc/haproxy/haproxy.crt.pem.key" ================================================ FILE: deployments/local/cluster/haproxy/docker-compose-haproxy.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: "${ACME_KEYCLOAK:-acme-keycloak}" environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth ports: - "8080" - "8443" - "8787" - "9990:9990" depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-2: extends: file: ../docker-compose.yml service: "${ACME_KEYCLOAK:-acme-keycloak}" environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: . volumes: # relative paths needs to be relative to the docker-compose cwd. - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z # - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z # - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z # - ../run/haproxy/run:/var/run:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/haproxy/haproxy.cfg ================================================ # See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/ #--------------------------------------------------------------------- # Global settings #--------------------------------------------------------------------- global # to have these messages end up in /var/log/haproxy.log you will # need to: # # 1) configure syslog to accept network log events. This is done # by adding the '-r' option to the SYSLOGD_OPTIONS in # /etc/sysconfig/syslog # # 2) configure local2 events to go to the /var/log/haproxy.log # file. A line like the following can be added to # /etc/sysconfig/syslog # # local2.* /var/log/haproxy.log # log 127.0.0.1 local2 #chroot /var/lib/haproxy #pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon # turn on stats unix socket # stats socket /var/lib/haproxy/stats # utilize system-wide crypto-policies ## Disable cipher config to workaround ## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58] # ssl-default-bind-ciphers PROFILE=SYSTEM # ssl-default-server-ciphers PROFILE=SYSTEM # modern configuration # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12 ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets #--------------------------------------------------------------------- # common defaults that all the 'listen' and 'backend' sections will # use if not designated in their block #--------------------------------------------------------------------- defaults mode http log global option httplog option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 2 # see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server timeout http-request 10s timeout queue 1m timeout connect 2s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 3s maxconn 3000 frontend id.acme.test # Copy the haproxy.crt.pem file to /etc/haproxy bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem # ACLs based on typical "scanner noise" acl is_bad_url path -m end -i .php acl is_bad_url path -m end -i .asp # acl is_bad_url url -m sub ../.. # If the request matches one of the known "bad stuff" rules, reject. http-request deny if is_bad_url use_backend keycloak backend keycloak mode http stats enable stats uri /haproxy?status option httpchk http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost option forwardfor http-request add-header X-Forwarded-Proto https http-request add-header X-Forwarded-Port 1443 http-request redirect scheme https unless { ssl_fc } cookie KC_ROUTE insert indirect nocache balance roundrobin # Configure transport encryption with https / tls # http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check server kc1 acme-keycloak-1:8443/auth ssl verify none check cookie kc1 server kc2 acme-keycloak-2:8443/auth ssl verify none check cookie kc2 # Configure plain transport with http # server kc1 acme-keycloak-1:8080/auth check cookie kc1 # server kc2 acme-keycloak-2:8080/auth check cookie kc2 ================================================ FILE: deployments/local/cluster/haproxy-database-ispn/cli/0010-add-jmx-user.sh ================================================ #!/usr/bin/env bash echo Add JMX user /opt/jboss/keycloak/bin/add-user.sh jmxuser password ================================================ FILE: deployments/local/cluster/haproxy-database-ispn/cli/0300-onstart-setup-ispn-jdbc-store.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin Infinispan jdbc-store configuration. /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove() batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(owners=1) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/component=expiration:add(lifespan=900000000000000000) # You can use this to limit the number of cache items in memory # See https://infinispan.org/docs/stable/titles/configuring/configuring.html#eviction_configuring-memory-usage /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/memory=heap:add(max-entries=${env.KEYCLOAK_ISPN_CACHE_SESSION_MEMORY_MAX_ITEMS:50000}) # Enable statistics for sessions cache /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc:add( \ datasource="java:jboss/datasources/KeycloakDS", \ passivation=false, \ fetch-state=true, \ preload=false, \ purge=false, \ shared=true, \ max-batch-size=1000 \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc:write-attribute( \ name=properties.databaseType, \ value=POSTGRES \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=jdbc/table=string:add( \ data-column={type=bytea}, \ drop-on-stop=false, \ fetch-size=5000, \ prefix=ispn \ ) # Optionally also persist clientSessions in the jdbc-store run-batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:remove() batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add(owners=1) /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/component=expiration:add(lifespan=900000000000000000) # You can use this to limit the number of cache items in memory # See https://infinispan.org/docs/stable/titles/configuring/configuring.html#eviction_configuring-memory-usage /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/memory=heap:add(max-entries=${env.KEYCLOAK_ISPN_CACHE_CLIENTSESSIONS_MEMORY_MAX_ITEMS:50000}) # Enable statistics for sessions cache /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:write-attribute(name=statistics-enabled,value=true) /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc:add( \ datasource="java:jboss/datasources/KeycloakDS", \ passivation=false, \ fetch-state=true, \ preload=false, \ purge=false, \ shared=true, \ max-batch-size=1000 \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc:write-attribute( \ name=properties.databaseType, \ value=POSTGRES \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc/table=string:add( \ data-column={type=bytea}, \ drop-on-stop=false, \ fetch-size=5000, \ prefix=ispn \ ) # Note we need to use a custom Key2StringMapper here, since the keys are UUIDs which # are not supported by jdbc-store in the infinispan version used by keycloak 15.0.2 /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=jdbc:write-attribute(name=properties.key2StringMapper,value=org.keycloak.patch.infinispan.keymappers.CustomDefaultTwoWayKey2StringMapper) run-batch echo SETUP: Finished Infinispan jdbc-store configuration. stop-embedded-server ================================================ FILE: deployments/local/cluster/haproxy-database-ispn/docker-compose-haproxy-jdbc-store.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth KEYCLOAK_STATISTICS: all volumes: - ./cli/0010-add-jmx-user.sh:/opt/jboss/startup-scripts/0010-add-jmx-user.sh:z - ./cli/0300-onstart-setup-ispn-jdbc-store.cli:/opt/jboss/startup-scripts/0300-onstart-setup-ispn-jdbc-store.cli:z - ./patch/keycloak-model-infinispan-16.1.x-patch.jar:/opt/jboss/keycloak/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/keycloak-model-infinispan-16.1.0.jar:z command: - "--debug" - "*:8787" - "-b" - "0.0.0.0" - "-bmanagement" - "0.0.0.0" - "-Dwildfly.statistics-enabled=true" - "-Dkeycloak.infinispan.ignoreSkipCacheStore=true" depends_on: acme-keycloak-db: condition: service_healthy ports: - "9990:9990" - "8787:8787" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:1443/auth volumes: - ./cli/0300-onstart-setup-ispn-jdbc-store.cli:/opt/jboss/startup-scripts/0300-onstart-setup-ispn-jdbc-store.cli:z - ./patch/keycloak-model-infinispan-16.1.x-patch.jar:/opt/jboss/keycloak/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/keycloak-model-infinispan-16.1.0.jar:z command: [ "-Dwildfly.statistics-enabled=true", "-Dkeycloak.infinispan.ignoreSkipCacheStore=true" ] depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db ports: - "15432:5432" acme-haproxy-lb: build: ../haproxy volumes: - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/haproxy-encrypted-ispn/cli/0200-onstart-setup-jgroups-encryption.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin JGroups encryption configuration... echo SETUP: Configure JGroups symmetric encryption /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:add(add-index=5) /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name="properties.provider",value="SunJCE") /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name="properties.sym_algorithm",value="AES") /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name="properties.keystore_type",value="PKCS12") /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name="properties.keystore_name",value="${jboss.server.config.dir}/jgroups.p12") /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name="properties.alias",value="${env.KEYCLOAK_JGROUPS_KEYSTORE_ALIAS:jgroups}") /subsystem=jgroups/stack=tcp/protocol=SYM_ENCRYPT:write-attribute(name="properties.store_password",value="${env.KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD:changeme3}") echo SETUP: Configure JGroups authentication /subsystem=jgroups/stack=tcp/protocol=AUTH:add(add-index=9,properties={auth_class=org.jgroups.auth.MD5Token,token_hash=SHA,auth_value="${env.KEYCLOAK_JGROUPS_AUTH_PASSWORD:changeme2}"}) echo SETUP: Finished JGroups encryption configuration. stop-embedded-server ================================================ FILE: deployments/local/cluster/haproxy-encrypted-ispn/docker-compose-enc-haproxy.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak volumes: - ./cli/0200-onstart-setup-jgroups-encryption.cli:/opt/jboss/startup-scripts/0200-onstart-setup-jgroups-encryption.cli:z - ./ispn/jgroups.p12:/opt/jboss/keycloak/standalone/configuration/jgroups.p12:z command: [ "--debug", "*:8787", "-Dwildfly.statistics-enabled=true" ] depends_on: acme-keycloak-db: condition: service_healthy ports: - "8080" - "8443" - "9990:9990" - "8787:8787" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak volumes: - ./cli/0200-onstart-setup-jgroups-encryption.cli:/opt/jboss/startup-scripts/0200-onstart-setup-jgroups-encryption.cli:z - ./ispn/jgroups.p12:/opt/jboss/keycloak/standalone/configuration/jgroups.p12:z command: [ "-Dwildfly.statistics-enabled=true" ] depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: ../haproxy volumes: - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/haproxy-encrypted-ispn/jgroups-keystore.sh ================================================ #!/usr/bin/env bash KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD=${KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD:-changeme3} keytool -genseckey \ -keyalg AES \ -keysize 256 \ -alias jgroups \ -keystore ispn/jgroups.p12 \ -deststoretype pkcs12 \ -storepass ${KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD} \ -keypass ${KEYCLOAK_JGROUPS_KEYSTORE_PASSWORD} \ -noprompt ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/cli/0100-onstart-setup-hotrod-caches.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin hotrod Keycloak cache configuration... echo SETUP: Create remote remote-destination-outbound-socket-binding for accessing remote keycloak-hotrod-cache # see https://docs.wildfly.org/23/wildscribe/socket-binding-group/remote-destination-outbound-socket-binding/index.html /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-1:add( \ host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME1:keycloak-ispn1}, \ port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \ ) /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-2:add( \ host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME2:keycloak-ispn2}, \ port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \ ) echo SETUP: Create remote cache container keycloak-hotrod-cache # see https://docs.wildfly.org/25/wildscribe/subsystem/infinispan/index.html # see https://docs.wildfly.org/25/wildscribe/subsystem/infinispan/remote-cache-container/index.html # TODO configure sslContext explicitly batch /subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:add( \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT}, \ modules=[org.keycloak.keycloak-model-infinispan], \ default-remote-cluster=ispn-remote \ ) /subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:write-attribute(name=statistics-enabled,value="${wildfly.infinispan.statistics-enabled:${wildfly.statistics-enabled:false}}") /subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:write-attribute(name=protocol-version,value="${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0}") /subsystem=infinispan/remote-cache-container=keycloak-hotrod-container:write-attribute(name=properties,value={ \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password} \ }) /subsystem=infinispan/remote-cache-container=keycloak-hotrod-container/remote-cluster=ispn-remote:add( \ socket-bindings=[ispn-remote-1,ispn-remote-2] \ ) run-batch echo SETUP: Remove Keycloak caches /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:remove() echo SETUP: Create remote cache work batch /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add() /subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache sessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add() # see https://docs.wildfly.org/23/wildscribe/subsystem/infinispan/cache-container/distributed-cache/store/hotrod/index.html /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache clientSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache authenticationSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache offlineSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache offlineClientSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache actionTokens batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Create remote cache loginFailures batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=hotrod:add( \ shared=true, \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ remote-cache-container=keycloak-hotrod-container \ ) run-batch echo SETUP: Finished Keycloak cache configuration. stop-embedded-server ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/cli/0100-onstart-setup-remote-caches.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin Remote Keycloak cache configuration... # See https://github.com/keycloak/keycloak-documentation/blob/master/server_installation/topics/operating-mode/crossdc/proc-configuring-infinispan.adoc /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-1:add( \ host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME1:keycloak-ispn1}, \ port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \ ) /socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=ispn-remote-2:add( \ host=${env.KEYCLOAK_REMOTE_ISPN_HOSTNAME2:keycloak-ispn2}, \ port=${env.KEYCLOAK_REMOTE_ISPN_PORT:11222} \ ) /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:remove() /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:remove() # See https://docs.jboss.org/infinispan/11.0/configdocs/infinispan-cachestore-remote-config-11.0.html echo SETUP: Configure Remote Keycloak cache: work batch /subsystem=infinispan/cache-container=keycloak/replicated-cache=work:add() /subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:add( \ cache=work, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/replicated-cache=work/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: sessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:add( \ cache=sessions, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: clientSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:add( \ cache=clientSessions, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=clientSessions/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: authenticationSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/store=remote:add( \ cache=authenticationSessions, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: offlineSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:add( \ cache=offlineSessions, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: offlineClientSessions batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:add( \ cache=offlineClientSessions, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineClientSessions/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: actionTokens batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:add( \ cache=actionTokens, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=actionTokens/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Configure Remote Keycloak cache: loginFailures batch /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add() /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:add( \ cache=loginFailures, \ remote-servers=[ispn-remote-1,ispn-remote-2], \ passivation=false, \ fetch-state=false, \ purge=false, \ preload=false, \ shared=true, \ socket-timeout=${env.KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT}, \ connection-timeout=${env.KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT} \ ) /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures/store=remote:write-attribute(name=properties,value={ \ rawValues=true, \ marshaller=org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory, \ infinispan.client.hotrod.auth_username=${env.KEYCLOAK_REMOTE_ISPN_USERNAME:keycloak}, \ infinispan.client.hotrod.auth_password=${env.KEYCLOAK_REMOTE_ISPN_PASSWORD:password}, \ infinispan.client.hotrod.auth_realm=${env.KEYCLOAK_REMOTE_ISPN_REALM:default}, \ infinispan.client.hotrod.auth_server_name=${env.KEYCLOAK_REMOTE_ISPN_SERVER:infinispan}, \ infinispan.client.hotrod.trust_store_file_name=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PATH:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks}, \ infinispan.client.hotrod.trust_store_type=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_TYPE:JKS}, \ infinispan.client.hotrod.trust_store_password=${env.KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD:password}, \ protocolVersion=${env.KEYCLOAK_INFINISPAN_HOTROD_PROTOCOL_VERSION:3.0} \ }) run-batch echo SETUP: Finished Keycloak cache configuration. stop-embedded-server ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-hotrod.yml ================================================ services: acme-ispn-1: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-2: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak env_file: - ./haproxy-external-ispn.env volumes: - ./cli/0100-onstart-setup-hotrod-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-hotrod-caches.cli:z - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: [ "--debug", "*:8787", "-Dwildfly.statistics-enabled=true", "-Djboss.site.name=site1" ] depends_on: acme-ispn-1: condition: service_healthy ports: - "8080" - "8443" - "8787:8787" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak env_file: - ./haproxy-external-ispn.env volumes: - ./cli/0100-onstart-setup-hotrod-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-hotrod-caches.cli:z - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: [ "-Dwildfly.statistics-enabled=true", "-Djboss.site.name=site1" ] depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-1: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: ../haproxy volumes: # relative paths needs to be relative to the docker-compose cwd. - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z # - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z # - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z # - ../run/haproxy/run:/var/run:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml ================================================ services: acme-ispn-1: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-2: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak env_file: - ./haproxy-external-ispn.env volumes: - ./cli/0100-onstart-setup-remote-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-remote-caches.cli:z - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z # Patched wildfly infinispan extension to support connect-timeout on remote-store # - ../../../../keycloak/patches/wildfly-clustering-infinispan-extension-patch/target/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z # - ./patch/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z - ./patch/wildfly-clustering-infinispan-extension-patch-26.0.1.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-26.0.1.Final.jar:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: [ "--debug", "*:8787", "-Dwildfly.statistics-enabled=true", "-Djboss.site.name=site1" ] depends_on: acme-ispn-1: condition: service_healthy ports: - "8080" - "8443" - "9990:9990" - "8787:8787" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak env_file: - ./haproxy-external-ispn.env volumes: - ./cli/0100-onstart-setup-remote-caches.cli:/opt/jboss/startup-scripts/0100-onstart-setup-remote-caches.cli:z - ./ispn/ispn-truststore.jks:/opt/jboss/keycloak/standalone/configuration/ispn-truststore.jks:z # Patched wildfly infinispan extension to support connect-timeout on remote-store # - ../../../../keycloak/patches/wildfly-clustering-infinispan-extension-patch/target/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z # - ./patch/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z - ./patch/wildfly-clustering-infinispan-extension-patch-26.0.1.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-26.0.1.Final.jar:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: [ "-Dwildfly.statistics-enabled=true", "-Djboss.site.name=site1" ] depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-1: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: ../haproxy volumes: # relative paths needs to be relative to the docker-compose cwd. - ../haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z # - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z # - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z # - ../run/haproxy/run:/var/run:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/haproxy-external-ispn.env ================================================ KEYCLOAK_FRONTEND_URL=https://id.acme.test:1443/auth KEYCLOAK_REMOTE_ISPN_HOSTNAME1=acme-ispn-1 KEYCLOAK_REMOTE_ISPN_HOSTNAME2=acme-ispn-2 KEYCLOAK_REMOTE_ISPN_USERNAME=keycloak KEYCLOAK_REMOTE_ISPN_PASSWORD=password KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD=password KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT=60000 KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT=5000 ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/ispn/Dockerfile ================================================ FROM quay.io/infinispan/server:12.1.9.Final-1 USER 0 RUN true \ && microdnf clean all \ && microdnf install shadow-utils \ && microdnf update --nodocs \ && adduser ispn \ && microdnf remove shadow-utils \ && microdnf clean all RUN chown -R ispn:0 /opt/infinispan USER ispn CMD [ "-c", "infinispan-keycloak.xml" ] ENTRYPOINT [ "/opt/infinispan/bin/server.sh" ] ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/ispn/conf/infinispan-keycloak.xml ================================================ ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/ispn/conf/users.properties ================================================ keycloak=password ================================================ FILE: deployments/local/cluster/haproxy-external-ispn/readme.md ================================================ Clustered Keycloak with Remote Infinispan Cache configuration behind haproxy --- This example provides the configuration to connect Keycloak to an external infinispan cluster. The external infinispan cluster contains the [cache configurations required by Keycloak](ispn/conf/infinispan-keycloak.xml). Keycloak / Wildfly offers multiple options for accessing an external infinispan cluster, e.g. via the (deprecated) remote and the recommended hotrod cache store configuration. This example project contains configurations for both variants: - [Remote cache store configuration](docker-compose-haproxy-ispn-remote.yml) with the [remote cache cli](cli/0100-onstart-setup-remote-caches.cli) adjustments. - [HotRod cache store configuration](docker-compose-haproxy-ispn-hotrod.yml) with the [hotrod cache cli](cli/0100-onstart-setup-hotrod-caches.cli) adjustments. # Setup ## Prepare Infinispan Keystore and Truststore ``` keytool -genkey \ -alias server \ -keyalg RSA \ -keystore ispn-server.jks \ -keysize 2048 \ -storepass password \ -dname "CN=ispn, OU=keycloak, O=tdlabs, L=Saarbrücken, ST=SL, C=DE" keytool -exportcert \ -keystore ispn-server.jks \ -alias server \ -storepass password \ -file ispn-server.crt keytool -importcert \ -keystore ispn-truststore.jks \ -storepass password \ -alias server \ -file ispn-server.crt \ -noprompt rm ispn-server.crt ``` # Run To run this example see the [readme.md](../readme.md) in the cluster folder. ## Misc ### Patch CA Certs As of Keycloak image 14.0.0 the used JDK Truststore contains expired certificates which lead to an exception during server start. To fix this, we need to remove the expired certificates. To get rid of exceptions such as: ``` acme-keycloak_1 | 11:32:21,725 WARN [org.wildfly.extension.elytron] (MSC service thread 1-7) WFLYELY00024: Certificate [soneraclass2rootca] in KeyStore is not valid: java.security.cert.CertificateExpiredException: NotAfter: Tue Apr 06 07:29:40 GMT 2021 acme-keycloak_1 | at java.base/sun.security.x509.CertificateValidity.valid(CertificateValidity.java:277) acme-keycloak_1 | at java.base/sun.security.x509.X509CertImpl.checkValidity(X509CertImpl.java:675) acme-keycloak_1 | at java.base/sun.security.x509.X509CertImpl.checkValidity(X509CertImpl.java:648) acme-keycloak_1 | at org.wildfly.extension.elytron@15.0.1.Final//org.wildfly.extension.elytron.KeyStoreService.checkCertificatesValidity(KeyStoreService.java:230) acme-keycloak_1 | at org.wildfly.extension.elytron@15.0.1.Final//org.wildfly.extension.elytron.KeyStoreService.start(KeyStoreService.java:192) acme-keycloak_1 | at org.jboss.msc@1.4.12.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1739) acme-keycloak_1 | at org.jboss.msc@1.4.12.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.execute(ServiceControllerImpl.java:1701) acme-keycloak_1 | at org.jboss.msc@1.4.12.Final//org.jboss.msc.service.ServiceControllerImpl$ControllerTask.run(ServiceControllerImpl.java:1559) acme-keycloak_1 | at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35) acme-keycloak_1 | at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990) acme-keycloak_1 | at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486) acme-keycloak_1 | at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1363) acme-keycloak_1 | at java.base/java.lang.Thread.run(Thread.java:829) ``` Copy the cacerts keystore from a running Keycloak container locally ``` docker cp gifted_bhaskara:/etc/pki/ca-trust/extracted/java/cacerts ./ispn/cacerts chmod u+w cacerts ``` ``` keytool -delete -keystore ./ispn/cacerts -alias quovadisrootca -storepass changeit keytool -delete -keystore ./ispn/cacerts -alias soneraclass2rootca -storepass changeit chmod u-w ./ispn/cacerts ``` Now mount the fixed `cacerts` into the container via `./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z` ## Problems ### Infinispan connect-timeout for remote caches not supported by Keycloak/Wildfly Cannot configure connect-timeout for remote caches as the configuration attribute is not supported by wildfly, but supported by infinispan. See: https://issues.redhat.com/browse/WFLY-15046 A possible workaround is using [wildfly-clustering-infinispan-extension-patch](/keycloak/patches/wildfly-clustering-infinispan-extension-patch), which contains a patched version of `wildfly-clustering-infinispan-extension.jar` with support for configuring `connect-timeouts`. ### Infinispan store type hotrod not supported by Keycloak The current Keycloak implementation (as of Keycloak 15.0.2) doesn't seem to support the remote cache store type hotrod. Only the hotrod serialization protocol seems to be supported, see: https://github.com/thomasdarimont/keycloak-project-example/issues/22 ================================================ FILE: deployments/local/cluster/nginx/docker-compose-nginx.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:2443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloak environment: KEYCLOAK_FRONTEND_URL: https://id.acme.test:2443/auth depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-nginx-lb: image: nginx:1.21.0-alpine # logging: # driver: none volumes: # relative paths needs to be relative to the docker-compose cwd. - ./nginx.conf:/etc/nginx/conf.d/default.conf:z # - ./dhparams:/etc/ssl/dhparams:z - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/nginx/certs/id.acme.test.crt:z - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/nginx/certs/id.acme.test.key:z ports: - "2443:2443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/cluster/nginx/nginx.conf ================================================ server { listen 2443 ssl http2; server_name id.acme.test; # this is the internal Docker DNS, cache only for 30s resolver 127.0.0.11 valid=15s; # Time to wait to connect to an upstream server proxy_connect_timeout 3; proxy_send_timeout 10; proxy_read_timeout 15; send_timeout 10; # Disable access log access_log off; # generated via https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl_certificate /etc/nginx/certs/id.acme.test.crt; ssl_certificate_key /etc/nginx/certs/id.acme.test.key; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam # ssl_dhparam /etc/ssl/dhparams; # intermediate configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # HSTS (ngx_http_headers_module is required) (63072000 seconds) # add_header Strict-Transport-Security "max-age=63072000" always; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://backend; # health_check feature only available in nginx-plus # health_check interval=2s # fails=2 # passes=5 # uri=/auth # match=signin # # match signin { # status 200; # header Content-Type = text/html; # body ~ "Sign In" # } } } upstream backend { # see https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#choosing-a-load-balancing-method ip_hash; # http://nginx.org/en/docs/http/ngx_http_upstream_module.html#resolver_timeout # resolver_timeout only available in nginx-plus # resolver_timeout 5s; server acme-keycloak-1:8080 max_fails=1 fail_timeout=3s; server acme-keycloak-2:8080 max_fails=1 fail_timeout=3s; # Sticky sessions feature needs nginx-plus # sticky cookie srv_id expires=1h domain=.id.acme.test path=/auth; } ================================================ FILE: deployments/local/cluster/readme.md ================================================ Keycloak Clustering Examples ---- # Cluster with haproxy Load-Balancer ## Prepare Copy the `acme.test*.pem` files from the `config/stage/dev/tls` into the [haproxy](haproxy) directory. ## Run ``` docker compose --env-file ../../../keycloak.env --file haproxy/docker-compose-haproxy.yml up --remove-orphans ``` haproxy status URL: https://id.acme.test:1443/haproxy?status HAProxy Keycloak URL: https://id.acme.test:1443/auth ## Run with encrypted and authenticated JGroups traffic The encryption uses JGroup's `SYM_ENCRYPT` protocol with AES encryption by default. Note that you might generate a new PKCS12 keystore with a secretkey via the script in `haproxy-encrypted-ispn/jgroups-keystore.sh`. Make sure that every Keycloak instance in the cluster must use the exactly same file. The JGroups authentication uses the `AUTH` module with a pre-shared key. ``` docker compose --env-file ../../../keycloak.env --file haproxy-encrypted-ispn/docker-compose-enc-haproxy.yml up --remove-orphans ``` ## Run with Infinispan cache content stored in jdbc-store This example shows how to store data from the user session cache in a database that survives restarts. ``` docker compose --env-file ../../../keycloak.env --file haproxy-database-ispn/docker-compose-haproxy-jdbc-store.yml up --remove-orphans ``` ## Run with dedicated Infinispan Cluster with Remote store The haproxy example can also be started with a dedicated infinispan cluster where the distributed and replicated caches in Keycloak will be stored in an external infinispan cluster with cache store type `remote`. Note that this example uses a [patched version](../../../keycloak/patches/wildfly-clustering-infinispan-extension-patch) of the `wildfly-clustering-infinispan-extension.jar` in order to allow to configure a `connect-timeout` on the remote-store. To start the environment with a dedicated infinispan cluster, just run: ``` docker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml up ``` ## Run with dedicated Infinispan Cluster with Hotrod store [This doesn't work at the moment](https://github.com/thomasdarimont/keycloak-project-example/issues/22) try to use the [Infinispan Cluster with Remote store](#run-with-dedicated-infinispan-cluster-with-remote-store) variant. The haproxy example can also be started with a dedicated infinispan cluster where the distributed and replicated caches in Keycloak will be stored in an external infinispan cluster with cache store type `hotrod` To start the environment with a dedicated infinispan cluster, just run: ``` docker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-hotrod.yml up ``` # Cluster with nginx Load-Balancer ## Run ``` docker compose --env-file ../../../keycloak.env --file nginx/docker-compose-nginx.yml up --remove-orphans ``` Nginx Keycloak URL: https://id.acme.test:2443/auth # Cluster with Apache mod_proxy Load-Balancer ## Run ``` docker compose --env-file ../../../keycloak.env --file apache/docker-compose-apache.yml up --remove-orphans ``` Apache Keycloak URL: https://id.acme.test:3443/auth # Cluster with Caddy Load-Balancer ## Run ``` docker compose --env-file ../../../keycloak.env --file caddy/docker-compose-caddy.yml up --remove-orphans ``` Caddy Keycloak URL: https://id.acme.test:5443/auth ================================================ FILE: deployments/local/clusterx/docker-compose.yml ================================================ services: acme-keycloakx: build: context: "./keycloakx" dockerfile: "./Dockerfile" environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_DB: postgres KC_DB_URL_HOST: acme-keycloak-db KC_DB_DATABASE: keycloak KC_DB_USERNAME: keycloak KC_DB_PASSWORD: keycloak KC_DB_SCHEMA: public # Enable remote debugging DEBUG: "true" DEBUG_PORT: "*:8787" # KC_CACHE: local # uses keycloakx/cache-custom.xml #KC_CACHE: cache-custom.xml KC_CACHE_CONFIG_FILE: cache-custom-jgroups.xml # Workaround for missing postgres JDBC driver for JDBC Ping # JAVA_OPTS_APPEND: "-Xbootclasspath/a:/opt/keycloak/lib/lib/main/org.postgresql.postgresql-42.3.1.jar" # Default JAVA_OPTS #JAVA_OPTS: -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true JAVA_OPTS: "-XX:MaxRAMPercentage=80 -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+DisableExplicitGC -Djava.net.preferIPv4Stack=true" # Allow access via visualvm and jmc JAVA_TOOL_OPTIONS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" mem_limit: 1024m mem_reservation: 1024m cpus: 2 volumes: # This configures the key and certificate for HTTPS. - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/https/tls.crt:z - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/x509/https/tls.key:z # Allow TLS connection to ourselves, this is necessary for cross realm Identity Brokering - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z command: - "--verbose" - "start" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter - "--hostname=id.acme.test:1443" ports: - "8080" - "8443" - "8787" - "8790" acme-keycloak-db: image: postgres:11.12 environment: POSTGRES_USER: keycloak POSTGRES_PASSWORD: keycloak POSTGRES_DB: keycloak ports: - "55432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U keycloak"] interval: 10s timeout: 5s retries: 5 volumes: - ./run/postgres/data:/var/lib/postgresql/data:z ================================================ FILE: deployments/local/clusterx/haproxy/Dockerfile ================================================ FROM haproxy:2.4.10-alpine COPY --chown=haproxy:haproxy "./acme.test+1.pem" "/etc/haproxy/haproxy.crt.pem" COPY --chown=haproxy:haproxy "./acme.test+1-key.pem" "/etc/haproxy/haproxy.crt.pem.key" ================================================ FILE: deployments/local/clusterx/haproxy/docker-compose-haproxy.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloakx environment: KC_HOSTNAME: id.acme.test:1443 ports: - "8080" - "8443" - "18787:8787" - "9990:9990" - "8790:8790" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/auth/"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloakx environment: KC_HOSTNAME: id.acme.test:1443 # depends_on: # acme-keycloak-db: # condition: service_healthy depends_on: acme-keycloak-1: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: . volumes: # relative paths needs to be relative to the docker-compose cwd. - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z # - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z # - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z # - ../run/haproxy/run:/var/run:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/clusterx/haproxy/haproxy.cfg ================================================ # See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/ #--------------------------------------------------------------------- # Global settings #--------------------------------------------------------------------- global # to have these messages end up in /var/log/haproxy.log you will # need to: # # 1) configure syslog to accept network log events. This is done # by adding the '-r' option to the SYSLOGD_OPTIONS in # /etc/sysconfig/syslog # # 2) configure local2 events to go to the /var/log/haproxy.log # file. A line like the following can be added to # /etc/sysconfig/syslog # # local2.* /var/log/haproxy.log # log 127.0.0.1 local2 #chroot /var/lib/haproxy #pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon # turn on stats unix socket # stats socket /var/lib/haproxy/stats # utilize system-wide crypto-policies ## Disable cipher config to workaround ## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58] # ssl-default-bind-ciphers PROFILE=SYSTEM # ssl-default-server-ciphers PROFILE=SYSTEM # modern configuration # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12 ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets #--------------------------------------------------------------------- # common defaults that all the 'listen' and 'backend' sections will # use if not designated in their block #--------------------------------------------------------------------- defaults log global option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 2 # see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server timeout http-request 10s timeout queue 1m timeout connect 2s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 3s maxconn 3000 frontend id.acme.test mode http # Copy the haproxy.crt.pem file to /etc/haproxy bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem option httplog # ACLs based on typical "scanner noise" acl is_bad_url path -m end -i .php acl is_bad_url path -m end -i .asp # acl is_bad_url url -m sub ../.. # If the request matches one of the known "bad stuff" rules, reject. http-request deny if is_bad_url use_backend keycloak backend keycloak mode http stats enable stats uri /haproxy?status option httpchk option forwardfor http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost http-request add-header X-Forwarded-Proto https http-request add-header X-Forwarded-Port 1443 http-request redirect scheme https unless { ssl_fc } cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none balance roundrobin # Configure transport encryption with https / tls # http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check server kc1 acme-keycloak-1:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc1 server kc2 acme-keycloak-2:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc2 # Configure plain transport with http # server kc1 acme-keycloak-1:8080/auth check cookie kc1 # server kc2 acme-keycloak-2:8080/auth check cookie kc2 ================================================ FILE: deployments/local/clusterx/haproxy-database-ispn/Dockerfile ================================================ FROM haproxy:2.4.10-alpine COPY --chown=haproxy:haproxy "./acme.test+1.pem" "/etc/haproxy/haproxy.crt.pem" COPY --chown=haproxy:haproxy "./acme.test+1-key.pem" "/etc/haproxy/haproxy.crt.pem.key" ================================================ FILE: deployments/local/clusterx/haproxy-database-ispn/cache-ispn-database.xml ================================================ ================================================ FILE: deployments/local/clusterx/haproxy-database-ispn/docker-compose.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloakx environment: KC_HOSTNAME: id.acme.test:1443 ports: - "8080" - "8443" - "18787:8787" - "9990:9990" - "8790:8790" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/auth/"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy volumes: - ./cache-ispn-database.xml:/opt/keycloak/conf/cache-ispn-database.xml:z - ./patch/keycloak-model-infinispan-20.0.1.jar:/opt/keycloak/lib/lib/main/org.keycloak.keycloak-model-infinispan-20.0.1.jar:z - ./lib/infinispan-cachestore-jdbc-common-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc-common.jar:z - ./lib/infinispan-cachestore-jdbc-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc.jar:z command: - "--verbose" - "start" - "--auto-build" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" - "--hostname=id.acme.test:1443" - "--cache-config-file=cache-ispn-database.xml" # used by patched keycloak-model-infinispan jar to propagate cache update to the jdbc store - "-Dkeycloak.infinispan.ignoreSkipCacheStore=true" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloakx environment: KC_HOSTNAME: id.acme.test:1443 # depends_on: # acme-keycloak-db: # condition: service_healthy depends_on: acme-keycloak-1: condition: service_healthy volumes: - ./cache-ispn-database.xml:/opt/keycloak/conf/cache-ispn-database.xml:z - ./patch/keycloak-model-infinispan-20.0.1.jar:/opt/keycloak/lib/lib/main/org.keycloak.keycloak-model-infinispan-20.0.1.jar:z - ./lib/infinispan-cachestore-jdbc-common-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc-common.jar:z - ./lib/infinispan-cachestore-jdbc-13.0.10.Final.jar:/opt/keycloak/providers/infinispan-cachestore-jdbc.jar:z command: - "--verbose" - "start" - "--auto-build" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" - "--hostname=id.acme.test:1443" - "--cache-config-file=cache-ispn-database.xml" # used by patched keycloak-model-infinispan jar to propagate cache update to the jdbc store - "-Dkeycloak.infinispan.ignoreSkipCacheStore=true" acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: . volumes: # relative paths needs to be relative to the docker-compose cwd. - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/clusterx/haproxy-database-ispn/haproxy.cfg ================================================ # See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/ #--------------------------------------------------------------------- # Global settings #--------------------------------------------------------------------- global # to have these messages end up in /var/log/haproxy.log you will # need to: # # 1) configure syslog to accept network log events. This is done # by adding the '-r' option to the SYSLOGD_OPTIONS in # /etc/sysconfig/syslog # # 2) configure local2 events to go to the /var/log/haproxy.log # file. A line like the following can be added to # /etc/sysconfig/syslog # # local2.* /var/log/haproxy.log # log 127.0.0.1 local2 #chroot /var/lib/haproxy #pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon # turn on stats unix socket # stats socket /var/lib/haproxy/stats # utilize system-wide crypto-policies ## Disable cipher config to workaround ## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58] # ssl-default-bind-ciphers PROFILE=SYSTEM # ssl-default-server-ciphers PROFILE=SYSTEM # modern configuration # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12 ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets #--------------------------------------------------------------------- # common defaults that all the 'listen' and 'backend' sections will # use if not designated in their block #--------------------------------------------------------------------- defaults log global option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 2 # see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server timeout http-request 10s timeout queue 1m timeout connect 2s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 3s maxconn 3000 frontend id.acme.test mode http option httplog # Copy the haproxy.crt.pem file to /etc/haproxy bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem # ACLs based on typical "scanner noise" acl is_bad_url path -m end -i .php acl is_bad_url path -m end -i .asp # acl is_bad_url url -m sub ../.. # If the request matches one of the known "bad stuff" rules, reject. http-request deny if is_bad_url use_backend keycloak backend keycloak mode http stats enable stats uri /haproxy?status option httpchk http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost option forwardfor http-request add-header X-Forwarded-Proto https http-request add-header X-Forwarded-Port 1443 http-request redirect scheme https unless { ssl_fc } cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none balance roundrobin # Configure transport encryption with https / tls # http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check server kc1 acme-keycloak-1:8443/auth ssl verify none check cookie kc1 server kc2 acme-keycloak-2:8443/auth ssl verify none check cookie kc2 # Configure plain transport with http # server kc1 acme-keycloak-1:8080/auth check cookie kc1 # server kc2 acme-keycloak-2:8080/auth check cookie kc2 ================================================ FILE: deployments/local/clusterx/haproxy-database-ispn/readme.md ================================================ Keycloak with Database backed Sessions --- # Generate keys To generate the certificate and keys install https://github.com/FiloSottile/mkcert and run the following command: ``` mkcert -install mkcert "*.acme.test" ``` # Additional libraries In order to apply the cache store configuration the following libraries are needed that are currently not packaged with Keycloak: - infinispan-cachestore-jdbc-13.0.10.Final.jar - infinispan-cachestore-jdbc-common-13.0.10.Final.jar # Required patches ## keycloak-model-infinispan-20.0.1.jar * "Replace operation set wrong lifespan in remote infinispan database an… #15619" backported to 20.0.1 This fixes the computation of the cache item timestamp for remote stores. See: https://github.com/keycloak/keycloak/pull/15619#issuecomment-1324187372 * Changed CacheDecorators to support to ignore skipCacheStore hints backported to 20.0.1 This is necessary in order to propagate the cache write to the configured persistance store. This behaviour can be activated with the system property `-Dkeycloak.infinispan.ignoreSkipCacheStore=true` See: https://github.com/thomasdarimont/keycloak-project-example/blob/main/keycloak/patches/keycloak-model-infinispan-patch/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java#L25 ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/cache-ispn-remote.xml ================================================ true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory true org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml ================================================ services: acme-ispn-1: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-2: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloakx environment: DEBUG: "true" DEBUG_PORT: "*:8787" env_file: - ./haproxy-external-ispn.env volumes: - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: - "--verbose" - "start" # - "--auto-build" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter - "--hostname=id.acme.test:1443" - "--cache-config-file=cache-ispn-remote.xml" # Disable infinispan session affinity since we control this with the load-balancer - "--spi-sticky-session-encoder-infinispan-should-attach-route=false" - "-Djboss.site.name=site1" depends_on: acme-ispn-1: condition: service_healthy ports: - "8080" - "8443" - "9990:9990" - "8787:8787" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloakx env_file: - ./haproxy-external-ispn.env volumes: - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: - "--verbose" - "start" # - "--auto-build" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter - "--hostname=id.acme.test:1443" - "--cache-config-file=cache-ispn-remote.xml" # Disable infinispan session affinity since we control this with the load-balancer - "--spi-sticky-session-encoder-infinispan-should-attach-route=false" - "-Djboss.site.name=site1" depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-1: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-haproxy-lb: build: ../haproxy volumes: # relative paths needs to be relative to the docker-compose cwd. - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z # - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z # - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z # - ../run/haproxy/run:/var/run:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/haproxy-external-ispn.env ================================================ KEYCLOAK_FRONTEND_URL=https://id.acme.test:1443/auth KEYCLOAK_REMOTE_ISPN_HOSTNAME1=acme-ispn-1 KEYCLOAK_REMOTE_ISPN_HOSTNAME2=acme-ispn-2 KEYCLOAK_REMOTE_ISPN_USERNAME=keycloak KEYCLOAK_REMOTE_ISPN_PASSWORD=password KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD=password KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT=60000 KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT=5000 ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/haproxy.cfg ================================================ # See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/ #--------------------------------------------------------------------- # Global settings #--------------------------------------------------------------------- global # to have these messages end up in /var/log/haproxy.log you will # need to: # # 1) configure syslog to accept network log events. This is done # by adding the '-r' option to the SYSLOGD_OPTIONS in # /etc/sysconfig/syslog # # 2) configure local2 events to go to the /var/log/haproxy.log # file. A line like the following can be added to # /etc/sysconfig/syslog # # local2.* /var/log/haproxy.log # log 127.0.0.1 local2 #chroot /var/lib/haproxy #pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon # turn on stats unix socket # stats socket /var/lib/haproxy/stats # utilize system-wide crypto-policies ## Disable cipher config to workaround ## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58] # ssl-default-bind-ciphers PROFILE=SYSTEM # ssl-default-server-ciphers PROFILE=SYSTEM # modern configuration # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12 ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets #--------------------------------------------------------------------- # common defaults that all the 'listen' and 'backend' sections will # use if not designated in their block #--------------------------------------------------------------------- defaults # mode http log global option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 2 # see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server timeout http-request 10s timeout queue 1m timeout connect 2s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 3s maxconn 3000 frontend id.acme.test mode http option httplog # Copy the haproxy.crt.pem file to /etc/haproxy bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem # ACLs based on typical "scanner noise" acl is_bad_url path -m end -i .php acl is_bad_url path -m end -i .asp # acl is_bad_url url -m sub ../.. # If the request matches one of the known "bad stuff" rules, reject. http-request deny if is_bad_url use_backend keycloak backend keycloak mode http stats enable stats uri /haproxy?status option httpchk option forwardfor http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost http-request add-header X-Forwarded-Proto https http-request add-header X-Forwarded-Port 1443 http-request redirect scheme https unless { ssl_fc } cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none balance roundrobin # Configure transport encryption with https / tls # http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check server kc1 acme-keycloak-1:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc1 server kc2 acme-keycloak-2:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc2 # Configure plain transport with http # server kc1 acme-keycloak-1:8080/auth check cookie kc1 # server kc2 acme-keycloak-2:8080/auth check cookie kc2 frontend infinispan-lb mode tcp bind *:11222 use_backend infinispan backend infinispan mode tcp # option httpchk # http-check send meth GET uri /console/welcome ver HTTP/1.1 hdr Host localhost balance roundrobin server ispn1 acme-ispn-1:11222 check inter 2s downinter 1s fall 4 rise 3 server ispn2 acme-ispn-2:11222 check inter 2s downinter 1s fall 4 rise 3 ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/ispn/Dockerfile ================================================ FROM quay.io/infinispan/server:13.0.15.Final-1 USER 0 RUN true \ && microdnf clean all \ && microdnf install shadow-utils \ && microdnf update --nodocs \ && adduser ispn \ && microdnf remove shadow-utils \ && microdnf clean all RUN chown -R ispn:0 /opt/infinispan USER ispn CMD [ "-c", "infinispan-keycloak.xml" ] ENTRYPOINT [ "/opt/infinispan/bin/server.sh" ] ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/ispn/conf/infinispan-keycloak.xml ================================================ ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn/ispn/conf/users.properties ================================================ keycloak=password ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/cache-ispn-remote.xml ================================================ ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/docker-compose-haproxy-ispn-remote-database.yml ================================================ services: acme-ispn-1: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak-database.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z # - ./ispn/data/ispn-1:/opt/infinispan/server/mydata:z environment: DB_HOSTNAME: acme-ispn-db DB_USERNAME: ispn DB_PASSWORD: ispn DB_DATABASE: ispn healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-ispn-db: condition: service_healthy acme-ispn-2: build: ./ispn volumes: # relative paths needs to be relative to the docker-compose cwd. - ./ispn/conf/infinispan-keycloak-database.xml:/opt/infinispan/server/conf/infinispan-keycloak.xml:z - ./ispn/conf/users.properties:/opt/infinispan/server/conf/users.properties:z - ./ispn/ispn-server.jks:/opt/infinispan/server/conf/ispn-server.jks:z # - ./ispn/data/ispn-2:/opt/infinispan/server/mydata:z environment: DB_HOSTNAME: acme-ispn-db DB_USERNAME: ispn DB_PASSWORD: ispn DB_DATABASE: ispn healthcheck: test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):11222"] interval: 10s timeout: 5s retries: 5 depends_on: acme-ispn-db: condition: service_healthy acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloakx environment: DEBUG: "true" DEBUG_PORT: "*:8787" env_file: - ./haproxy-external-ispn.env volumes: - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: - "--verbose" - "start" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter - "--hostname=id.acme.test:1443" - "--spi-events-listener-jboss-logging-success-level=info" - "--spi-events-listener-jboss-logging-error-level=warn" - "--cache-config-file=cache-ispn-remote.xml" # Disable infinispan session affinity since we control this with the load-balancer - "--spi-sticky-session-encoder-infinispan-should-attach-route=false" - "-Djboss.site.name=site1" depends_on: acme-ispn-1: condition: service_healthy ports: - "8080" - "8443" - "9990:9990" - "8787:8787" acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloakx env_file: - ./haproxy-external-ispn.env volumes: - ./cache-ispn-remote.xml:/opt/keycloak/conf/cache-ispn-remote.xml:z - ./ispn/ispn-truststore.jks:/opt/keycloak/conf/ispn-truststore.jks:z # Patched cacerts without the expired certificates - ./ispn/cacerts:/etc/pki/ca-trust/extracted/java/cacerts:z command: - "--verbose" - "start" - "--https-certificate-file=/etc/x509/https/tls.crt" - "--https-certificate-key-file=/etc/x509/https/tls.key" - "--http-enabled=true" - "--http-relative-path=/auth" - "--http-port=8080" - "--proxy=passthrough" # Note upcoming versions of Keycloak.X will leave out the port in the hostname parameter - "--hostname=id.acme.test:1443" - "--spi-events-listener-jboss-logging-success-level=info" - "--spi-events-listener-jboss-logging-error-level=warn" - "--cache-config-file=cache-ispn-remote.xml" # Disable infinispan session affinity since we control this with the load-balancer - "--spi-sticky-session-encoder-infinispan-should-attach-route=false" - "-Djboss.site.name=site1" depends_on: acme-keycloak-db: condition: service_healthy acme-ispn-1: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-ispn-db: image: postgres:11.12 environment: POSTGRES_USER: ispn POSTGRES_PASSWORD: ispn POSTGRES_DB: ispn ports: - "56432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ispn"] interval: 10s timeout: 5s retries: 5 volumes: - ../run/postgres-ispn/data:/var/lib/postgresql/data:z acme-haproxy-lb: build: ../haproxy networks: default: aliases: - infinispan-lb volumes: # relative paths needs to be relative to the docker-compose cwd. - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:z # - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/haproxy/haproxy.crt.pem:z # - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/haproxy/haproxy.crt.pem.key:z # - ../run/haproxy/run:/var/run:z sysctls: - net.ipv4.ip_unprivileged_port_start=0 ports: - "1443:1443" depends_on: - acme-keycloak-1 - acme-keycloak-2 mailhog: # Web Interface: http://localhost:1080/# # Web API: http://localhost:1080/api/v2/messages image: mailhog/mailhog:v1.0.1@sha256:8d76a3d4ffa32a3661311944007a415332c4bb855657f4f6c57996405c009bea logging: driver: none ports: - "1080:8025" - "1025:1025" ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/haproxy-external-ispn.env ================================================ KEYCLOAK_FRONTEND_URL=https://id.acme.test:1443/auth KEYCLOAK_REMOTE_ISPN_HOSTNAME1=acme-ispn-1 KEYCLOAK_REMOTE_ISPN_HOSTNAME2=acme-ispn-2 KEYCLOAK_REMOTE_ISPN_USERNAME=keycloak KEYCLOAK_REMOTE_ISPN_PASSWORD=password KEYCLOAK_REMOTE_ISPN_TRUSTSTORE_PASSWORD=password KEYCLOAK_REMOTE_ISPN_SOCK_TIMEOUT=60000 KEYCLOAK_REMOTE_ISPN_CONN_TIMEOUT=5000 ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/haproxy.cfg ================================================ # See https://www.haproxy.com/blog/the-four-essential-sections-of-an-haproxy-configuration/ #--------------------------------------------------------------------- # Global settings #--------------------------------------------------------------------- global # to have these messages end up in /var/log/haproxy.log you will # need to: # # 1) configure syslog to accept network log events. This is done # by adding the '-r' option to the SYSLOGD_OPTIONS in # /etc/sysconfig/syslog # # 2) configure local2 events to go to the /var/log/haproxy.log # file. A line like the following can be added to # /etc/sysconfig/syslog # # local2.* /var/log/haproxy.log # log 127.0.0.1 local2 #chroot /var/lib/haproxy #pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon # turn on stats unix socket # stats socket /var/lib/haproxy/stats # utilize system-wide crypto-policies ## Disable cipher config to workaround ## Proxy 'id.acme.test': unable to set SSL cipher list to 'PROFILE=SYSTEM' for bind '*:1443' at [/usr/local/etc/haproxy/haproxy.cfg:58] # ssl-default-bind-ciphers PROFILE=SYSTEM # ssl-default-server-ciphers PROFILE=SYSTEM # modern configuration # generated via https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=modern&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 # Note that we left out no-tlsv12, since Keycloak currently uses tlsv12 ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets #--------------------------------------------------------------------- # common defaults that all the 'listen' and 'backend' sections will # use if not designated in their block #--------------------------------------------------------------------- defaults log global option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 2 # see https://cbonte.github.io/haproxy-dconv/2.4/configuration.html#3.9-timeout%20server timeout http-request 10s timeout queue 1m timeout connect 2s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 3s maxconn 3000 frontend id.acme.test mode http option httplog # Copy the haproxy.crt.pem file to /etc/haproxy bind *:1443 ssl crt /etc/haproxy/haproxy.crt.pem # ACLs based on typical "scanner noise" acl is_bad_url path -m end -i .php acl is_bad_url path -m end -i .asp # acl is_bad_url url -m sub ../.. # If the request matches one of the known "bad stuff" rules, reject. http-request deny if is_bad_url use_backend keycloak backend keycloak mode http stats enable stats uri /haproxy?status option httpchk option forwardfor http-check send meth GET uri /auth/realms/master ver HTTP/1.1 hdr Host localhost http-request add-header X-Forwarded-Proto https http-request add-header X-Forwarded-Port 1443 http-request redirect scheme https unless { ssl_fc } cookie KC_ROUTE insert indirect nocache secure httponly attr samesite=none balance roundrobin # Configure transport encryption with https / tls # http://cbonte.github.io/haproxy-dconv/2.4/configuration.html#check server kc1 acme-keycloak-1:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc1 server kc2 acme-keycloak-2:8443/auth ssl verify none check inter 2s downinter 1s fall 4 rise 3 cookie kc2 # Configure plain transport with http # server kc1 acme-keycloak-1:8080/auth check cookie kc1 # server kc2 acme-keycloak-2:8080/auth check cookie kc2 frontend infinispan-lb mode tcp bind *:11222 use_backend infinispan backend infinispan mode tcp # option httpchk # http-check send meth GET uri /console/welcome ver HTTP/1.1 hdr Host localhost balance roundrobin server ispn1 acme-ispn-1:11222 check inter 2s downinter 1s fall 4 rise 3 server ispn2 acme-ispn-2:11222 check inter 2s downinter 1s fall 4 rise 3 ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/ispn/Dockerfile ================================================ FROM quay.io/infinispan/server:13.0.15.Final-1 USER 0 RUN true \ && microdnf clean all \ && microdnf install shadow-utils \ && microdnf update --nodocs \ && adduser ispn \ && microdnf remove shadow-utils \ && microdnf clean all RUN chown -R ispn:0 /opt/infinispan RUN curl https://jdbc.postgresql.org/download/postgresql-42.5.2.jar --output /opt/infinispan/lib/postgresql-42.5.2.jar USER ispn CMD [ "-c", "infinispan-keycloak.xml" ] ENTRYPOINT [ "/opt/infinispan/bin/server.sh" ] ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/ispn/conf/infinispan-keycloak-database.xml ================================================ ================================================ FILE: deployments/local/clusterx/haproxy-external-ispn-database/ispn/conf/users.properties ================================================ keycloak=password ================================================ FILE: deployments/local/clusterx/keycloakx/Dockerfile ================================================ FROM quay.io/keycloak/keycloak:20.0.3 USER 0 # Add simple custom JGroups configuration COPY --chown=keycloak:keycloak ./cache-custom.xml /opt/keycloak/conf/cache-custom.xml # Add enhanced custom JGroups configuration with encryption support COPY --chown=keycloak:keycloak ./cache-custom-jgroups.xml /opt/keycloak/conf/cache-custom-jgroups.xml COPY --chown=keycloak:keycloak ./jgroups-multicast-enc.xml /opt/keycloak/conf/jgroups-multicast-enc.xml COPY --chown=keycloak:keycloak ./jgroups-multicast-diag.xml /opt/keycloak/conf/jgroups-multicast-diag.xml COPY --chown=keycloak:keycloak ./jgroups-jdbcping-enc.xml /opt/keycloak/conf/jgroups-jdbcping-enc.xml COPY --chown=keycloak:keycloak ./jgroups.p12 /opt/keycloak/conf/jgroups.p12 ## Workaround for adding the current certifcate to the cacerts truststore # Import certificate into cacerts truststore COPY --chown=keycloak:keycloak "./acme.test+1.pem" "/etc/x509/tls.crt.pem" RUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit USER keycloak ENTRYPOINT [ "/opt/keycloak/bin/kc.sh" ] ================================================ FILE: deployments/local/clusterx/keycloakx/cache-custom-jgroups-tcp.xml ================================================ ================================================ FILE: deployments/local/clusterx/keycloakx/cache-custom-jgroups.xml ================================================ ================================================ FILE: deployments/local/clusterx/keycloakx/cache-custom.xml ================================================ ================================================ FILE: deployments/local/clusterx/keycloakx/jgroups-jdbcping-enc.xml ================================================ ================================================ FILE: deployments/local/clusterx/keycloakx/jgroups-multicast-diag.xml ================================================ ================================================ FILE: deployments/local/clusterx/keycloakx/jgroups-multicast-enc.xml ================================================ ================================================ FILE: deployments/local/clusterx/nginx/docker-compose-nginx.yml ================================================ services: acme-keycloak-1: extends: file: ../docker-compose.yml service: acme-keycloakx environment: KC_HOSTNAME: id.acme.test:2443 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/auth/"] interval: 10s timeout: 5s retries: 5 depends_on: acme-keycloak-db: condition: service_healthy acme-keycloak-2: extends: file: ../docker-compose.yml service: acme-keycloakx environment: KC_HOSTNAME: id.acme.test:2443 # depends_on: # acme-keycloak-db: # condition: service_healthy depends_on: acme-keycloak-1: condition: service_healthy acme-keycloak-db: extends: file: ../docker-compose.yml service: acme-keycloak-db acme-nginx-lb: image: nginx:1.21.0-alpine # logging: # driver: none volumes: # relative paths needs to be relative to the docker-compose cwd. - ./nginx.conf:/etc/nginx/conf.d/default.conf:z # - ./dhparams:/etc/ssl/dhparams:z - ../../../../config/stage/dev/tls/acme.test+1.pem:/etc/nginx/certs/id.acme.test.crt:z - ../../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/nginx/certs/id.acme.test.key:z ports: - "2443:2443" depends_on: - acme-keycloak-1 - acme-keycloak-2 ================================================ FILE: deployments/local/clusterx/nginx/nginx.conf ================================================ server { listen 2443 ssl http2; server_name id.acme.test; # this is the internal Docker DNS, cache only for 30s resolver 127.0.0.11 valid=15s; # Time to wait to connect to an upstream server proxy_connect_timeout 3; proxy_send_timeout 10; proxy_read_timeout 15; send_timeout 10; # Disable access log access_log off; # generated via https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl_certificate /etc/nginx/certs/id.acme.test.crt; ssl_certificate_key /etc/nginx/certs/id.acme.test.key; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam # ssl_dhparam /etc/ssl/dhparams; # intermediate configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # HSTS (ngx_http_headers_module is required) (63072000 seconds) # add_header Strict-Transport-Security "max-age=63072000" always; location / { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://backend; # health_check feature only available in nginx-plus # health_check interval=2s # fails=2 # passes=5 # uri=/auth # match=signin # # match signin { # status 200; # header Content-Type = text/html; # body ~ "Sign In" # } } } upstream backend { # see https://docs.nginx.com/nginx/admin-guide/load-balancer/http-load-balancer/#choosing-a-load-balancing-method ip_hash; # http://nginx.org/en/docs/http/ngx_http_upstream_module.html#resolver_timeout # resolver_timeout only available in nginx-plus # resolver_timeout 5s; server acme-keycloak-1:8080 max_fails=1 fail_timeout=3s; server acme-keycloak-2:8080 max_fails=1 fail_timeout=3s; # Sticky sessions feature needs nginx-plus # sticky cookie srv_id expires=1h domain=.id.acme.test path=/auth; } ================================================ FILE: deployments/local/clusterx/readme.md ================================================ Keycloak.X Clustering Examples ---- # Prepare Copy `../../../config/stage/dev/tls/*.pem` to `{./haproxy ./keycloakx ./nginx}`. ``` cp ../../../config/stage/dev/tls/*.pem ./haproxy cp ../../../config/stage/dev/tls/*.pem ./keycloakx cp ../../../config/stage/dev/tls/*.pem ./nginx ``` # Run Keycloak.X cluster behind Nginx Start: ``` docker compose --env-file ../../../keycloak.env --file nginx/docker-compose-nginx.yml up --remove-orphans --build ``` Browse to: https://id.acme.test:2443/auth Stop: ``` docker compose --env-file ../../../keycloak.env --file nginx/docker-compose-nginx.yml down --remove-orphans ``` # Run Keycloak.X cluster behind HA-Proxy Start: ``` docker compose --env-file ../../../keycloak.env --file haproxy/docker-compose-haproxy.yml up --remove-orphans --build ``` Browse to: https://id.acme.test:1443/auth HA-Proxy status URL: https://id.acme.test:1443/haproxy?status Stop: ``` docker compose --env-file ../../../keycloak.env --file haproxy/docker-compose-haproxy.yml down --remove-orphans ``` # Run Keycloak.X cluster with database backed user sessions Start: ``` docker compose --env-file ../../../keycloak.env --file haproxy-database-ispn/docker-compose.yml up --remove-orphans --build ``` Browse to: https://id.acme.test:1443/auth Stop: ``` docker compose --env-file ../../../keycloak.env --file haproxy-database-ispn/docker-compose.yml down --remove-orphans ``` # Run Keycloak.X cluster behind HA-Proxy with external Infinispan Start: ``` docker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml up --remove-orphans --build ``` Browse to: https://id.acme.test:1443/auth Stop: ``` docker compose --env-file ../../../keycloak.env --file haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml down --remove-orphans ``` # Run Keycloak.X cluster behind HA-Proxy with external Infinispan and database persistence Start: ``` docker compose --env-file ../../../keycloak.env --file haproxy-external-ispn-database/docker-compose-haproxy-ispn-remote-database.yml up --remove-orphans --build ``` Browse to: https://id.acme.test:1443/auth Stop: ``` docker compose --env-file ../../../keycloak.env --file haproxy-external-ispn-database/docker-compose-haproxy-ispn-remote-database.yml down --remove-orphans ``` ================================================ FILE: deployments/local/dev/docker-compose-ci-github.yml ================================================ services: acme-keycloak: user: "${USER}:${GROUP}" # github ci only supports 2 CPUs max cpus: 2 build: context: "./keycloakx" dockerfile: "./Dockerfile-ci" ================================================ FILE: deployments/local/dev/docker-compose-grafana.yml ================================================ services: acme-grafana: image: grafana/grafana:11.6.1 ports: - 3000:3000 user: "1000:1000" environment: GF_SERVER_PROTOCOL: "https" GF_SERVER_HTTP_PORT: 3000 GF_SERVER_DOMAIN: "ops.acme.test" GF_SERVER_ROOT_URL: "%(protocol)s://%(domain)s:%(http_port)s/grafana" GF_SERVER_SERVE_FROM_SUB_PATH: "true" GF_SERVER_CERT_FILE: "/etc/grafana/cert.pem" GF_SERVER_CERT_KEY: "/etc/grafana/key.pem" GF_SECURITY_ADMIN_USER: "devops_fallback" GF_SECURITY_ADMIN_PASSWORD: "test" GF_AUTH_GENERIC_OAUTH_ENABLED: "true" GF_AUTH_GENERIC_OAUTH_NAME: "acme-ops" GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: "true" GF_AUTH_GENERIC_OAUTH_SCOPES: "openid email" GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "acme-ops-grafana" GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "acme-ops-grafana-secret" GF_AUTH_GENERIC_OAUTH_AUTH_URL: "https://id.acme.test:8443/auth/realms/acme-ops/protocol/openid-connect/auth" GF_AUTH_GENERIC_OAUTH_TOKEN_URL: "https://id.acme.test:8443/auth/realms/acme-ops/protocol/openid-connect/token" GF_AUTH_GENERIC_OAUTH_API_URL: "https://id.acme.test:8443/auth/realms/acme-ops/protocol/openid-connect/userinfo" # Generic client role mapping does not work because of '-' in the client name GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: "resource_access.grafana.roles[0]" GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: "true" GF_INSTALL_PLUGINS: "grafana-clock-panel,grafana-simple-json-datasource,grafana-piechart-panel" GF_SMTP_ENABLED: "true" GF_SMTP_HOST: "mail" GF_SMTP_PORT: 1025 volumes: # - ./run/grafana:/var/lib/grafana:z # - ../../../config/stage/dev/grafana/provisioning:/etc/grafana/provisioning:z - ../../../config/stage/dev/grafana/provisioning/dashboards/dashboard.yml:/etc/grafana/provisioning/dashboards/dashboard.yml:z - ../../../config/stage/dev/grafana/provisioning/dashboards/keycloak-metrics_rev1.json:/etc/grafana/provisioning/dashboards/keycloak-metrics_rev1.json:z - ../../../config/stage/dev/grafana/provisioning/datasources/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:z - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/grafana/cert.pem:z - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/grafana/key.pem:z - ${CA_ROOT_CERT:-}:/etc/ssl/certs/ca-cert-acme-root.crt:z extra_hosts: # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal - "id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" ================================================ FILE: deployments/local/dev/docker-compose-graylog.yml ================================================ services: # MongoDB: https://hub.docker.com/_/mongo/ acme-graylog-mongo: labels: - "co.elastic.logs/enabled=false" image: mongo:4.2 volumes: - ./run/graylog/data/mongodb:/data/db:z command: --quiet # Elasticsearch: https://www.elastic.co/guide/en/elasticsearch/reference/7.10/docker.html acme-graylog-elasticsearch: labels: - "co.elastic.logs/enabled=false" image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2 environment: - http.host=0.0.0.0 - transport.host=localhost - network.host=0.0.0.0 - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Dlog4j2.formatMsgNoLookups=true" ulimits: memlock: soft: -1 hard: -1 deploy: resources: limits: memory: 1g # Graylog: https://hub.docker.com/r/graylog/graylog/ acme-graylog: labels: - "co.elastic.logs/enabled=false" # image: graylog/graylog:4.1 build: context: "../../../config/stage/dev/tls" dockerfile: "../../../../deployments/local/dev/graylog/Dockerfile" environment: # CHANGE ME (must be at least 16 characters)! - GRAYLOG_PASSWORD_SECRET=somepasswordpepper # Password: admin - GRAYLOG_ROOT_PASSWORD_SHA2=8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 - GRAYLOG_HTTP_PUBLISH_URI=https://apps.acme.test:9000/ - GRAYLOG_HTTP_EXTERNAL_URI=https://apps.acme.test:9000/ - GRAYLOG_HTTP_ENABLE_TLS=true # certs added in custom docker build - GRAYLOG_HTTP_TLS_CERT_FILE=/usr/share/graylog/data/config/ssl/cert.crt - GRAYLOG_HTTP_TLS_KEY_FILE=/usr/share/graylog/data/config/ssl/key.key # Enable E-mail configuration for alerts - GRAYLOG_TRANSPORT_EMAIL_ENABLED=true - GRAYLOG_TRANSPORT_EMAIL_USE_AUTH=false - GRAYLOG_TRANSPORT_EMAIL_HOSTNAME=mail - GRAYLOG_TRANSPORT_EMAIL_PORT=1025 - GRAYLOG_TRANSPORT_EMAIL_USE_TLS=false - GRAYLOG_TRANSPORT_EMAIL_USE_SSL=false - GRAYLOG_TRANSPORT_EMAIL_SUBJECT_PREFIX=GrayLog - GRAYLOG_TRANSPORT_EMAIL_FROM_EMAIL=graylog@id.acme.test # Install keycloak message extractors - GRAYLOG_CONTENT_PACKS_AUTO_INSTALL=iam-keycloak-content-pack-v1.json - JAVA_TOOL_OPTIONS=-Dlog4j2.formatMsgNoLookups=true entrypoint: /usr/bin/tini -- wait-for-it elasticsearch:9200 -- /docker-entrypoint.sh volumes: - graylog_journal:/usr/share/graylog/data/journal - ./graylog/contentpacks:/usr/share/graylog/data/contentpacks:z # restart: always depends_on: - acme-graylog-mongo - acme-graylog-elasticsearch # healthcheck: # test: ["CMD-SHELL", "curl -k https://$$(ip route get 1.2.3.4 | awk '{print $$7}'):9000"] # interval: 10s # timeout: 5s # retries: 5 links: - acme-graylog-elasticsearch:elasticsearch - acme-graylog-mongo:mongo ports: # Graylog web interface and REST API - 9000:9000 # Syslog TCP - 1514:1514 # Syslog UDP - 1514:1514/udp # Filebeat - 5044:5044 # GELF TCP - 12201:12201 # GELF UDP - 12201:12201/udp # acme-filebeat: # labels: # - "co.elastic.logs/enabled=false" # image: docker.elastic.co/beats/filebeat:7.13.4 # user: root # # Need to override user so we can access the log files, and docker.sock # command: > # ./filebeat -e -c /etc/motd # -E "filebeat.inputs=[{type:docker,containers:{ids:'*'}}]" # -E "output.logstash.hosts=['acme-graylog:5044']" # volumes: # - /var/lib/docker/containers:/var/lib/docker/containers:z # - /var/run/docker.sock:/var/run/docker.sock:z # acme-keycloak: # labels: # - "co.elastic.logs/enabled=true" # - "co.elastic.logs/multiline.type=pattern" # - "co.elastic.logs/multiline.pattern='^\\['" # - "co.elastic.logs/multiline.negate=true" # - "co.elastic.logs/multiline.match=after" acme-keycloak: volumes: # Add logstash-gelf-module - ./graylog/modules/logstash-gelf-1.14.1/biz:/opt/jboss/keycloak/modules/system/layers/base/biz:z # Enable logging to logstash gelf - ./graylog/cli/0020-onstart-setup-graylog-logging.cli:/opt/jboss/startup-scripts/0020-onstart-setup-graylog-logging.cli:z volumes: graylog_journal: driver: local ================================================ FILE: deployments/local/dev/docker-compose-keycloak.yml ================================================ services: acme-keycloak: #image: quay.io/keycloak/keycloak:$KEYCLOAK_VERSION build: context: "./keycloak" dockerfile: "./Dockerfile" # user: "${USER}:${GROUP}" env_file: - ./keycloak-common.env - ./keycloak-http.env environment: # KEYCLOAK_USER: "admin" # KEYCLOAK_PASSWORD: "admin" DB_VENDOR: "h2" KEYCLOAK_THEME_CACHING: "false" KEYCLOAK_THEME_TEMPLATE_CACHING: "false" PROXY_ADDRESS_FORWARDING: "true" # force usage for standalone.xml for local dev KEYCLOAK_CONFIG_FILE: "standalone.xml" # Exposes Metrics via http://localhost:9990/metrics KEYCLOAK_STATISTICS: all #JAVA_OPTS: "-XX:MaxRAMPercentage=80 -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -Djava.net.preferIPv4Stack=true -XX:FlightRecorderOptions=stackdepth=256" mem_limit: 1024m mem_reservation: 1024m cpus: 2 # KEYCLOAK_IMPORT: "/opt/jboss/imex/custom-realm.json" # use `docker-compose --env-file custom-keycloak.env up` to populate the KEYCLOAK_CONFIG_FILE variable. command: - "--debug" - "*:8787" - "--server-config" - "$KEYCLOAK_CONFIG_FILE" - "-b" - "0.0.0.0" - "-bmanagement" - "0.0.0.0" - "-Dwildfly.statistics-enabled=true" # - "-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog" extra_hosts: # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal - "id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" - "apps.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" ports: - "8080:8080" - "8443:8443" - "8790:8790" - "9990:9990" - "127.0.0.1:8787:8787" volumes: - ../../../keycloak/themes/apps:/opt/jboss/keycloak/themes/apps:z - ../../../keycloak/themes/internal:/opt/jboss/keycloak/themes/internal:z - ../../../keycloak/themes/internal-modern:/opt/jboss/keycloak/themes/internal-modern:z - ../../../keycloak/config/profile.properties:/opt/jboss/keycloak/standalone/configuration/profile.properties:z - ../../../keycloak/imex:/opt/jboss/imex:z # This will exposes *.sh and *.cli startup scripts that are executed by Keycloak. - ../../../keycloak/cli:/opt/jboss/startup-scripts:z - ./run/keycloak/data:/opt/jboss/keycloak/standalone/data:z - ./run/keycloak/logs:/opt/jboss/keycloak/standalone/logs:z - ./run/keycloak/perf:/opt/jboss/keycloak/standalone/perf:z # Add third-party extensions # - ./keycloak-ext/keycloak-metrics-spi-2.5.3-SNAPSHOT.jar:/opt/jboss/keycloak/standalone/deployments/keycloak-metrics-spi-2.5.3-SNAPSHOT.jar:z - ./keycloak-ext/keycloak-home-idp-discovery-18.0.0.jar:/opt/jboss/keycloak/standalone/deployments/keycloak-home-idp-discovery.jar:z ================================================ FILE: deployments/local/dev/docker-compose-keycloakx.yml ================================================ services: acme-keycloak: #image: quay.io/keycloak/keycloak:$KEYCLOAK_VERSION build: context: "./keycloakx" dockerfile: "./Dockerfile" # user: "${USER}:${GROUP}" env_file: - ./keycloak-common.env - ./keycloak-http.env environment: # Enable remote debugging KC_DEBUG: "true" KC_DEBUG_PORT: "*:8787" # DEBUG_SUSPEND: "y" JAVA_OPTS: "-XX:MaxRAMPercentage=80 -XX:+UseG1GC -Djava.net.preferIPv4Stack=true -Djava.security.egd=file:/dev/urandom" # Append additional JVM Options besides the default JVM_OPTS # See: https://github.com/keycloak/keycloak/blob/main/distribution/server-x-dist/src/main/content/bin/kc.sh#L66 # -XX:+PrintFlagsFinal JAVA_OPTS_APPEND: "--show-version" # Force usage of HTTP 1.1 to be able to honor HTTP Header size limits # QUARKUS_HTTP_HTTP2: "false" # QUARKUS_HTTP_LIMITS_MAX_HEADER_SIZE: "64k" # Allow access via visualvm and jmc (remote jmx connection to localhost 8790 without ssl) # see https://docs.oracle.com/en/java/javase/11/management/monitoring-and-management-using-jmx-technology.html#GUID-D4CBA2D6-2E24-4856-A7D8-62B3DFFB76EA # JAVA_TOOL_OPTIONS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/opt/keycloak/conf/jmxremote.password -Dcom.sun.management.jmxremote.ssl=false -XX:FlightRecorderOptions=stackdepth=256" #JAVA_TOOL_OPTIONS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -XX:FlightRecorderOptions=stackdepth=256" # JAVA_TOOL_OPTIONS="" jcmd 1 JFR.start duration=1m settings=profile name=debug filename=/opt/keycloak/perf/debug.jfr # JAVA_TOOL_OPTIONS="" jcmd 1 JFR.dump name=debug filename=/opt/keycloak/perf/debug.jfr JAVA_TOOL_OPTIONS: "-XX:+PrintCommandLineFlags -XX:FlightRecorderOptions=stackdepth=256 -XX:+FlightRecorder -XX:StartFlightRecording=duration=200s,filename=/opt/keycloak/perf/debug.jfr,name=debug" mem_limit: 2048m mem_reservation: 2048m cpus: 4 command: - "--verbose" - "start-dev" - "--http-enabled=true" - "--http-port=8080" - "--https-client-auth=request" - "--http-relative-path=auth" # - "--http-management-port=9090" - "--metrics-enabled=true" # see: https://www.keycloak.org/observability/event-metrics - "--event-metrics-user-enabled=true" - "--event-metrics-user-tags=realm,idp,clientId" # see: https://www.keycloak.org/observability/configuration-metrics - "--http-metrics-histograms-enabled=true" - "--cache-metrics-histograms-enabled=false" # - "--http-metrics-slos=5,10,25,50,250,500,1000,2500,5000,10000" - "--health-enabled=true" # see https://www.keycloak.org/server/features # ,declarative-ui # Token Exchange V1 # - "--features=preview,transient-users,dynamic-scopes,admin-fine-grained-authz:v1,quick-theme,declarative-ui,oid4vc-vci" # Token Exchange V2 - "--features=preview,transient-users,dynamic-scopes,admin-fine-grained-authz,quick-theme,declarative-ui,oid4vc-vci" - "--features-disabled=token-exchange" # - "--features-disabled=persistent-user-sessions" - "--cache=local" - "--proxy-headers=xforwarded" # - "--log-console-output=json" - "--https-protocols=TLSv1.3,TLSv1.2" - "--db-debug-jpql=true" - "--db-log-slow-queries-threshold=5000" - "--spi-theme-cache-themes=false" - "--spi-theme-cache-templates=false" - "--spi-theme-static-max-age=-1" - "--spi-events-listener-email-exclude-events=LOGIN,LOGIN_ERROR,UPDATE_TOTP,REMOVE_TOTP" - "--spi-events-listener-email-include-events=UPDATE_PASSWORD" - "--spi-events-listener-jboss-logging-success-level=info" - "--spi-events-listener-jboss-logging-error-level=warn" # - "--spi-oauth2-token-exchange-default=standard" # Disable automatic migration # - "--spi-connections-jpa-legacy-migration-strategy=manual" # - "--spi-truststore-file-file=/opt/keycloak/conf/truststore.p12" # - "--spi-truststore-file-password=changeit" # Example for static overrides for .well-known/openid-configuration endpoint # - "--spi-well-known-openid-configuration-openid-configuration-override=/opt/keycloak/conf/openid-config.json" # - "--spi-well-known-openid-configuration-include-client-scopes=true" # Workaround to allow logouts via old Keycloak Admin-Console # see: org.keycloak.protocol.oidc.endpoints.LogoutEndpoint.logout(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String) - "--spi-login-protocol-openid-connect-legacy-logout-redirect-uri=false" # - "--log-level=info,io.quarkus.vertx:debug,io.netty:debug,io.vertx:debug" # - "--log-level=info,org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider:debug" # - "--log-level=info,org.hibernate:debug" # - "--log-level=info,org.hibernate.SQL:debug" # - "--log-level=info,org.hibernate.SQL:debug,org.hibernate.type.descriptor.sql.BasicBinder:trace" - "--log-level=info,org.hibernate.SQL_SLOW:info,com.github.thomasdarimont.keycloak.custom.health.CustomHealthChecks:info,com.github.thomasdarimont.keycloak.custom:debug,com.arjuna.ats.jta:off,org.keycloak.services.resources.admin.permissions:debug,org.hibernate.SQL_SLOW:info" # - "--verbose" # - "-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog" - "--spi-admin-realm-restapi-extension-custom-admin-resources-users-provisioning-required-realm-role=user-modifier-acme" - "--spi-admin-realm-restapi-extension-custom-admin-resources-users-provisioning-managed-attribute-pattern=(.*)" - "-Dio.netty.http2.maxHeaderListSize=16384" extra_hosts: # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal # the special host-gateway value resolves to the internal IP address of the host. # see: https://docs.docker.com/reference/cli/docker/container/run/#add-host - "id.acme.test:host-gateway" - "apps.acme.test:host-gateway" - "ops.acme.test:host-gateway" - "account-service:host-gateway" - "samlidp.acme.test:host-gateway" - "authzen.openid.test:host-gateway" ports: - "8080:8080" # - "9090:9090" - "8443:8443" - "9000:9000" - "127.0.0.1:8790:8790" - "127.0.0.1:8787:8787" volumes: - ../../../keycloak/themes:/opt/keycloak/themes:z - ../../../keycloak/config/quarkus.properties:/opt/keycloak/conf/quarkus.properties:z - ../../../keycloak/config/openid-config.json:/opt/keycloak/conf/openid-config.json:z - ../../../keycloak/config/jmxremote.password:/opt/keycloak/conf/jmxremote.password:ro - ../../../keycloak/imex:/opt/keycloak/imex:z - ./run/keycloakx/data265:/opt/keycloak/data:z - ./run/keycloakx/logs:/opt/keycloak/logs:z - ./run/keycloakx/perf:/opt/keycloak/perf:z # Add keycloak extensions # - ../../../keycloak/extensions/target/extensions.jar:/opt/keycloak/providers/extensions.jar:z - ../../../keycloak/extensions/target/extensions-jar-with-dependencies.jar:/opt/keycloak/providers/extensions.jar:z # Add third-party extensions # - ./keycloak-ext/keycloak-metrics-spi-3.0.0.jar:/opt/keycloak/providers/keycloak-metrics-spi.jar:z - ./keycloak-ext/keycloak-restrict-client-auth-25.0.0.jar:/opt/keycloak/providers/keycloak-restrict-client-auth.jar:z - ./keycloak-ext/keycloak-home-idp-discovery-25.0.0.jar:/opt/keycloak/providers/keycloak-home-idp-discovery.jar:z - ./keycloak-ext/apple-identity-provider-1.13.0.jar:/opt/keycloak/providers/apple-identity-provider.jar:z - ./keycloak-ext/keycloak-benchmark-dataset-0.7.jar:/opt/keycloak/providers/keycloak-benchmark-dataset.jar:z # - ./keycloak-ext/flyweight-user-storage-provider-extension-1.0.0.0-SNAPSHOT.jar:/opt/keycloak/providers/flyweight-user-storage-provider-extension-1.0.0.0-SNAPSHOT.jar:z - ./keycloakx/health_check.sh:/health_check.sh:z healthcheck: test: ["CMD", "./health_check.sh"] interval: 10s timeout: 5s retries: 10 ================================================ FILE: deployments/local/dev/docker-compose-mssql.yml ================================================ services: acme-keycloak-db: build: context: "../../../config/stage/dev/tls" dockerfile: "../../../../deployments/local/dev/sqlserver/Dockerfile" environment: SA_PASSWORD: "Keycloak123" ACCEPT_EULA: "Y" MSSQL_PID: "Standard" ports: - "5434:1433" healthcheck: test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -Q "SELECT 1" || exit 1 interval: 10s timeout: 5s retries: 5 command: /bin/bash /var/opt/mssql/docker-entrypoint.sh volumes: # - ./run/mssql/data:/var/opt/mssql/data:z - ./sqlserver/mssql.conf:/var/opt/mssql/mssql.conf:z - ./sqlserver/docker-entrypoint.sh:/var/opt/mssql/docker-entrypoint.sh:z - ./sqlserver/db-init.sh:/opt/mssql/bin/db-init.sh:z - ./sqlserver/db-init.sql:/opt/mssql-tools/bin/db-init.sql:z acme-keycloak: env_file: - ./keycloak-db.env environment: # See MSSQL JDBC URL parameters: https://docs.microsoft.com/en-us/sql/connect/jdbc/connecting-with-ssl-encryption?view=sql-server-ver15 DB_VENDOR: mssql DB_PASSWORD: Keycloak123 DB_SCHEMA: dbo JDBC_PARAMS: encrypt=true;trustServerCertificate=true;applicationName=keycloak # keycloak-x KC_DB: mssql KC_DB_PASSWORD: Keycloak123 KC_DB_SCHEMA: dbo # https://www.keycloak.org/server/db#_unicode_support_for_a_microsoft_sql_server_database KC_DB_URL_PROPERTIES: ";encrypt=true;trustServerCertificate=true;applicationName=keycloak;sendStringParametersAsUnicode=false" # Workaround for Keycloak.X and MSSQL, see: https://groups.google.com/g/keycloak-dev/c/ZGuX0CFWqwo/m/fhhT8qnOBgAJ?utm_medium=email&utm_source=footer&pli=1 KC_TRANSACTION_XA_ENABLED: "false" #KC_DB_DRIVER: "com.microsoft.sqlserver.jdbc.SQLServerDriver" volumes: # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z depends_on: acme-keycloak-db: condition: service_healthy ================================================ FILE: deployments/local/dev/docker-compose-mysql.yml ================================================ services: acme-keycloak-db: build: context: "../../../config/stage/dev/tls" dockerfile: "../../../../deployments/local/dev/mysql/Dockerfile" environment: MYSQL_ROOT_PASSWORD: "mysql" MYSQL_USER: "keycloak" MYSQL_PASSWORD: "keycloak" MYSQL_DATABASE: "keycloak" MYSQL_ROOT_HOST: "%" MYSQL_HOST: localhost command: # Certificates are added in the Dockerfile with the proper permissions for mysql - "mysqld" - "--bind-address=0.0.0.0" - "--require_secure_transport=ON" - "--ssl-ca=/etc/certs/ca.crt" - "--ssl-cert=/etc/certs/server.crt" - "--ssl-key=/etc/certs/server.key" ports: - "53306:3306" healthcheck: test: "mysqladmin --user root --password=mysql status" interval: 10s timeout: 5s retries: 5 volumes: - ./run/mysql/data:/var/lib/mysql:z acme-keycloak: env_file: - ./keycloak-db.env environment: # See MySQL JDBC URL parameters: https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html DB_VENDOR: MYSQL JDBC_PARAMS: requireSSL=true&enabledTLSProtocols=TLSv1.2 # keycloak-x KC_DB: mysql KC_DB_URL_PROPERTIES: "?requireSSL=true&enabledTLSProtocols=TLSv1.2" KC_DB_SCHEMA: "keycloak" volumes: # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z depends_on: acme-keycloak-db: condition: service_healthy ================================================ FILE: deployments/local/dev/docker-compose-nats.yml ================================================ services: acme-nats: image: 'nats:2.10.14' ports: - "8222:8222" - "4222:4222" command: -c /etc/my-server.conf --name acme-nats -p 4222 volumes: - ./nats/server.conf:/etc/my-server.conf ================================================ FILE: deployments/local/dev/docker-compose-opa.yml ================================================ services: acme-opa: image: openpolicyagent/opa:1.15.1 platform: linux/amd64 command: - run - --server - --addr - :8181 - --set - "decision_logs.console=true" # Watch for changes in policy folder - "--watch" - "/policies" volumes: - ../../../config/stage/dev/opa/iam:/policies/iam:z - ../../../config/stage/dev/opa/policies/keycloak:/policies/keycloak:z ports: - "18181:8181" ================================================ FILE: deployments/local/dev/docker-compose-openldap.yml ================================================ services: openldap: image: osixia/openldap:1.5.0 env_file: - ./keycloak-common.env - ./keycloak-http.env - ./keycloak-openldap.env environment: # use LDAP_LOG_LEVEL: 256 for verbose logging LDAP_LOG_LEVEL: "0" # LDAP_LOG_LEVEL: "256" LDAP_ORGANISATION: "Acme Inc." LDAP_DOMAIN: "corp.acme.local" LDAP_BASE_DN: "" # admin User: cn=admin,dc=corp,dc=acme,dc=local LDAP_ADMIN_PASSWORD: "admin" LDAP_CONFIG_PASSWORD: "config" LDAP_READONLY_USER: "false" #LDAP_READONLY_USER_USERNAME: "readonly" #LDAP_READONLY_USER_PASSWORD: "readonly" LDAP_RFC2307BIS_SCHEMA: "false" LDAP_BACKEND: "mdb" LDAP_TLS: "true" LDAP_TLS_CRT_FILENAME: "ldap.crt" LDAP_TLS_KEY_FILENAME: "ldap.key" LDAP_TLS_DH_PARAM_FILENAME: "dhparam.pem" LDAP_TLS_CA_CRT_FILENAME: "ca.crt" LDAP_TLS_ENFORCE: "false" LDAP_TLS_CIPHER_SUITE: "SECURE256:-VERS-SSL3.0" LDAP_TLS_VERIFY_CLIENT: "demand" LDAP_REPLICATION: "false" #LDAP_REPLICATION_CONFIG_SYNCPROV: 'binddn="cn=admin,cn=config" bindmethod=simple credentials="$$LDAP_CONFIG_PASSWORD" searchbase="cn=config" type=refreshAndPersist retry="60 +" timeout=1 starttls=critical' #LDAP_REPLICATION_DB_SYNCPROV: 'binddn="cn=admin,$$LDAP_BASE_DN" bindmethod=simple credentials="$$LDAP_ADMIN_PASSWORD" searchbase="$$LDAP_BASE_DN" type=refreshAndPersist interval=00:00:00:10 retry="60 +" timeout=1 starttls=critical' #LDAP_REPLICATION_HOSTS: "#PYTHON2BASH:['ldap://ldap.example.org','ldap://ldap2.example.org']" KEEP_EXISTING_CONFIG: "false" LDAP_REMOVE_CONFIG_AFTER_SETUP: "false" LDAP_SSL_HELPER_PREFIX: "ldap" LDAP_SEED_INTERNAL_LDIF_PATH: "/acme/openldap/ldif/10-bootstrap.ldif" tty: true stdin_open: true volumes: - /var/lib/ldap - /etc/ldap/slapd.d - /container/service/slapd/assets/certs/ - ../../../config/stage/dev/openldap/demo.ldif:/acme/openldap/ldif/10-bootstrap.ldif:z ports: - "1389:389" - "1636:636" # For replication to work correctly, domainname and hostname must be # set correctly so that "hostname"."domainname" equates to the # fully-qualified domain name for the host. domainname: "acme.test" hostname: "ldap1" command: [ "--copy-service", "--loglevel", "debug" ] phpldapadmin: image: osixia/phpldapadmin:latest environment: PHPLDAPADMIN_LDAP_HOSTS: "openldap" PHPLDAPADMIN_HTTPS: "false" ports: - "17080:80" depends_on: - openldap ================================================ FILE: deployments/local/dev/docker-compose-oracle.yml ================================================ services: acme-keycloak-db: build: context: "../../../config/stage/dev/tls" dockerfile: "../../../../deployments/local/dev/oracle/Dockerfile" environment: ORACLE_PASSWORD: 'secret' APP_USER: 'keycloak' APP_USER_PASSWORD: 'keycloak' ports: - "1521:1521" healthcheck: test: ["CMD-SHELL", "healthcheck.sh"] interval: 10s timeout: 5s retries: 5 volumes: - keycloak-data-oracle1:/opt/oracle/oradata:z acme-keycloak: environment: KC_DB: oracle KC_DB_DRIVER: "oracle.jdbc.OracleDriver" KC_DB_URL: "jdbc:oracle:thin:@acme-keycloak-db:1521/FREEPDB1" KC_TRANSACTION_XA_ENABLED: "false" KC_DB_USERNAME: 'keycloak' KC_DB_PASSWORD: 'keycloak' depends_on: acme-keycloak-db: condition: service_healthy volumes: keycloak-data-oracle1: name: keycloak-data-oracle1 ================================================ FILE: deployments/local/dev/docker-compose-postgres.yml ================================================ services: acme-keycloak-db: build: context: "../../../config/stage/dev/tls" dockerfile: "../../../../deployments/local/dev/postgresql/Dockerfile" environment: POSTGRES_USER: keycloak POSTGRES_PASSWORD: keycloak POSTGRES_DB: keycloak command: # Certificates are added in the Dockerfile with the proper permissions for postgresql - "-c" - "ssl=on" - "-c" - "ssl_cert_file=/var/lib/postgresql/server.crt" - "-c" - "ssl_key_file=/var/lib/postgresql/server.key" - "-c" - "shared_preload_libraries=pg_stat_statements" - "-c" - "pg_stat_statements.track=all" - "-c" - "max_connections=200" ports: - "55432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U keycloak"] interval: 10s timeout: 5s retries: 5 volumes: - ./run/postgres/data265:/var/lib/postgresql/data:z acme-keycloak: env_file: - ./keycloak-db.env environment: # default 0 QUARKUS_DATASOURCE_JDBC_INITIAL_SIZE: 64 # default 0 QUARKUS_DATASOURCE_JDBC_MIN_SIZE: 64 # default 20, see https://quarkus.io/version/2.13/guides/all-config#quarkus-agroal_quarkus.datasource.jdbc.max-size QUARKUS_DATASOURCE_JDBC_MAX_SIZE: 64 # default 20, see https://quarkus.io/version/2.13/guides/all-config#quarkus-vertx_quarkus.vertx.worker-pool-size QUARKUS_VERTX_WORKER_POOL_SIZE: 64 # default 5 seconds, see https://quarkus.io/version/2.13/guides/all-config#quarkus-agroal_quarkus.datasource.jdbc.acquisition-timeout QUARKUS_DATASOURCE_JDBC_ACQUISITION_TIMEOUT: 20 KC_DB: postgres # See postgres JDBC URL parameters: https://jdbc.postgresql.org/documentation/head/connect.html KC_DB_URL_PROPERTIES: "?ApplicationName=keycloak&ssl=true&sslmode=verify-ca&sslrootcert=/etc/x509/ca/tls.crt" volumes: # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z # IntelliJ currently does not support the depends_on condition syntax depends_on: acme-keycloak-db: condition: service_healthy ================================================ FILE: deployments/local/dev/docker-compose-prometheus.yml ================================================ services: acme-prometheus: image: prom/prometheus:v2.53.4 user: "65534:1000" ports: - "9090:9090" volumes: - ../../../config/stage/dev/prometheus:/etc/prometheus:z # - ./run/prometheus:/prometheus:z command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.console.libraries=/usr/share/prometheus/console_libraries' - '--web.console.templates=/usr/share/prometheus/consoles' ================================================ FILE: deployments/local/dev/docker-compose-provisioning.yml ================================================ services: acme-keycloak-provisioning: image: quay.io/adorsys/keycloak-config-cli:6.5.0-26.5.4 env_file: # generated via start.java - ./../../../generated.env.tmp environment: KEYCLOAK_AVAILABILITYCHECK_ENABLED: "true" KEYCLOAK_AVAILABILITYCHECK_TIMEOUT: "120s" # see: https://github.com/adorsys/keycloak-config-cli/blob/v5.0.0/CHANGELOG.md IMPORT_FILES_LOCATION: "/config/*" # IMPORT_PATH: "/config" IMPORT_CACHE_ENABLED: "true" # IMPORT_FORCE: "false" IMPORT_VAR_SUBSTITUTION_ENABLED: "true" # IMPORT_VARSUBSTITUTION: "true" IMPORT_VALIDATE: "true" # See https://github.com/adorsys/keycloak-config-cli#log-level #LOGGING_LEVEL_KEYCLOAK_CONFIG_CLI: "DEBUG" # Note: the above does not work but _KCC does LOGGING_LEVEL_KCC: "DEBUG" # Veeeeery verbose HTTP log! #LOGGING_LEVEL_HTTP: "DEBUG" #LOGGING_LEVEL_ROOT: "DEBUG" LOGGING_LEVEL_ROOT: "INFO" volumes: - ../../../config/stage/dev/realms:/config:z depends_on: acme-keycloak: condition: service_healthy ================================================ FILE: deployments/local/dev/docker-compose-simplesaml.yml ================================================ services: # see: https://hub.docker.com/r/kenchan0130/simplesamlphp saml-idp: build: context: "simplesaml/idp" dockerfile: "./Dockerfile" ports: - "18380:8080" environment: # adapt URLs for different realm if necessary SIMPLESAMLPHP_SP_ENTITY_ID: https://id.acme.test:8443/auth/realms/acme-apps SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE: https://id.acme.test:8443/auth/realms/acme-apps/broker/idp-simplesaml/endpoint SIMPLESAMLPHP_SP_SINGLE_LOGOUT_SERVICE: https://id.acme.test:8443/auth/realms/acme-apps/broker/idp-simplesaml/endpoint SIMPLESAMLPHP_IDP_ADMIN_PASSWORD: admin SIMPLESAMLPHP_IDP_SESSION_DURATION_SECONDS: 600 extra_hosts: # ${DOCKER_HOST_IP:-172.17.0.1} is host.docker.internal - "id.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" volumes: - ./simplesaml/idp/authsources.php:/var/www/simplesamlphp/config/authsources.php:z ================================================ FILE: deployments/local/dev/docker-compose-tls.yml ================================================ services: acme-keycloak: env_file: - ./keycloak-common.env - ./keycloak-http.env - ./keycloak-tls.env volumes: # This configures the key and certificate for HTTPS. - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/https/tls.crt:z - ../../../config/stage/dev/tls/acme.test+1-key.pem:/etc/x509/https/tls.key:z # Allow TLS connection to ourself, this is necessary for cross realm Identity Brokering - ../../../config/stage/dev/tls/acme.test+1.pem:/etc/x509/ca/tls.crt:z # Configure truststore for out-going https requests - ../../../config/stage/dev/tls/acme.test+1.p12:/opt/keycloak/conf/truststore.p12:z # make calls to external systems that use the tls certs - ${CA_ROOT_CERT:-}:/etc/x509/ca/tls-root.crt:z acme-account-console: image: httpd:2.4.51-bullseye volumes: - ../../../apps/acme-account-console:/usr/local/apache2/htdocs/acme-account:z - ../../../apps/acme-greetme:/usr/local/apache2/htdocs/acme-greetme:z - ../../../config/stage/dev/tls/acme.test+1.pem:/usr/local/apache2/conf/server.crt:z - ../../../config/stage/dev/tls/acme.test+1-key.pem:/usr/local/apache2/conf/server.key:z ports: - "4000:80" - "4443:443" command: - /bin/sh - -c - | echo 'ServerName apps.acme.test' >> conf/httpd.conf sed -i -e 's/^#\(Include .*httpd-ssl.conf\)/\1/' conf/httpd.conf sed -i -e 's/^#\(LoadModule .*mod_ssl.so\)/\1/' conf/httpd.conf sed -i -e 's/^#\(LoadModule .*mod_socache_shmcb.so\)/\1/' conf/httpd.conf exec httpd-foreground ================================================ FILE: deployments/local/dev/docker-compose-tracing-tls.yml ================================================ services: acme-otel-collector: volumes: - ../../../config/stage/dev/otel/otel-collector-config-tls.yaml:/etc/otel-collector-config.yaml:z - ${CA_ROOT_CERT:-}:/rootca.pem:z acme-jaeger: volumes: - ../../../config/stage/dev/tls/acme.test+1.pem:/cert.pem:z - ../../../config/stage/dev/tls/acme.test+1-key.pem:/key.pem:z - ${CA_ROOT_CERT:-}:/rootca.pem:z # command: # - "--query.http.tls.enabled" # - "--query.http.tls.key=/key.pem" # - "--query.http.tls.cert=/cert.pem" # - "--query.http.tls.min-version=1.2" # - "--query.http.tls.max-version=1.3" # - "--collector.grpc.tls.enabled" # - "--collector.grpc.tls.key=/key.pem" # - "--collector.grpc.tls.cert=/cert.pem" # - "--collector.grpc.tls.min-version=1.2" # - "--collector.grpc.tls.max-version=1.3" # Jaeger sends traces to itself. If we only allow TLS inbound, we need to do this via the hostname # and validate the certificate # - "--reporter.grpc.tls.enabled" # - "--reporter.grpc.tls.ca=/rootca.pem" # - "--reporter.grpc.host-port=ops.acme.test:14250" acme-keycloak: environment: OTEL_EXPORTER_OTLP_ENDPOINT: 'https://ops.acme.test:14317' ================================================ FILE: deployments/local/dev/docker-compose-tracing.yml ================================================ # https://quarkus.io/version/2.7/guides/opentelemetry#run-the-application services: acme-otel-collector: build: context: "../../../config/stage/dev/tls" dockerfile: "../../../../deployments/local/dev/otel-collector/Dockerfile" command: ["--config=/etc/otel-collector-config.yaml"] ports: - "13133:13133" # Health_check extension - "4317:4317" # OTLP gRPC receiver volumes: - ../../../config/stage/dev/otel/otel-collector-config.yaml:/etc/otel-collector-config.yaml:z extra_hosts: - "ops.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" acme-jaeger: image: jaegertracing/jaeger:2.5.0 ports: - "16686:16686" - "14317:14317" - "14318:14318" extra_hosts: - "ops.acme.test:${DOCKER_HOST_IP:-172.17.0.1}" acme-keycloak: env_file: - ./keycloak-tracing.env environment: # -Dcom.sun.management.jmxremote.authenticate=true -Dcom.sun.management.jmxremote.password.file=/opt/keycloak/conf/jmxremote.password # JAVA_TOOL_OPTIONS: '-javaagent:/opt/keycloak/opentelemetry-javaagent.jar -Dotel.javaagent.debug=false -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8790 -Dcom.sun.management.jmxremote.local.only=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -XX:FlightRecorderOptions=stackdepth=256' OTEL_EXPORTER_OTLP_ENDPOINT: 'http://ops.acme.test:4317' KC_LOG_CONSOLE_FORMAT: "%d{HH:mm:ss} %-5p ${TRACING_LOG_FORMAT} [%c{2.}] (%t) %s%e%n" ================================================ FILE: deployments/local/dev/docker-compose.yml ================================================ services: # Web Interface: http://localhost:1080/mail # Web API: https://github.com/maildev/maildev/blob/master/docs/rest.md mail: image: maildev/maildev:2.1.0 #@sha256:57e0b96fefb5dfeda8b39fb04c666ee7eef7be899ac8ea0e4d983bb0ea64aaff environment: MAILDEV_BASE_PATHNAME: "/mail" ports: - "1080:1080" - "1025:1025" acme-account-console: image: httpd:2.4.51-bullseye volumes: - ../../../apps/acme-account-console:/usr/local/apache2/htdocs/acme-account:z - ../../../apps/acme-greetme:/usr/local/apache2/htdocs/acme-greetme:z - ../../../apps/site:/usr/local/apache2/htdocs/site:z ports: - "4000:80" - "4443:443" redis: image: redis:6.2.6-alpine3.15 ports: - '6379:6379' # --requirepass redispass command: redis-server --save 20 1 --loglevel warning ================================================ FILE: deployments/local/dev/graylog/Dockerfile ================================================ FROM graylog/graylog:4.2.3-1-jre11@sha256:0f277b217c988cd4a0ce6f536271edde61e8b610ede0a96c9a214cbf0f86b4bf COPY --chown=1100:0 "./acme.test+1.pem" /usr/share/graylog/data/config/ssl/cert.crt COPY --chown=1100:0 "./acme.test+1-key.pem" /usr/share/graylog/data/config/ssl/key.key USER 0 RUN echo "Import Acme cert into truststore" && \ keytool \ -import \ -noprompt \ -keystore /usr/local/openjdk-11/lib/security/cacerts \ -storetype JKS \ -storepass changeit \ -alias acmecert \ -file /usr/share/graylog/data/config/ssl/cert.crt USER 1100 ================================================ FILE: deployments/local/dev/graylog/cli/0020-onstart-setup-graylog-logging.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin Graylog custom configuration... echo SETUP: Configure GELF logging to graylog if (outcome == failed) of /subsystem=logging/custom-handler=GELF/:read-resource /subsystem=logging/custom-handler=GELF/:add(module=biz.paluch.logging, class=biz.paluch.logging.gelf.wildfly.WildFlyGelfLogHandler, properties={ \ host=${env.LOG_SERVER_HOST:udp:acme-graylog}, \ port=${env.LOG_SERVER_PORT:12201}, \ version="1.1", \ extractStackTrace=true, \ filterStackTrace=true, \ mdcProfiling=false, \ timestampPattern="yyyy-MM-dd HH:mm:ss,SSSS", \ maximumMessageSize=8192, \ additionalFields="appGrp=iam,appSvc=iam-keycloak,appStage=${env.KEYCLOAK_DEPLOYMENT_STAGE:dev}", \ additionalFieldTypes="appGrp=String,appSvc=String,MessageParam0=String,MessageParam1=String,MessageParam2=String,MessageParam3=String,MessageParam4=String,MessageParam5=String,MessageParam6=String" \ }) echo end-if /subsystem=logging/custom-handler=GELF/:change-log-level(level=ALL) /subsystem=logging/root-logger=ROOT/:write-attribute(name=level,value=INFO) /subsystem=logging/root-logger=ROOT/:write-attribute(name=handlers,value=[CONSOLE,GELF]) ================================================ FILE: deployments/local/dev/graylog/contentpacks/iam-keycloak-content-pack-v1.json ================================================ { "v": 1, "id": "96eac170-1af1-460f-91ce-4c2051b80f21", "rev": 1, "name": "Keycloak IAM Content Pack", "summary": "Grok Patterns, Inputs, Streams and Dashboard for a Keycloak based IAM infrastructure", "description": "", "vendor": "Thomas Darimont", "url": "https://github.com/thomasdarimont", "parameters": [], "entities": [ { "v": "1", "type": { "name": "dashboard", "version": "2" }, "id": "1f594532-c090-4369-9116-73a464cd6cae", "data": { "summary": { "@type": "string", "@value": "IAM related metrics" }, "search": { "queries": [ { "id": "d74b7c22-f0a6-42e9-8b41-e42b76abf251", "timerange": { "type": "relative", "from": 300 }, "query": { "type": "elasticsearch", "query_string": "" }, "search_types": [ { "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN_ERROR" }, "name": "chart", "timerange": { "type": "relative", "from": 300 }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "series": [ { "type": "count", "id": "Message Count", "field": null } ], "filter": null, "rollup": true, "row_groups": [], "type": "pivot", "id": "c2b24848-6196-4a90-aa52-1da21708cffe", "column_groups": [], "sort": [] }, { "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGOUT" }, "name": "chart", "timerange": { "type": "relative", "from": 300 }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "series": [ { "type": "count", "id": "Message Count", "field": null } ], "filter": null, "rollup": true, "row_groups": [], "type": "pivot", "id": "50e9a922-177e-42d2-9a92-f34e1f7bec96", "column_groups": [], "sort": [] }, { "name": "events", "timerange": { "type": "relative", "from": 86400 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN_ERROR" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "id": "95657cd3-18ac-4057-8e20-d22a6704e4c4", "type": "events", "filter": null }, { "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN" }, "name": "chart", "timerange": { "type": "relative", "from": 86400 }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "series": [ { "type": "count", "id": "Logins", "field": "timestamp" } ], "filter": null, "rollup": true, "row_groups": [ { "type": "time", "field": "timestamp", "interval": { "type": "timeunit", "timeunit": "5m" } } ], "type": "pivot", "id": "b8b6da96-b411-409d-b0a7-e7c2eafe61b4", "column_groups": [], "sort": [] }, { "name": "events", "timerange": { "type": "relative", "from": 86400 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "id": "c94f6b5f-effc-4869-9aec-5d8b04611729", "type": "events", "filter": null }, { "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN_ERROR" }, "name": "chart", "timerange": { "type": "relative", "from": 86400 }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "series": [ { "type": "count", "id": "Login Errors", "field": "timestamp" } ], "filter": null, "rollup": true, "row_groups": [ { "type": "time", "field": "timestamp", "interval": { "type": "timeunit", "timeunit": "5m" } } ], "type": "pivot", "id": "491f93c5-6350-43b8-8c2e-3db68b21e367", "column_groups": [], "sort": [] }, { "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN" }, "name": "chart", "timerange": { "type": "relative", "from": 300 }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "series": [ { "type": "count", "id": "Message Count", "field": null } ], "filter": null, "rollup": true, "row_groups": [], "type": "pivot", "id": "ad9d4a73-133c-4cca-8d39-339eac286e06", "column_groups": [], "sort": [] } ] } ], "parameters": [], "requires": {}, "owner": "operator", "created_at": "2021-07-29T17:21:41.888Z" }, "created_at": "2021-07-29T17:01:17.010Z", "requires": {}, "state": { "d74b7c22-f0a6-42e9-8b41-e42b76abf251": { "selected_fields": null, "static_message_list_id": null, "titles": { "tab": { "title": "IAM Login" }, "widget": { "043a52c0-4562-4fa1-874e-b9603cb871b2": "Logins (5m)", "5023ca70-9092-4137-9e9f-9e2d114a01c5": "Login Errors (5m)", "4a374665-a9c8-4054-832e-a1226077a3bd": "Logouts (5m)", "319340a5-6ecf-416a-b949-2e7ba09c1e7b": "Logins (24h)", "8ac3ec2b-5dc5-44b2-9b54-0a963d26b736": "Login Errors (24h)" } }, "widgets": [ { "id": "8ac3ec2b-5dc5-44b2-9b54-0a963d26b736", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "from": 86400 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN_ERROR" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "config": { "visualization": "bar", "event_annotation": true, "row_pivots": [ { "field": "timestamp", "type": "time", "config": { "interval": { "type": "timeunit", "value": 5, "unit": "minutes" } } } ], "series": [ { "config": { "name": "Login Errors" }, "function": "count(timestamp)" } ], "rollup": true, "column_pivots": [], "visualization_config": { "barmode": "group" }, "formatting_settings": null, "sort": [] } }, { "id": "043a52c0-4562-4fa1-874e-b9603cb871b2", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "from": 300 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "config": { "visualization": "numeric", "event_annotation": false, "row_pivots": [], "series": [ { "config": { "name": "Message Count" }, "function": "count()" } ], "rollup": true, "column_pivots": [], "visualization_config": null, "formatting_settings": null, "sort": [] } }, { "id": "5023ca70-9092-4137-9e9f-9e2d114a01c5", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "from": 300 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN_ERROR" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "config": { "visualization": "numeric", "event_annotation": false, "row_pivots": [], "series": [ { "config": { "name": "Message Count" }, "function": "count()" } ], "rollup": true, "column_pivots": [], "visualization_config": null, "formatting_settings": null, "sort": [] } }, { "id": "4a374665-a9c8-4054-832e-a1226077a3bd", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "from": 300 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGOUT" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "config": { "visualization": "numeric", "event_annotation": false, "row_pivots": [], "series": [ { "config": { "name": "Message Count" }, "function": "count()" } ], "rollup": true, "column_pivots": [], "visualization_config": null, "formatting_settings": null, "sort": [] } }, { "id": "319340a5-6ecf-416a-b949-2e7ba09c1e7b", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "from": 86400 }, "query": { "type": "elasticsearch", "query_string": "kc_event_type:LOGIN" }, "streams": [ "2d44506b-3f84-4987-b446-134622e5710f" ], "config": { "visualization": "bar", "event_annotation": true, "row_pivots": [ { "field": "timestamp", "type": "time", "config": { "interval": { "type": "timeunit", "value": 5, "unit": "minutes" } } } ], "series": [ { "config": { "name": "Logins" }, "function": "count(timestamp)" } ], "rollup": true, "column_pivots": [], "visualization_config": { "barmode": "group" }, "formatting_settings": null, "sort": [] } } ], "widget_mapping": { "043a52c0-4562-4fa1-874e-b9603cb871b2": [ "ad9d4a73-133c-4cca-8d39-339eac286e06" ], "5023ca70-9092-4137-9e9f-9e2d114a01c5": [ "c2b24848-6196-4a90-aa52-1da21708cffe" ], "4a374665-a9c8-4054-832e-a1226077a3bd": [ "50e9a922-177e-42d2-9a92-f34e1f7bec96" ], "319340a5-6ecf-416a-b949-2e7ba09c1e7b": [ "b8b6da96-b411-409d-b0a7-e7c2eafe61b4", "c94f6b5f-effc-4869-9aec-5d8b04611729" ], "8ac3ec2b-5dc5-44b2-9b54-0a963d26b736": [ "95657cd3-18ac-4057-8e20-d22a6704e4c4", "491f93c5-6350-43b8-8c2e-3db68b21e367" ] }, "positions": { "043a52c0-4562-4fa1-874e-b9603cb871b2": { "col": 1, "row": 5, "height": 4, "width": 4 }, "5023ca70-9092-4137-9e9f-9e2d114a01c5": { "col": 9, "row": 5, "height": 4, "width": 4 }, "4a374665-a9c8-4054-832e-a1226077a3bd": { "col": 5, "row": 5, "height": 4, "width": 4 }, "319340a5-6ecf-416a-b949-2e7ba09c1e7b": { "col": 1, "row": 9, "height": 4, "width": 4 }, "8ac3ec2b-5dc5-44b2-9b54-0a963d26b736": { "col": 9, "row": 10, "height": 4, "width": 4 } }, "formatting": { "highlighting": [] }, "display_mode_settings": { "positions": {} } } }, "properties": [], "owner": "operator", "title": { "@type": "string", "@value": "IAM Dashboard" }, "type": "DASHBOARD", "description": { "@type": "string", "@value": "" } }, "constraints": [ { "type": "server-version", "version": ">=4.1.2+20cd592" } ] }, { "v": "1", "type": { "name": "input", "version": "1" }, "id": "ced821be-02fd-40fb-a026-55a2c15a3b48", "data": { "title": { "@type": "string", "@value": "IAM Input" }, "configuration": { "recv_buffer_size": { "@type": "integer", "@value": 262144 }, "port": { "@type": "integer", "@value": 12201 }, "number_worker_threads": { "@type": "integer", "@value": 12 }, "bind_address": { "@type": "string", "@value": "0.0.0.0" }, "decompress_size_limit": { "@type": "integer", "@value": 8388608 } }, "static_fields": {}, "type": { "@type": "string", "@value": "org.graylog2.inputs.gelf.udp.GELFUDPInput" }, "global": { "@type": "boolean", "@value": true }, "extractors": [ { "target_field": { "@type": "string", "@value": "kc_event_type" }, "condition_value": { "@type": "string", "@value": "" }, "order": { "@type": "integer", "@value": 0 }, "converters": [], "configuration": { "regex_value": { "@type": "string", "@value": "type=([^,]+)" } }, "source_field": { "@type": "string", "@value": "message" }, "title": { "@type": "string", "@value": "Keycloak user event type" }, "type": { "@type": "string", "@value": "REGEX" }, "cursor_strategy": { "@type": "string", "@value": "COPY" }, "condition_type": { "@type": "string", "@value": "NONE" } }, { "target_field": { "@type": "string", "@value": "kc_userId" }, "condition_value": { "@type": "string", "@value": "" }, "order": { "@type": "integer", "@value": 0 }, "converters": [], "configuration": { "regex_value": { "@type": "string", "@value": "userId=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})" } }, "source_field": { "@type": "string", "@value": "message" }, "title": { "@type": "string", "@value": "Keycloak userId" }, "type": { "@type": "string", "@value": "REGEX" }, "cursor_strategy": { "@type": "string", "@value": "COPY" }, "condition_type": { "@type": "string", "@value": "NONE" } }, { "target_field": { "@type": "string", "@value": "kc_ipAddress" }, "condition_value": { "@type": "string", "@value": "" }, "order": { "@type": "integer", "@value": 0 }, "converters": [], "configuration": { "regex_value": { "@type": "string", "@value": "ipAddress=((?=4.1.2+20cd592" } ] }, { "v": "1", "type": { "name": "stream", "version": "1" }, "id": "2d44506b-3f84-4987-b446-134622e5710f", "data": { "alarm_callbacks": [], "outputs": [], "remove_matches": { "@type": "boolean", "@value": false }, "title": { "@type": "string", "@value": "IAM Messages" }, "stream_rules": [ { "type": { "@type": "string", "@value": "EXACT" }, "field": { "@type": "string", "@value": "appGrp" }, "value": { "@type": "string", "@value": "iam" }, "inverted": { "@type": "boolean", "@value": false }, "description": { "@type": "string", "@value": "" } } ], "alert_conditions": [], "matching_type": { "@type": "string", "@value": "AND" }, "disabled": { "@type": "boolean", "@value": false }, "description": { "@type": "string", "@value": "Stream containing all messages from IAM System Components" }, "default_stream": { "@type": "boolean", "@value": false } }, "constraints": [ { "type": "server-version", "version": ">=4.1.2+20cd592" } ] }, { "v": "1", "type": { "name": "dashboard", "version": "2" }, "id": "4a13228d-8d49-4b98-9a8c-be88ea5ab6ea", "data": { "summary": { "@type": "string", "@value": "This is a list of all sources that sent in messages to Graylog." }, "search": { "queries": [ { "id": "a1647eb6-a064-4fe6-b459-1e4267d3f659", "timerange": { "type": "relative", "range": 300 }, "query": { "type": "elasticsearch", "query_string": "" }, "search_types": [ { "query": null, "name": "chart", "timerange": { "type": "relative", "range": 300 }, "streams": [], "series": [ { "type": "count", "id": "Message count", "field": null } ], "filter": null, "rollup": true, "row_groups": [ { "type": "values", "field": "source", "limit": 10 } ], "type": "pivot", "id": "a964f1c5-e108-4b5e-a907-ffe0b0f0683c", "column_groups": [], "sort": [ { "type": "series", "field": "count()", "direction": "Descending" } ] }, { "query": null, "name": "chart", "timerange": { "type": "relative", "range": 300 }, "streams": [], "series": [ { "type": "count", "id": "Message count", "field": null } ], "filter": null, "rollup": true, "row_groups": [ { "type": "values", "field": "source", "limit": 15 } ], "type": "pivot", "id": "011b2894-49e5-44d8-aab6-8c4d4457a886", "column_groups": [], "sort": [ { "type": "series", "field": "count()", "direction": "Descending" } ] }, { "query": null, "name": "chart", "timerange": { "type": "relative", "range": 300 }, "streams": [], "series": [ { "type": "count", "id": "Message count", "field": null } ], "filter": null, "rollup": true, "row_groups": [ { "type": "time", "field": "timestamp", "interval": { "type": "auto", "scaling": 1 } } ], "type": "pivot", "id": "481de18f-938e-40d5-8ab2-6eaf6a28f091", "column_groups": [], "sort": [] } ] } ], "parameters": [], "requires": {}, "owner": "admin", "created_at": "2019-11-22T10:58:47.255Z" }, "created_at": "2019-11-22T10:54:50.950Z", "requires": {}, "state": { "a1647eb6-a064-4fe6-b459-1e4267d3f659": { "selected_fields": null, "static_message_list_id": null, "titles": { "tab": { "title": "Sources Overview" }, "widget": { "6c127c5d-be75-4157-b43f-ac0194ac0586": "Selected sources", "92d63811-e4dd-47db-bd3b-db03c8a9bd53": "Messages per Source", "00637e63-d728-4b3e-932b-7c8696b4855d": "Messages over time" } }, "widgets": [ { "id": "6c127c5d-be75-4157-b43f-ac0194ac0586", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "range": 300 }, "query": null, "streams": [], "config": { "visualization": "table", "event_annotation": false, "row_pivots": [ { "field": "source", "type": "values", "config": { "limit": 15 } } ], "series": [ { "config": { "name": "Message count" }, "function": "count()" } ], "rollup": true, "column_pivots": [], "visualization_config": null, "formatting_settings": null, "sort": [ { "type": "series", "field": "count()", "direction": "Descending" } ] } }, { "id": "00637e63-d728-4b3e-932b-7c8696b4855d", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "range": 300 }, "query": null, "streams": [], "config": { "visualization": "line", "event_annotation": false, "row_pivots": [ { "field": "timestamp", "type": "time", "config": { "interval": { "type": "auto", "scaling": 1 } } } ], "series": [ { "config": { "name": "Message count" }, "function": "count()" } ], "rollup": true, "column_pivots": [], "visualization_config": null, "formatting_settings": null, "sort": [] } }, { "id": "92d63811-e4dd-47db-bd3b-db03c8a9bd53", "type": "aggregation", "filter": null, "timerange": { "type": "relative", "range": 300 }, "query": null, "streams": [], "config": { "visualization": "pie", "event_annotation": false, "row_pivots": [ { "field": "source", "type": "values", "config": { "limit": 10 } } ], "series": [ { "config": { "name": "Message count" }, "function": "count()" } ], "rollup": true, "column_pivots": [], "visualization_config": null, "formatting_settings": null, "sort": [ { "type": "series", "field": "count()", "direction": "Descending" } ] } } ], "widget_mapping": { "6c127c5d-be75-4157-b43f-ac0194ac0586": [ "011b2894-49e5-44d8-aab6-8c4d4457a886" ], "92d63811-e4dd-47db-bd3b-db03c8a9bd53": [ "a964f1c5-e108-4b5e-a907-ffe0b0f0683c" ], "00637e63-d728-4b3e-932b-7c8696b4855d": [ "481de18f-938e-40d5-8ab2-6eaf6a28f091" ] }, "positions": { "6c127c5d-be75-4157-b43f-ac0194ac0586": { "col": 1, "row": 5, "height": 4, "width": 6 }, "92d63811-e4dd-47db-bd3b-db03c8a9bd53": { "col": 7, "row": 5, "height": 4, "width": 6 }, "00637e63-d728-4b3e-932b-7c8696b4855d": { "col": 1, "row": 1, "height": 4, "width": "Infinity" } }, "formatting": { "highlighting": [] }, "display_mode_settings": { "positions": {} } } }, "properties": [], "owner": "admin", "title": { "@type": "string", "@value": "Sources" }, "type": "DASHBOARD", "description": { "@type": "string", "@value": "This is a list of all sources that sent in messages to Graylog. You can narrow the timerange by zooming in on the message histogram, or you can increase the time range by specifying a broader one in the controls at the top. You can also specify filters to limit the results you are seeing. You can also add additional widgets to this dashboard, or adapt the appearance of existing widgets to suit your needs." } }, "constraints": [ { "type": "server-version", "version": ">=4.1.2+20cd592" } ] } ] } ================================================ FILE: deployments/local/dev/graylog/modules/logstash-gelf-1.14.1/biz/paluch/logging/main/module.xml ================================================ ================================================ FILE: deployments/local/dev/keycloak/Dockerfile ================================================ # Stay on 17.0.1 due to regression in Keycloak 18.0.0: admin-console -> index.html /auth[/]js/keycloak.js ARG KEYCLOAK_VERSION=17.0.1 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy USER root # Add java-11-openjdk-devel JDK for debugging RUN microdnf update -y && microdnf install -y java-11-openjdk-devel && microdnf clean all USER 1000 # Add local JMX user for testing RUN /opt/jboss/keycloak/bin/add-user.sh jmxuser password # Note that we need to register the smallrye components via 0010-register-smallrye-extensions.cli ================================================ FILE: deployments/local/dev/keycloak-common.env ================================================ # Keycloak KEYCLOAK_USER=admin KEYCLOAK_PASSWORD=admin # Keycloak Quarkus KC_BOOTSTRAP_ADMIN_USERNAME=admin KC_BOOTSTRAP_ADMIN_PASSWORD=admin ================================================ FILE: deployments/local/dev/keycloak-db.env ================================================ DB_ADDR=acme-keycloak-db DB_DATABASE=keycloak DB_USER=keycloak DB_PASSWORD=keycloak DB_SCHEMA=public # Keycloak.X KC_DB_URL_HOST=acme-keycloak-db KC_DB_URL_DATABASE=keycloak KC_DB_USERNAME=keycloak KC_DB_PASSWORD=keycloak KC_DB_SCHEMA=public ================================================ FILE: deployments/local/dev/keycloak-ext/readme.md ================================================ External Keycloak Extensions --- # keycloak-metrics-spi Custom version of the keycloak-metrics-spi plugin that is compatible with Keycloak and Keycloak.X. The code can be found here: https://github.com/thomasdarimont/keycloak-metrics-spi/tree/poc/keycloak-x-support The upstream PR can be found here: https://github.com/aerogear/keycloak-metrics-spi/pull/120 # keycloak-home-idp-discovery The [keycloak-home-idp-discovery](https://github.com/sventorben/keycloak-home-idp-discovery) provides a simple Keycloak authenticator to redirect users to their home identity provider during login. ================================================ FILE: deployments/local/dev/keycloak-http.env ================================================ # Configure an explicit Keycloak frontend URL KEYCLOAK_FRONTEND_URL=http://id.acme.test:8080/auth KEYCLOAK_ADMIN_URL=http://admin.acme.test:8080/auth APPS_FRONTEND_URL_MINISPA=http://apps.acme.test:4000/acme-account APPS_FRONTEND_URL_GREETME=http://apps.acme.test:4000/acme-greetme ================================================ FILE: deployments/local/dev/keycloak-openldap.env ================================================ LDAP_URL=ldap://openldap:389 ACME_LDAP_GROUP_DN=dc=corp,dc=acme,dc=local ACME_LDAP_USERS_DN=dc=corp,dc=acme,dc=local LDAP_USER=cn=keycloak,dc=corp,dc=acme,dc=local LDAP_PASSWORD=keycloak ================================================ FILE: deployments/local/dev/keycloak-provisioning.env ================================================ # Provided by keycloak-common.env #KEYCLOAK_USER= #KEYCLOAK_PASSWORD= # URL for Keycloak container within docker-compose KEYCLOAK_URL=http://acme-keycloak:8080/auth # Properties for Provisioning Config # Provided by keycloak-openldap.env #LDAP_URL= #ACME_LDAP_GROUP_DN= #ACME_LDAP_USERS_DN= #LDAP_USER= #LDAP_PASSWORD= ACME_APPS_INTERNAL_IDP_BROKER_SECRET=secret # Variables for Keycloak Config CLI Provisioning ACME_AZURE_AAD_TENANT_URL=https://login.microsoftonline.com/dummy-azuread-tenant-id ================================================ FILE: deployments/local/dev/keycloak-tls.env ================================================ # Configure an explicit Keycloak frontend URL KEYCLOAK_FRONTEND_URL=https://id.acme.test:8443/auth KEYCLOAK_ADMIN_URL=https://admin.acme.test:8443/auth APPS_FRONTEND_URL_MINISPA=https://apps.acme.test:4443/acme-account APPS_FRONTEND_URL_GREETME=https://apps.acme.test:4443/acme-greetme # Triggers Truststore generation and dynamic TlS certificate import X509_CA_BUNDLE=/etc/x509/ca/*.crt # Needed for Keycloak.X https KC_HTTPS_CERTIFICATE_FILE=/etc/x509/https/tls.crt KC_HTTPS_CERTIFICATE_KEY_FILE=/etc/x509/https/tls.key # used as frontend URL #KC_SPI_HOSTNAME_DEFAULT_HOSTNAME=id.acme.test:8443 # used as admin URL for admin-console #KC_SPI_HOSTNAME_DEFAULT_ADMIN=id.acme.test # used as frontend URL hostname # KC_HOSTNAME=id.acme.test:8443 KC_HOSTNAME=https://id.acme.test:8443/auth # used as admin URL hostname for admin-console #KC_HOSTNAME_ADMIN=admin.acme.test ================================================ FILE: deployments/local/dev/keycloak-tracing.env ================================================ # see: https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/logger-mdc-instrumentation.md TRACING_LOG_FORMAT=trace_id=%X{trace_id}, parent_id=%X{parent_id}, span_id=%X{span_id}, sampled=%X{sampled} OTEL_JAVAAGENT_EXCLUDE_CLASSES=io.micrometer.* OTEL_SERVICE_NAME=acme-keycloak #OTEL_PROPAGATORS=b3multi OTEL_PROPAGATORS=tracecontext,baggage,jaeger OTEL_METRICS_EXPORTER=none OTEL_TRACES_EXPORTER=otlp #OTEL_EXPORTER_OTLP_ENDPOINT=http://ops.acme.test:4317 # see: https://www.keycloak.org/observability/tracing KC_TRACING_ENABLED=true ================================================ FILE: deployments/local/dev/keycloakx/Dockerfile ================================================ ARG KEYCLOAK_VERSION=26.5.7 ## Example for adding custom packages (curl) to Keycloak image ## See: https://www.keycloak.org/server/containers#_installing_additional_rpm_packages FROM registry.access.redhat.com/ubi9 AS ubi-micro-build RUN mkdir -p /mnt/rootfs RUN dnf install --installroot /mnt/rootfs curl --releasever 9 --setopt install_weak_deps=false --nodocs -y && \ dnf --installroot /mnt/rootfs clean all && \ rpm --root /mnt/rootfs -e --nodeps setup #see https://www.keycloak.org/server/containers FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION COPY --from=ubi-micro-build /mnt/rootfs / #FROM thomasdarimont/keycloak:21.0.999.1 USER root #RUN echo "Add nashorn javascript engine" #RUN --mount=from=busybox:1.36.0,src=/bin/,dst=/bin/ \ # wget -O /opt/keycloak/providers/nashorn-core-15.3.jar https://search.maven.org/remotecontent?filepath=org/openjdk/nashorn/nashorn-core/15.3/nashorn-core-15.3.jar ## Workaround for adding the current certifcate to the cacerts truststore # Import certificate into cacerts truststore RUN echo 1659621300842 COPY --chown=keycloak:keycloak "./acme.test+1.pem" "/etc/x509/tls.crt.pem" RUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit || echo "Failed to import cert" #RUN export AEROGEAR_VERSION=2.5.1 && \ # curl https://github.com/aerogear/keycloak-metrics-spi/releases/download/$AEROGEAR_VERSION/keycloak-metrics-spi-$AEROGEAR_VERSION.jar \ # --location \ # --output /opt/jboss/keycloak/providers/keycloak-metrics-spi-$AEROGEAR_VERSION.jar # sRUN echo "Downloading OpenTelemetry Javaagent and support libs" #ENV OTEL_AGENT_VERSION=1.33.6 #ENV OTEL_TRACE_VERSION=1.42.1 #ADD --chown=keycloak:keycloak https://search.maven.org/remotecontent?filepath=io/opentelemetry/javaagent/opentelemetry-javaagent/$OTEL_AGENT_VERSION/opentelemetry-javaagent-$OTEL_AGENT_VERSION.jar /opt/keycloak/opentelemetry-javaagent.jar #ADD --chown=keycloak:keycloak https://repo1.maven.org/maven2/io/opentelemetry/opentelemetry-extension-trace-propagators/$OTEL_TRACE_VERSION/opentelemetry-extension-trace-propagators-$OTEL_TRACE_VERSION.jar /opt/keycloak/providers/opentelemetry-extension-trace-propagators.jar USER keycloak ================================================ FILE: deployments/local/dev/keycloakx/Dockerfile-ci ================================================ #see https://www.keycloak.org/server/containers ARG KEYCLOAK_VERSION=26.5.7 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION USER root ## Workaround for adding the current certifcate to the cacerts truststore # Import certificate into cacerts truststore RUN echo 1659621300842 COPY --chown=keycloak:keycloak "./acme.test+1.pem" "/etc/x509/tls.crt.pem" RUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit || echo "Failed to import cert" USER keycloak ================================================ FILE: deployments/local/dev/keycloakx/health_check.sh ================================================ #!/bin/bash # Send the HTTP request and store the response RESPONSE=$(curl -k -v "https://localhost:9000/auth/health") # Check if the response contains "HTTP/1.1 200 OK" if echo "$RESPONSE" | grep -q "\"status\": \"UP\""; then exit 0 else exit 1 fi ================================================ FILE: deployments/local/dev/mysql/Dockerfile ================================================ FROM mysql/mysql-server:8.0.28 # Copy certificates into image to adjust permissions as necessary # 27 uid of mysql user COPY --chown=27:0 "./acme.test+1.pem" /etc/certs/ca.crt COPY --chown=27:0 "./acme.test+1.pem" /etc/certs/server.crt COPY --chown=27:0 "./acme.test+1-key.pem" /etc/certs/server.key ================================================ FILE: deployments/local/dev/nats/readme.md ================================================ NATS Support --- ``` nats context add localhost --description "Localhost" ``` Add username / password in context config ``` vi ~/.config/nats/context/localhost.json ``` List contexts ``` nats context ls ``` Select context ``` nats ctx select localhost ``` Nats subscribe to subject with prefix ``` nats sub "acme.iam.keycloak.>" ``` --- # Misc ## Create stream ``` nats stream add KEYCLOAK --subjects "acme.iam.keycloak.*" --ack --max-msgs=-1 --max-bytes=-1 --max-age=1y --storage file --retention limits --max-msg-size=-1 --discard=old nats stream info KEYCLOAK ``` ## Add consumers ``` nats consumer add KEYCLOAK USER --filter "acme.iam.keycloak.user" --ack explicit --pull --deliver all --max-deliver=-1 --sample 100 nats consumer add KEYCLOAK ADMIN --filter "acme.iam.keycloak.admin" --ack explicit --pull --deliver all --max-deliver=-1 --sample 100 nats consumer add KEYCLOAK MONITOR --filter '' --ack none --target monitor.KEYCLOAK --deliver last --replay instant ``` ## Stream Status https://docs.nats.io/running-a-nats-service/configuration/clustering/jetstream_clustering/administration ``` nats stream report nats server report jetstream --user "admin" --password "password" ``` ## Read consumer ``` nats consumer next KEYCLOAK USER --count 1000 ``` ## Subscribe to subject ``` nats sub "acme.iam.keycloak.user" --translate 'jq .' --count 10 ``` --- # Misc nats stream add IOT --subjects "iot.*" --ack --max-msgs=-1 --max-bytes=-1 --max-age=1y --storage file --retention limits --max-msg-size=-1 --discard=old nats consumer add IOT CMD --filter "iot.cmd" --ack explicit --pull --deliver last --max-deliver=-1 --sample 100 nats consumer next IOT CMD --count 3 nats pub iot.cmd --count=6 --sleep 1s "iot cmd #{{Count}} @ {{TimeStamp}}" nats sub iot.cmd --last nats sub iot.cmd --new nats sub "iot.*" --last-per-subject ================================================ FILE: deployments/local/dev/nats/server.conf ================================================ accounts: { $SYS: { users: [ { user: "admin", password: "password" } ] }, KEYCLOAK: { jetstream: true, users: [ { user: "keycloak", password: "keycloak" } ] } } jetstream: { } #cluster: { # name: LOCAL, # port: 6222, # routes: [ # "nats://acme_nats_1:6222" # ] #} ================================================ FILE: deployments/local/dev/oracle/Dockerfile ================================================ FROM gvenzl/oracle-free:latest # customizations here... ================================================ FILE: deployments/local/dev/otel-collector/Dockerfile ================================================ ARG OTEL_VERSION=0.123.0 FROM otel/opentelemetry-collector:$OTEL_VERSION USER 0 COPY --chown=10001:0 "./acme.test+1-key.pem" /key.pem COPY --chown=10001:0 "./acme.test+1.pem" /cert.pem USER 10001 ================================================ FILE: deployments/local/dev/postgresql/Dockerfile ================================================ FROM postgres:16.6 # 999 uid of postgresql COPY --chown=999:0 "./acme.test+1.pem" /var/lib/postgresql/server.crt COPY --chown=999:0 "./acme.test+1-key.pem" /var/lib/postgresql/server.key ================================================ FILE: deployments/local/dev/simplesaml/idp/Dockerfile ================================================ FROM kenchan0130/simplesamlphp:1.19.9 ================================================ FILE: deployments/local/dev/simplesaml/idp/authsources.php ================================================ 'ab4f07dc-b661-48a3-a173-d0103d6981b2', 'http://schemas.microsoft.com/identity/claims/objectidentifier' => '', 'http://schemas.microsoft.com/identity/claims/displayname' => '', 'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups' => array(), 'http://schemas.microsoft.com/identity/claims/identityprovider' => 'https://sts.windows.net/da2a1472-abd3-47c9-95a4-4a0068312122/', 'http://schemas.microsoft.com/claims/authnmethodsreferences' => array('http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/password', 'http://schemas.microsoft.com/claims/multipleauthn'), 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => '', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => '', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => '', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => '' ); $config = array( 'admin' => array( 'core:AdminPassword', ), 'example-userpass' => array( 'exampleauth:UserPass', 'user1:password' => array_merge($test_user_base, array( 'http://schemas.microsoft.com/identity/claims/objectidentifier' => 'f2d75402-e1ae-40fe-8cc9-98ca1ab9cd5e', 'http://schemas.microsoft.com/identity/claims/displayname' => 'User1 Taro', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => 'user1@example.com', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => 'Taro', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => 'User1', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => 'user1@example.com' )), 'user2:password' => array_merge($test_user_base, array( 'http://schemas.microsoft.com/identity/claims/objectidentifier' => 'f2a94916-2fcb-4b68-9eb1-5436309006a3', 'http://schemas.microsoft.com/identity/claims/displayname' => 'User2 Taro', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' => 'user2@example.com', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname' => 'Taro', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname' => 'User2', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => 'user2@example.com' )), ), ); ================================================ FILE: deployments/local/dev/sqlserver/Dockerfile ================================================ ARG SQLSERVER_VERSION=2019-CU14-ubuntu-20.04 FROM mcr.microsoft.com/mssql/server:$SQLSERVER_VERSION # Copy certificates into image to adjust permissions as necessary # 10001 uid of sqlserver user COPY --chown=10001:0 "./acme.test+1.pem" /var/opt/mssql/certs/mssql.pem COPY --chown=10001:0 "./acme.test+1-key.pem" /var/opt/mssql/private/mssql.key ================================================ FILE: deployments/local/dev/sqlserver/db-init.sh ================================================ #!/usr/bin/env bash #wait for the SQL Server to come up echo "MSSQL: Waiting for MSSQL server to come up..." while ! timeout 1 bash -c "echo > /dev/tcp/localhost/1433"; do sleep 1 done sleep 3 echo "MSSQL: Create initial Keycloak database" #run the setup script to create the DB and the schema in the DB /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -C -i /opt/mssql-tools/bin/db-init.sql #/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -Q "CREATE DATABASE keycloak" -C ; #/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -Q "CREATE USER keycloak WITH PASSWORD='Keycloak123'" -C ; ================================================ FILE: deployments/local/dev/sqlserver/db-init.sql ================================================ USE [master] GO IF DB_ID('keycloak') IS NOT NULL set noexec on -- prevent creation when already exists CREATE DATABASE [keycloak]; GO USE [keycloak] GO CREATE LOGIN keycloak WITH PASSWORD='Keycloak123'; GO CREATE USER keycloak FOR LOGIN keycloak; GO GRANT ALL ON keycloak TO [keycloak]; GO EXEC sp_addrolemember 'db_owner', N'keycloak' GO ================================================ FILE: deployments/local/dev/sqlserver/docker-entrypoint.sh ================================================ #!/usr/bin/env bash #start SQL Server, start the script to create/setup the DB # see https://github.com/microsoft/mssql-docker/issues/2#issuecomment-547699532 bash /opt/mssql/bin/db-init.sh & /opt/mssql/bin/sqlservr ================================================ FILE: deployments/local/dev/sqlserver/mssql.conf ================================================ [network] tlscert = /var/opt/mssql/certs/mssql.pem tlskey = /var/opt/mssql/private/mssql.key tlsprotocols = 1.2 forceencryption = 1 ================================================ FILE: deployments/local/standalone/docker-compose.yml ================================================ services: database: image: docker.io/postgres:15 environment: POSTGRES_USER: keycloak POSTGRES_PASSWORD: passw0rd ports: - 25432:5432 volumes: - keycloak-db-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U keycloak"] interval: 10s timeout: 5s retries: 5 keycloak: build: dockerfile: ./keycloak/Dockerfile command: start-dev env_file: .env environment: DEBUG: 'true' DEBUG_PORT: '*:8787' # Keycloak DB KC_DB: postgres KC_DB_URL_HOST: database KC_DB_URL_PORT: '5432' KC_DB_URL_DATABASE: keycloak KC_DB_USERNAME: keycloak KC_DB_PASSWORD: passw0rd KC_LOG_LEVEL: INFO,com.acme.iam.keycloak:debug KC_FEATURES: preview KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin KC_HTTPS_CERTIFICATE_FILE: /opt/keycloak/conf/certs/cert.pem KC_HTTPS_CERTIFICATE_KEY_FILE: /opt/keycloak/conf/certs/cert-key.pem KC_HOSTNAME: id.acme.test KC_PROXY: edge ports: - "8080:8080" - "8443:8443" - "8787:8787" volumes: - ./keycloak/providers:/opt/keycloak/providers - ./keycloak/themes:/opt/keycloak/themes - ./keycloak/conf/keycloak.conf:/opt/keycloak/conf/keycloak.conf - ./keycloak/conf/quarkus.properties:/opt/keycloak/conf/quarkus.properties - ./config/certs/keycloak-cert.pem:/opt/keycloak/conf/certs/cert.pem - ./config/certs/keycloak-cert-key.pem:/opt/keycloak/conf/certs/cert-key.pem proxy: image: nginx:alpine volumes: - ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf - ./config/certs/acme.test-cert.pem:/etc/tls/cert.pem - ./config/certs/acme.test-cert-key.pem:/etc/tls/cert-key.pem - ./config/certs/rootCA.pem:/etc/tls/rootCA.pem ports: - "443:443" depends_on: - keycloak mailserver: # Web Interface: http://localhost:1080/mail # Web API: https://github.com/maildev/maildev/blob/master/docs/rest.md image: maildev/maildev:2.1.0@sha256:57e0b96fefb5dfeda8b39fb04c666ee7eef7be899ac8ea0e4d983bb0ea64aaff environment: MAILDEV_BASE_PATHNAME: "/mail" ports: - "1080:1080" #web ui - "1025:1025" #smtp networks: - backend volumes: keycloak-db-data: name: keycloak-db-data ================================================ FILE: deployments/local/standalone/keycloak/Dockerfile ================================================ #see https://www.keycloak.org/server/containers ARG KEYCLOAK_VERSION=25.0.6 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION USER root USER keycloak ================================================ FILE: deployments/local/standalone/keycloak/conf/keycloak.conf ================================================ spi-events-listener-jboss-logging-success-level=info spi-events-listener-jboss-logging-error-level=warn ================================================ FILE: deployments/local/standalone/keycloak/conf/quarkus.properties ================================================ ================================================ FILE: deployments/local/standalone/proxy/nginx.conf ================================================ error_log stdout info; access_log stdout; # Disable server name header server_tokens off; server { listen 443 ssl; server_name id.acme.test; # generated via https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=intermediate&openssl=1.1.1d&ocsp=false&guideline=5.6 ssl_certificate /etc/tls/cert.pem; ssl_certificate_key /etc/tls/cert-key.pem; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; # curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam # ssl_dhparam /etc/ssl/dhparams; # intermediate configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; # HSTS (ngx_http_headers_module is required) (63072000 seconds) # add_header Strict-Transport-Security "max-age=63072000" always; # OCSP stapling # ssl_stapling on; # ssl_stapling_verify on; # replace with the IP address of your resolver # resolver 127.0.0.1; location / { location = /robots.txt { allow all; log_not_found off; access_log off; } if ( $request_uri ~* ^.+\. ) { access_log off; } # if ( $request_uri ~ ^/(admin) ) { # return 403; # } proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; proxy_pass https://keycloak; proxy_connect_timeout 2s; proxy_ssl_trusted_certificate /etc/tls/rootCA.pem; proxy_ssl_verify on; proxy_ssl_session_reuse on; proxy_ssl_protocols TLSv1.2 TLSv1.3; proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; } } upstream keycloak { ip_hash; server keycloak:8443 fail_timeout=2s; } ================================================ FILE: deployments/local/standalone/readme.md ================================================ Keycloak Standalone Example --- # Make certificates mkcert -install Generate "external cert" mkcert -cert-file ./config/certs/acme.test-cert.pem -key-file ./config/certs/acme.test-cert-key.pem "*.acme.test" Generate internal cert mkcert -cert-file ./config/certs/keycloak-cert.pem -key-file ./config/certs/keycloak-cert-key.pem "keycloak" ================================================ FILE: deployments/local/standalone/up.sh ================================================ #!/usr/bin/env bash docker compose -p kc-simple -f docker-compose.yml up $@ ================================================ FILE: keycloak/cli/0001-onstart-init.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Begin Keycloak custom configuration... ### Logging ### echo SETUP: Disable file logging /subsystem=logging/root-logger=ROOT:remove-handler(name=FILE) echo SETUP: Configure log levels /subsystem=logging/console-handler=CONSOLE:write-attribute(name=level,value=ALL) /subsystem=logging/root-logger=ROOT/:write-attribute(name=level,value=${env.KEYCLOAK_LOGLEVEL_ROOT:INFO}) /subsystem=logging/logger=org.keycloak:write-attribute(name=level,value=${env.KEYCLOAK_LOGLEVEL_KEYCLOAK:INFO}) /subsystem=logging/logger=com.github.thomasdarimont.keycloak:add(level=${env.KEYCLOAK_LOGLEVEL_ACME:INFO}) echo SETUP: Configure HTTP log levels # You need to set the JVM System property to enable the request logging # -Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog /subsystem=logging/logger=org.apache.http:add(level=${env.KEYCLOAK_LOGLEVEL_HTTP_CLIENT:DEBUG}) /subsystem=logging/logger=org.apache.http.wire:add(level=${env.KEYCLOAK_LOGLEVEL_HTTP_CLIENT_WIRE:DEBUG}) ### Event Listeners SPI Configuration ### echo SETUP: Event Listeners configuration # Add dedicated eventsListener config element to allow configuring elements. if (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/:read-resource echo SETUP: Add missing eventsListener SPI /subsystem=keycloak-server/spi=eventsListener:add() echo end-if echo SETUP: Configure built-in "jboss-logging" event listener if (outcome == failed) of /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging/:read-resource echo SETUP: Add missing "jboss-logging" event listener /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:add(enabled=true) echo end-if # Propagate success events to INFO instead of DEBUG # This allows to track successful logins in log analysis /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.success-level,value=info) /subsystem=keycloak-server/spi=eventsListener/provider=jboss-logging:write-attribute(name=properties.error-level,value=warn) echo SETUP: Configure built-in "email" event listener to only send emails for user initiated UPDATE_PASSWORD events /subsystem=keycloak-server/spi=eventsListener/provider=email:add(enabled=true) /subsystem=keycloak-server/spi=eventsListener/provider=email:write-attribute(name=properties.exclude-events,value="[\"LOGIN_ERROR\",\"LOGIN\",\"UPDATE_TOTP\",\"REMOVE_TOTP\"]") /subsystem=keycloak-server/spi=eventsListener/provider=email:write-attribute(name=properties.include-events,value="[\"UPDATE_PASSWORD\"]") ### Theme Configuration ### echo SETUP: Theme configuration /subsystem=keycloak-server/theme=defaults:write-attribute(name=cacheThemes,value=${env.KEYCLOAK_THEME_CACHING:true}) /subsystem=keycloak-server/theme=defaults:write-attribute(name=cacheTemplates,value=${env.KEYCLOAK_THEME_TEMPLATE_CACHING:true}) /subsystem=keycloak-server/theme=defaults:write-attribute(name=welcomeTheme,value=${env.KEYCLOAK_WELCOME_THEME:keycloak}) /subsystem=keycloak-server/theme=defaults:write-attribute(name=default,value=${env.KEYCLOAK_DEFAULT_THEME:keycloak}) ### Hostname SPI Configuration ### echo SETUP: Hostname configuration # Configure Keycloak to use the frontend-URL as the base URL for backend endpoints /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.forceBackendUrlToFrontendUrl, value=${env.KEYCLOAK_FORCE_FRONTEND_TO_BACKEND_URL:true}) /subsystem=keycloak-server/spi=hostname/provider=default:write-attribute(name=properties.adminUrl, value=${env.KEYCLOAK_ADMIN_URL:}) ### Datasource Configuration ### # echo SETUP: Database configuration # /subsystem=datasources/data-source=KeycloakDS:write-attribute(name=min-pool-size,value=30) # /subsystem=datasources/data-source=KeycloakDS:write-attribute(name=max-pool-size,value=30) ### Offline Session Handling echo SETUP: Configure Lazy-Loading for Offline-Sessions echo SETUP: Offline-Sessions: Customize Keycloak UserSessions SPI configuration # Add dedicated userSession config element to allow configuring elements. if (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/:read-resource echo SETUP: Add missing userSessions SPI /subsystem=keycloak-server/spi=userSessions:add() echo end-if echo SETUP: Infinispan: Configure built-in "infinispan" echo SETUP: Offline-Sessions: Configure built-in "infinispan" UserSessions loader if (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/provider=infinispan/:read-resource echo SETUP: Add missing "infinispan" provider with disabled offlineSession preloading /subsystem=keycloak-server/spi=userSessions/provider=infinispan:add(enabled=true) /subsystem=keycloak-server/spi=userSessions/provider=infinispan:write-attribute(name=properties.preloadOfflineSessionsFromDatabase,value=${env.KEYCLOAK_INFINISPAN_SESSIONS_PRELOAD_DATABASE:false}) /subsystem=keycloak-server/spi=userSessions/provider=infinispan:write-attribute(name=properties.sessionsPerSegment,value=${env.KEYCLOAK_INFINISPAN_USER_SESSIONS_SESSIONS_PER_SEGMENT:512}) /subsystem=keycloak-server/spi=userSessions:write-attribute(name=default-provider,value=infinispan) echo end-if echo SETUP: Infinispan: Configure "authenticationSessions" SPI if (outcome == failed) of /subsystem=keycloak-server/spi=authenticationSessions/:read-resource /subsystem=keycloak-server/spi=authenticationSessions:add() echo SETUP: Infinispan: Configure "authenticationSessions" provider to mitigate CVE-2021-3637 # authSessionsLimit since Keycloak 14.0.0 # see https://bugzilla.redhat.com/show_bug.cgi?id=1979638 # see https://issues.redhat.com/browse/KEYCLOAK-16616 /subsystem=keycloak-server/spi=authenticationSessions/provider=infinispan:add(properties={authSessionsLimit => ${env.KEYCLOAK_AUTH_SESSIONS_LIMIT:10}},enabled=true) end-if ### Transactions echo SETUP: Transactions: Increasing default transaction timeout to 15 minutes /subsystem=transactions/:write-attribute(name=default-timeout,value=${env.KEYCLOAK_TRANSACTION_TIMEOUT:900}) ### MISC echo SETUP: Avoid ... WARN... # Avoid ... WARN [org.jboss.as.ejb3.remote] (ClusterTopologyRegistrar - 1) WFLYEJB0509: Clustered EJBs in Node: keycloak-0 are bound to INADDR_ANY(0.0.0.0). # Client cannot reach back the cluster when they are not in the same local network. # See https://developer.jboss.org/thread/276859 /socket-binding-group=standard-sockets/socket-binding=http:list-add(name=client-mappings,value={destination-address=${jboss.host.name}}) /socket-binding-group=standard-sockets/socket-binding=https:list-add(name=client-mappings,value={destination-address=${jboss.host.name}}) echo SETUP: Get rid of WARN WFLYTX0013 # Gets rid of WARN WFLYTX0013: Node identifier property is set to the default value. Please make sure it is unique. /subsystem=transactions:write-attribute(name=node-identifier,value="${env.NODE_IDENTIFIER:${jboss.node.name}}") echo SETUP: cleanup configuration if (outcome == success) of /subsystem=ejb3/service=remote:read-resource echo SETUP: Disable http remoting /subsystem=ejb3/service=remote:remove() echo end-if if (outcome == success) of /subsystem=modcluster/:read-resource echo SETUP: Remove modcluster subsystem /subsystem=modcluster:remove() /extension=org.jboss.as.modcluster:remove() /socket-binding-group=standard-sockets/socket-binding=modcluster:remove() echo end-if if (outcome == success) of /subsystem=undertow/server=default-server/ajp-listener=ajp:read-resource echo SETUP: Remove AJP Listener /subsystem=undertow/server=default-server/ajp-listener=ajp:remove() /socket-binding-group=standard-sockets/socket-binding=ajp:remove() echo end-if echo SETUP: Finished Keycloak custom configuration. stop-embedded-server ================================================ FILE: keycloak/cli/0010-register-smallrye-extensions.cli ================================================ embed-server --server-config=${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml} --std-out=echo echo Using server configuration file: :resolve-expression(expression=${env.JBOSS_HOME}/standalone/configuration/${env.KEYCLOAK_CONFIG_FILE:standalone-ha.xml}) echo SETUP: Add Smallrye Components /extension=org.wildfly.extension.microprofile.config-smallrye:add() /extension=org.wildfly.extension.microprofile.health-smallrye:add() /extension=org.wildfly.extension.microprofile.metrics-smallrye:add() # /subsystem=microprofile-config-smallrye:add() # /subsystem=microprofile-health-smallrye:add(security-enabled=false,empty-liveness-checks-status="${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}", empty-readiness-checks-status="${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}") # /subsystem=microprofile-metrics-smallrye:add(security-enabled=false,prefix="${wildfly.metrics.prefix:wildfly}") /subsystem=microprofile-metrics-smallrye/:write-attribute(name=exposed-subsystems,value=["*"]) echo SETUP: Finished Adding Smallrye Components stop-embedded-server ================================================ FILE: keycloak/cli/0020-onstart-setup-graylog-logging.cli ================================================ ================================================ FILE: keycloak/cli/0100-onstart-deploy-extensions.sh ================================================ #!/usr/bin/env bash set -eou pipefail echo Trigger Keycloak extensions deployment. touch /opt/jboss/keycloak/standalone/deployments/extensions.jar.dodeploy ================================================ FILE: keycloak/clisnippets/http-client-config.md ================================================ HTTP Client Configuration ---- # HTTP Client Definition ``` #### Keycloak HTTP-Client echo SETUP: Configure HTTP client for outgoing requests # General HTTP Connection properties /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.connection-pool-size, value=128) ``` # Proxy configuration ``` # Configure proxy routes for HttpClient SPI echo SETUP: Configure HTTP client with Proxy /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.proxy-mappings,value=[".*\\.(acme)\\.de;NO_PROXY",".*;http://www-proxy.acme.com:3128"]) ``` # Client Certificate configuration ``` # Configure client certificate for MTLS for HttpClient SPI echo SETUP: Configure HTTP client with client-certificate /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.client-keystore,value="") /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.client-keystore-password,value="changeit") /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.client-key-password,value="") /subsystem=keycloak-server/spi=connectionsHttpClient/provider=default:write-attribute(name=properties.disable-trust-manager,value="true") ``` ================================================ FILE: keycloak/clisnippets/json-logging.md ================================================ JSON Logging ---- ``` ### Logging Configuration ### # echo SETUP: Adjust Logging configuration # See: https://wildscribe.github.io/WildFly/13.0/subsystem/logging/json-formatter/index.html # See: https://github.com/wildfly/wildfly/blob/master/docs/src/main/asciidoc/_admin-guide/subsystem-configuration/Logging_Formatters.adoc#json-formatter # supported properties: [date-format, exception-output-type, key-overrides, meta-data, pretty-print, print-details, record-delimiter, zone-id] echo SETUP: Enable JSON Logging # /subsystem=logging/json-formatter=JSON-PATTERN:add(exception-output-type=formatted, key-overrides={timestamp=@timestamp,logger-name=logger_name,stack-trace=stack_trace,level=level_name}, meta-data={app=keycloak}) # /subsystem=logging/console-handler=CONSOLE:write-attribute(name=named-formatter,value=JSON-PATTERN) ``` ================================================ FILE: keycloak/clisnippets/map-keycloak-endpoint-to-custom-endpint.md ================================================ Example for mapping a Keycloak endpoint to a custom endpoint to workaround bugs --- ``` # Map Keycloak paths to custom endpoints to work around Keycloak bugs (the UUID regex matches a UUID V4 (random uuid pattern) /subsystem=undertow/configuration=filter/expression-filter=keycloakPathOverrideConsentEndpoint:add( \ expression="regex('/auth/admin/realms/acme-internal/users/([a-f\\d]{8}-[a-f\\d]{4}-4[a-f\\d]{3}-[89ab][a-f\\d]{3}-[a-f\\d]{12})/consents') -> rewrite('/auth/realms/acme-internal/custom-resources/users/$1/consents')" \ ) /subsystem=undertow/server=default-server/host=default-host/filter-ref=keycloakPathOverrideConsentEndpoint:add() ``` ================================================ FILE: keycloak/clisnippets/offline-sessions-lazy-loading.md ================================================ embed-server --server-config=standalone-ha.xml --std-out=echo /subsystem=logging/console-handler=CONSOLE:write-attribute(name=level,value=DEBUG) if (outcome == failed) of /subsystem=logging/logger=org.keycloak.models.sessions.infinispan/:read-resource /subsystem=logging/logger=org.keycloak.models.sessions.infinispan:add(level=DEBUG) end-if ###### echo SETUP: Customize Keycloak UserSessions SPI configuration # Add dedicated userSession config element to allow configuring elements. if (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/:read-resource echo SETUP: Add missing userSessions SPI /subsystem=keycloak-server/spi=userSessions:add() echo end-if echo SETUP: Configure built-in "infinispan" UserSessions loader if (outcome == failed) of /subsystem=keycloak-server/spi=userSessions/provider=infinispan/:read-resource echo SETUP: Add missing "infinispan" provider /subsystem=keycloak-server/spi=userSessions/provider=infinispan:add(enabled=true) /subsystem=keycloak-server/spi=userSessions/provider=infinispan:write-attribute(name=properties.preloadOfflineSessionsFromDatabase,value=${env.KEYCLOAK_INFINISPAN_SESSIONS_PRELOAD_DATABASE:false}) /subsystem=keycloak-server/spi=userSessions:write-attribute(name=default-provider,value=infinispan) echo end-if ================================================ FILE: keycloak/clisnippets/undertow-access.md ================================================ Undertow Configuration ---- ``` ### Undertow Configuration ### echo SETUP: Adjust Undertow configuration ## See undertow configuration # https://access.redhat.com/documentation/en-us/red_hat_jboss_enterprise_application_platform/7.4-beta/html/configuration_guide/configuring_the_web_server_undertow#undertow-configure-filters # https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#predicates-attributes-and-handlers # Reject requests for clients-registrations and the welcome page # -> response-code(302) # -> redirect('https://example.com') # -> redirect('${env.KEYCLOAK_FRONTEND_URL}') /subsystem=undertow/configuration=filter/expression-filter=rejectAccessDefault:add( \ expression="(regex('/auth/realms/.*/clients-registrations/openid-connect') or path('/auth/'))-> response-code(403)" \ ) /subsystem=undertow/server=default-server/host=default-host/filter-ref=rejectAccessDefault:add() ``` ================================================ FILE: keycloak/clisnippets/undertow-request-logging.md ================================================ Undertow Request Logging --- See: https://mirocupak.com/logging-requests-with-undertow/ Dump Requests for Debugging (Very verbose!!!) ``` batch /subsystem=undertow/configuration=filter/custom-filter=request-logging-filter:add(class-name=io.undertow.server.handlers.RequestDumpingHandler, module=io.undertow.core) /subsystem=undertow/server=default-server/host=default-host/filter-ref=request-logging-filter:add run-batch ``` Apache style access log See: https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#access-log-handler See: https://undertow.io/undertow-docs/undertow-docs-2.1.0/index.html#exchange-attributes-2 ``` /subsystem=undertow/server=default-server/host=default-host/setting=access-log:\ add(pattern="%h %t \"%r\" %s \"%{i,User-Agent}\"", use-server-log=true) ``` ================================================ FILE: keycloak/config/jmxremote.password ================================================ # https://docs.oracle.com/en/java/javase/11/management/monitoring-and-management-using-jmx-technology.html#GUID-3A4EFD73-E420-45E6-BF3D-277B496CD1E9 # controlRole default user from $JAVA_HOME/conf/management/jmxremote.access #username password controlRole password ================================================ FILE: keycloak/config/openid-config.json ================================================ { "device_authorization_endpoint": null, "backchannel_authentication_endpoint": null, "backchannel_authentication_request_signing_alg_values_supported": null, "mtls_endpoint_aliases": null } ================================================ FILE: keycloak/config/quarkus.properties ================================================ # Customize log level for the extensions package quarkus.log.category."com.github.thomasdarimont.keycloak".level=DEBUG # see https://quarkus.io/guides/smallrye-metrics # quarkus.smallrye-metrics.path=/actuator/metrics # see https://quarkus.io/guides/smallrye-health # quarkus.smallrye-health.root-path=/actuator/health # Use quarkus access logging # See https://quarkus.io/guides/http-reference#quarkus-vertx-http-config-group-access-log-config_quarkus.http.access-log.enabled #quarkus.http.access-log.enabled=true #quarkus.http.access-log.pattern=%h %l %u %t "%r" %s %b %m "%{i,Referer}" "%{i,User-Agent}" "%{i,X-Request-Id}" "%{i,X-Organization-Id}" %D # Needs to be true if the libraries are present, but disabling the tracer and exporter makes it do effectively nothing quarkus.opentelemetry.enabled=true quarkus.opentelemetry.tracer.enabled=false quarkus.opentelemetry.tracer.exporter.otlp.enabled=false # Disable http-server metrics to avoid dimensionality explosion # see: https://github.com/keycloak/keycloak/discussions/8490#discussioncomment-5092436 quarkus.micrometer.binder.http-server.enabled=false ================================================ FILE: keycloak/docker/pom.xml ================================================ com.github.thomasdarimont.keycloak keycloak-project-example ${revision}.${changelist} ../../pom.xml 4.0.0 docker pom ${project.organization.name} Keycloak Docker Image io.fabric8 docker-maven-plugin ${docker-maven-plugin.version} docker-build-100 docker build true true ${docker.image} ${project.version} ${keycloak.version} ${docker.file} ../extensions/target extensions.jar extensions ../themes themes ../config config ../cli cli ================================================ FILE: keycloak/docker/src/main/docker/keycloak/Dockerfile.alpine-slim ================================================ ARG KEYCLOAK_VERSION=18.0.2 ARG ALPINE_VERSION=3.15.0 ##################################################### # Base keycloak image with binary keycloak release # TODO use version from KEYCLOAK_VERSION once legacy keycloak image is available #FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy as keycloak FROM quay.io/keycloak/keycloak:17.0.1-legacy as keycloak ##################################################### # Prepare custom JDK with jlink # alpine:3.15.0 # see https://hub.docker.com/layers/alpine/library/alpine/3.15.0/images/sha256-c74f1b1166784193ea6c8f9440263b9be6cae07dfe35e32a5df7a31358ac2060?context=explore FROM alpine:$ALPINE_VERSION as java # See https://wiki.alpinelinux.org/wiki/Package_management#Advanced_APK_Usage #ENV OPENJDK_VERSION=~11.0 ENV OPENJDK_VERSION=11.0.14_p9-r0 RUN apk add binutils --no-cache --allow-untrusted --no-cache "openjdk11-jdk=${OPENJDK_VERSION}" "openjdk11-jmods=${OPENJDK_VERSION}" ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk ENV JAVA_TARGET /opt/java/java-runtime RUN echo "Create trimmed down JDK" && \ $JAVA_HOME/bin/jlink \ --no-header-files \ --strip-debug \ --no-man-pages \ --compress=2 \ --vm=server \ --exclude-files="**/bin/rmiregistry,**/bin/jrunscript,**/bin/rmid" \ --module-path "$JAVA_HOME/jmods" \ --add-modules java.base,java.instrument,java.logging,java.management,java.se,java.naming,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.security.auth,jdk.xml.dom,jdk.naming.dns,jdk.unsupported,jdk.crypto.cryptoki,jdk.crypto.ec,jdk.jcmd,jdk.internal.ed,jdk.internal.jvmstat,jdk.internal.le,jdk.internal.opt,jdk.internal.vm.ci,jdk.internal.vm.compiler,jdk.internal.vm.compiler.management,jdk.sctp \ --output $JAVA_TARGET && \ echo "Add Java Debugging tools from JDK" && \ cp $JAVA_HOME/lib/libjdwp.so $JAVA_TARGET/lib/ && \ cp $JAVA_HOME/lib/libdt_socket.so $JAVA_TARGET/lib/ && \ cp $JAVA_HOME/lib/*management*.so $JAVA_TARGET/lib/ && \ echo "Remove debug symbols from JDK" && \ strip -p --strip-unneeded $JAVA_TARGET/lib/server/libjvm.so ##################################################### # Start of custom Keycloak-Image assembly FROM alpine:$ALPINE_VERSION # Versions are determined by alpline base version RUN apk add -U --no-cache tzdata bash coreutils openssl # Copy customized JDK into this image COPY --from=java /opt/java/java-runtime /opt/java # Java ENV JAVA_HOME /opt/java ENV PATH $PATH:$JAVA_HOME/bin # Temporarily elevate permissions USER root # Keycloak ENV JBOSS_HOME /opt/jboss/keycloak ENV JBOSS_TOOLS /opt/jboss/tools # add dedicated group / user (jboss) to run keycloak instance RUN addgroup -S jboss -g 1000 && \ adduser -u 1000 -S -G jboss -h $JBOSS_HOME -s /sbin/nologin jboss # Copy Keycloak into this image COPY --from=keycloak --chown=jboss:jboss /opt/jboss /opt/jboss COPY --chown=jboss:jboss ./custom-docker-entrypoint.sh $JBOSS_TOOLS/ # Make tools executable by jboss user RUN chmod 755 $JBOSS_TOOLS/custom-docker-entrypoint.sh # Switch to jboss user USER jboss CMD ["-b", "0.0.0.0"] # Add custom Startup-Scripts COPY --chown=jboss:root maven/cli/ /opt/jboss/startup-scripts # Add feature configuration COPY --chown=jboss:root maven/config/ /opt/jboss/keycloak/standalone/configuration/ # Add Keycloak Extensions COPY --chown=jboss:root maven/extensions/ /opt/jboss/keycloak/standalone/deployments # Add custom Theme COPY --chown=jboss:root maven/themes/apps/ /opt/jboss/keycloak/themes/apps COPY --chown=jboss:root maven/themes/internal/ /opt/jboss/keycloak/themes/internal EXPOSE 8080 EXPOSE 8443 ENTRYPOINT [ "/opt/jboss/tools/custom-docker-entrypoint.sh" ] ================================================ FILE: keycloak/docker/src/main/docker/keycloak/Dockerfile.ci.plain ================================================ ARG KEYCLOAK_VERSION=18.0.2 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy ================================================ FILE: keycloak/docker/src/main/docker/keycloak/Dockerfile.plain ================================================ ARG KEYCLOAK_VERSION=18.0.2 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION-legacy USER root # Update OS packages RUN true \ && microdnf clean all \ && microdnf install zip \ && microdnf update --nodocs \ && microdnf clean all \ && true # Mitigate lo4j 1.x CVEs RUN echo Mitigating log4j CVEs && \ # https://www.cvedetails.com/cve/CVE-2022-23307/ # https://access.redhat.com/security/cve/cve-2022-23307 echo Mitigating Log4j CVE: CVE-2022-23307 && \ zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/chainsaw/* && \ echo Mitigating Log4j CVE: CVE-2022-23302 && \ # https://www.cvedetails.com/cve/CVE-2022-23302/ && \ zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/net/JMSAppender.class && \ zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/net/JMSSink.class && \ echo Mitigating Log4j CVE: CVE-2022-23305 && \ # https://www.cvedetails.com/cve/CVE-2022-23305/ zip -q -d $JBOSS_HOME/modules/system/layers/base/org/jboss/log4j/logmanager/main/log4j-jboss-logmanager-*.Final.jar org/apache/log4j/jdbc/* && \ echo Mitigating Log4j CVEs completed. USER jboss # Add custom Startup-Scripts COPY --chown=jboss:root maven/cli/ /opt/jboss/startup-scripts # Add feature configuration COPY --chown=jboss:root maven/config/ /opt/jboss/keycloak/standalone/configuration/ # Add Keycloak Extensions COPY --chown=jboss:root maven/extensions/ /opt/jboss/keycloak/standalone/deployments # Add custom Theme COPY --chown=jboss:root maven/themes/apps/ /opt/jboss/keycloak/themes/apps COPY --chown=jboss:root maven/themes/internal/ /opt/jboss/keycloak/themes/internal ================================================ FILE: keycloak/docker/src/main/docker/keycloak/custom-docker-entrypoint.sh ================================================ #!/bin/bash set -eou pipefail # Workaround for alpine base image differences if [[ -z ${BIND:-} ]]; then # BIND=$(hostname --all-ip-addresses) export BIND=$(hostname -i) fi # Call original Keycloak docker-entrypoint.sh exec /opt/jboss/tools/docker-entrypoint.sh ================================================ FILE: keycloak/docker/src/main/docker/keycloakx/Dockerfile.ci.plain ================================================ ARG KEYCLOAK_VERSION=26.5.7 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION ENV KC_FEATURES=preview ================================================ FILE: keycloak/docker/src/main/docker/keycloakx/Dockerfile.plain ================================================ ARG KEYCLOAK_VERSION=26.5.7 FROM quay.io/keycloak/keycloak:$KEYCLOAK_VERSION USER root ## Workaround for adding the current certifcate to the cacerts truststore # Import certificate into cacerts truststore #COPY --chown=keycloak:keycloak "./acme.test+1.pem" "/etc/x509/tls.crt.pem" #RUN keytool -import -cacerts -noprompt -file /etc/x509/tls.crt.pem -storepass changeit USER keycloak # Add feature configuration COPY --chown=keycloak:root maven/config/profile.properties /opt/keycloak/conf/profile.properties # Add Keycloak Extensions COPY --chown=keycloak:root maven/extensions/extensions.jar /opt/keycloak/providers/extensions.jar # Add custom Theme COPY --chown=keycloak:root maven/themes/apps/ /opt/keycloak/themes/apps COPY --chown=keycloak:root maven/themes/internal/ /opt/keycloak/themes/internal COPY --chown=keycloak:root maven/themes/internal-modern/ /opt/keycloak/themes/internal-modern COPY --chown=keycloak:root maven/themes/custom/ /opt/keycloak/themes/custom ================================================ FILE: keycloak/e2e-tests/.gitignore ================================================ cypress/videos/* cypress/screenshots/* ================================================ FILE: keycloak/e2e-tests/cypress/e2e/login/login.cy.ts ================================================ const {keycloak_host, test_realm} = Cypress.env(); import users from '../../fixtures/users.json' import i18nMsg from '../../fixtures/messages.json' import {loginUser, visitClient} from '../../utils/keycloakUtils' let browserLang = (navigator.language || 'en-EN').split("-")[0]; let msg = (i18nMsg as any)[browserLang]; let accountClientId = 'account-console'; context('Login...', () => { Cypress.on('uncaught:exception', (err, runnable) => { return false; }); it('with known Username and Password, then Logout should pass', () => { visitClient(accountClientId) cy.get('#kc-login').click() loginUser(users.tester) cy.get('.pf-v5-c-menu-toggle__text').click() cy.get('.pf-v5-c-menu__item').invoke("text").should('eq', msg.signOut) cy.get('.pf-v5-c-menu__item').click() }); it('with unknown Username should fail', () => { visitClient(accountClientId) cy.get('#kc-login').click() cy.get('#username').type(users.unknown.username) cy.get('input#kc-login').click() cy.get('#input-error-username').invoke("text").should(t => expect(t.trim()).equal(msg.errorInvalidUsernameOrEmail)) }); it('with known Username but invalid Password should fail', () => { visitClient(accountClientId) cy.get('#kc-login').click() loginUser(users.testerInvalidPass) cy.get('#input-error-password').invoke("text").should(t => expect(t.trim()).equal(msg.errorInvalidPassword)) }); }) ================================================ FILE: keycloak/e2e-tests/cypress/fixtures/messages.json ================================================ { "de": { "signOut": "Abmelden", "signIn": "Anmelden", "errorInvalidUsernameOrEmail": "Ungültiger Benutzername oder E-Mail.", "errorInvalidPassword": "Ungültiges Passwort." }, "en": { "signOut": "Sign out", "signIn": "Sign in", "errorInvalidUsernameOrEmail": "Invalid username or email.", "errorInvalidPassword": "Invalid password." } } ================================================ FILE: keycloak/e2e-tests/cypress/fixtures/users.json ================================================ { "tester": { "username": "tester", "password": "test", "email": "tester@local" }, "testerInvalidPass": { "username": "tester", "password": "invalid", "email": "tester@local" }, "unknown": { "username": "unknown", "password": "unknown", "email": "unknown@local" } } ================================================ FILE: keycloak/e2e-tests/cypress/plugins/index.ts ================================================ /// // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) import installLogsPrinter from "cypress-terminal-report/src/installLogsPrinter"; /** * @type {Cypress.PluginConfig} */ const pluginConfig: Cypress.PluginConfig = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config installLogsPrinter(on, { includeSuccessfulHookLogs: true, printLogsToConsole: 'onFail' }); on('before:browser:launch', (browser, launchOptions) => { // if (browser.family === 'chromium' && browser.name !== 'electron') { // // // use fixed langauge // // launchOptions.preferences.default['intl.accept_langauges'] = 'en'; // // // workaround for little memory in ci machines // // launchOptions.args.push( // // '--disable-dev-shm-usage' // // ) // } }); return config; } module.exports = pluginConfig; ================================================ FILE: keycloak/e2e-tests/cypress/support/commands.ts ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) import '@testing-library/cypress/add-commands' import 'cypress-xpath'; ================================================ FILE: keycloak/e2e-tests/cypress/support/e2e.ts ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: keycloak/e2e-tests/cypress/tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": [ "es5", "dom" ], "types": [ "cypress", "@types/testing-library__cypress" ], "strict": true, "esModuleInterop": true, "noEmit": true, "resolveJsonModule": true }, "include": [ "**/*.ts", "../node_modules/cypress" ], "exclude": [], } ================================================ FILE: keycloak/e2e-tests/cypress/utils/keycloakUtils.ts ================================================ const {keycloak_host, test_realm} = Cypress.env(); export function visitClient(clientId: string) { cy.visit(`${keycloak_host}/auth/realms/${test_realm}/clients/${clientId}/redirect`) } export function loginUser(user: any) { cy.get('#username').type(user.username) cy.get('#kc-login').click() cy.get('#password').type(user.password) cy.get('#kc-login').click() } ================================================ FILE: keycloak/e2e-tests/cypress.config.ts ================================================ import { defineConfig } from 'cypress' export default defineConfig({ viewportWidth: 1920, viewportHeight: 1080, env: { keycloak_host: 'https://id.acme.test:8443', test_realm: 'acme-internal', }, e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. setupNodeEvents(on, config) { return require('./cypress/plugins/index.ts')(on, config) }, }, }) ================================================ FILE: keycloak/e2e-tests/package.json ================================================ { "name": "keycloak-e2e-test", "version": "1.0.0", "description": "End to End tests for Keycloak", "author": "Thomas Darimont", "license": "SEE LICENSE IN LICENSE.md", "private": true, "scripts": { "cypress:open": "NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/cypress open", "cypress:test": "NODE_OPTIONS=--openssl-legacy-provider ./node_modules/.bin/cypress run", "type-check": "./node_modules/.bin/tsc --project ./cypress/tsconfig.json --noEmit" }, "devDependencies": { "@testing-library/cypress": "^8.0.3", "@types/testing-library__cypress": "^5.0.9", "cypress": "^10.8.0", "cypress-terminal-report": "^4.0.1", "cypress-xpath": "^1.6.2", "dotenv": "^16.0.1", "typescript": "^4.7.3" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } ================================================ FILE: keycloak/e2e-tests/readme.md ================================================ Keycloak End to End Tests --- # Build ``` yarn install ``` # Run Tests Start the Keycloak docker-compose system, then run the cypress tests with the following command: ``` yarn run cypress:test ``` ================================================ FILE: keycloak/extensions/pom.xml ================================================ com.github.thomasdarimont.keycloak keycloak-project-example ${revision}.${changelist} ../../pom.xml 4.0.0 extensions ${project.organization.name} Keycloak Extensions 4.0.1 4.1.0 4.0.1 1.12.2 4.5.14 2.19.1 6.2.9.Final org.keycloak keycloak-quarkus-server ${keycloak.version} provided org.keycloak keycloak-server-spi ${keycloak.version} provided org.keycloak keycloak-saml-core ${keycloak.version} provided org.keycloak keycloak-server-spi-private ${keycloak.version} provided org.keycloak keycloak-ldap-federation ${keycloak.version} provided org.keycloak keycloak-services ${keycloak.version} provided org.jboss.resteasy resteasy-multipart-provider org.apache.httpcomponents httpclient ${httpcomponents.version} provided org.keycloak keycloak-model-infinispan ${keycloak.version} provided org.freemarker freemarker ${freemarker.version} provided io.smallrye smallrye-health ${smallrye-health.version} provided io.micrometer micrometer-core ${micrometer.version} provided org.eclipse.microprofile.health microprofile-health-api ${microprofile-health-api.version} provided jakarta.enterprise jakarta.enterprise.cdi-api ${cdi-api.version} provided io.nats jnats ${jnats.version} org.keycloak keycloak-admin-client ${keycloak-admin-client.version} test org.jboss.resteasy resteasy-client org.jboss.resteasy resteasy-jackson2-provider org.jboss.resteasy resteasy-multipart-provider ${resteasy.version} test org.jboss.resteasy resteasy-client ${resteasy.version} test org.jboss.resteasy resteasy-jackson2-provider ${resteasy.version} test org.junit.jupiter junit-jupiter ${junit-jupiter.version} test org.assertj assertj-core ${assertj-core.version} test com.github.dasniko testcontainers-keycloak ${testcontainers-keycloak.version} test org.keycloak keycloak-model-jpa ${keycloak.version} provided org.wildfly.client wildfly-client-config 1.0.1.Final test org.wildfly.common wildfly-common 1.6.0.Final test org.projectlombok lombok ${lombok.version} provided true com.google.auto.service auto-service ${auto-service.version} provided true with-integration-tests org.apache.maven.plugins maven-failsafe-plugin ${maven-failsafe-plugin.version} integration-test integration-test verify **/*IntegrationTest.java true extensions org.apache.maven.plugins maven-surefire-plugin ${maven-surefire-plugin.version} **/*IntegrationTest.java java.util.logging.manager org.jboss.logmanager.LogManager org.apache.maven.plugins maven-jar-plugin ${maven-jar-plugin.version} true maven-assembly-plugin ${maven-assembly-plugin.version} jar-with-dependencies make-assembly package single ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountActivity.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import com.github.thomasdarimont.keycloak.custom.auth.mfa.MfaInfo; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.TrustedDeviceInfo; import com.github.thomasdarimont.keycloak.custom.support.RealmUtils; import jakarta.ws.rs.core.UriInfo; import lombok.extern.jbosslog.JBossLog; import org.keycloak.credential.CredentialModel; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel; import java.net.URI; import java.util.List; @JBossLog public class AccountActivity { public static void onUserMfaChanged(KeycloakSession session, RealmModel realm, UserModel user, CredentialModel credential, MfaChange change) { try { var credentialLabel = getCredentialLabel(credential); var mfaInfo = new MfaInfo(credential.getType(), credentialLabel); switch (change) { case ADD: AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("mfaInfo", mfaInfo); emailTemplateProvider.send("acmeMfaAddedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-mfa-added.ftl", attributes); }); break; case REMOVE: AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("mfaInfo", mfaInfo); emailTemplateProvider.send("acmeMfaRemovedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-mfa-removed.ftl", attributes); }); break; default: break; } } catch (EmailException e) { log.errorf(e, "Failed to send email for new user mfa change: %s.", change); } } public static void onAccountDeletionRequested(KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) { try { URI actionTokenUrl = AccountDeletion.createActionToken(session, realm, user, uriInfo); AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("actionTokenUrl", actionTokenUrl); emailTemplateProvider.send("acmeAccountDeletionRequestedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-account-deletion-requested.ftl", attributes); }); log.infof("Requested user account deletion. realm=%s userId=%s", realm.getName(), user.getId()); } catch (EmailException e) { log.errorf(e, "Failed to send email for account deletion request."); } } public static void onTrustedDeviceChange(KeycloakSession session, RealmModel realm, UserModel user, TrustedDeviceInfo trustedDeviceInfo, MfaChange change) { try { switch (change) { case ADD: AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("trustedDeviceInfo", trustedDeviceInfo); emailTemplateProvider.send("acmeTrustedDeviceAddedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-trusted-device-added.ftl", attributes); }); break; case REMOVE: AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("trustedDeviceInfo", trustedDeviceInfo); emailTemplateProvider.send("acmeTrustedDeviceRemovedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-trusted-device-removed.ftl", attributes); }); break; default: break; } } catch (EmailException e) { log.errorf(e, "Failed to send email for trusted device change: %s.", change); } } public static void onAccountLockedOut(KeycloakSession session, RealmModel realm, UserModel user, UserLoginFailureModel userLoginFailure) { try { AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("userLoginFailure", userLoginFailure); emailTemplateProvider.send("acmeAccountBlockedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-account-blocked.ftl", attributes); }); } catch (EmailException e) { log.errorf(e, "Failed to send email for user account block. userId=%s", userLoginFailure.getUserId()); } } public static void onAccountUpdate(KeycloakSession session, RealmModel realm, UserModel user, AccountChange update) { try { AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("update", update); emailTemplateProvider.send("acmeAccountUpdatedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-account-updated.ftl", attributes); }); } catch (EmailException e) { log.errorf(e, "Failed to send email for user account update. userId=%s", user.getId()); } } private static String getCredentialLabel(CredentialModel credential) { var type = credential.getType(); if (OTPCredentialModel.TYPE.equals(type)) { return type.toUpperCase(); } var label = credential.getUserLabel(); if (label == null || label.isEmpty()) { return ""; } return credential.getUserLabel(); } public static void onCredentialChange(KeycloakSession session, RealmModel realm, UserModel user, CredentialModel credential, MfaChange change) { log.debugf("credential change %s", change); if (WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType())) { onUserPasskeyChanged(session, realm, user, credential, change); return; } // TODO delegate to onUserMfaChanged } public static void onUserPasskeyChanged(KeycloakSession session, RealmModel realm, UserModel user, CredentialModel credential, MfaChange change) { try { var credentialLabel = getCredentialLabel(credential); var mfaInfo = new MfaInfo(credential.getType(), credentialLabel); switch (change) { case ADD: AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("passkeyInfo", mfaInfo); emailTemplateProvider.send("acmePasskeyAddedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-passkey-added.ftl", attributes); }); break; case REMOVE: AccountEmail.send(session, session.getProvider(EmailTemplateProvider.class), realm, user, (emailTemplateProvider, attributes) -> { attributes.put("passkeyInfo", mfaInfo); emailTemplateProvider.send("acmePasskeyRemovedSubject", List.of(RealmUtils.getDisplayName(realm)), "acme-passkey-removed.ftl", attributes); }); break; default: break; } } catch (EmailException e) { log.errorf(e, "Failed to send email for new user passkey change: %s.", change); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountChange.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import lombok.Data; @Data public class AccountChange { private final String changedAttribute; private final String changedValue; } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountDeletion.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import com.github.thomasdarimont.keycloak.custom.themes.login.AcmeUrlBean; import org.keycloak.common.util.Time; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.resources.LoginActionsService; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; import java.net.URI; public class AccountDeletion { public static URI createActionToken(KeycloakSession session, RealmModel realm, UserModel user, UriInfo uriInfo) { String userId = user.getId(); int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan(); int absoluteExpirationInSecs = Time.currentTime() + validityInSecs; RequestAccountDeletionActionToken requestAccountDeletionActionToken = new RequestAccountDeletionActionToken(userId, absoluteExpirationInSecs, Constants.ACCOUNT_MANAGEMENT_CLIENT_ID, new AcmeUrlBean(session).getAccountDeletedUrl()); String token = requestAccountDeletionActionToken.serialize(session, realm, uriInfo); UriBuilder builder = LoginActionsService.actionTokenProcessor(session.getContext().getUri()); builder.queryParam("key", token); String actionTokenLink = builder.build(realm.getName()).toString(); return URI.create(actionTokenLink); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountEmail.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.email.freemarker.beans.ProfileBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import java.util.HashMap; import java.util.Map; public class AccountEmail { public static void send(KeycloakSession session, EmailTemplateProvider emailTemplateProvider, RealmModel realm, UserModel user, SendEmailTask sendEmailTask) throws EmailException { if (emailTemplateProvider == null) { throw new EmailException("Missing emailTemplateProvider"); } emailTemplateProvider.setRealm(realm); emailTemplateProvider.setUser(user); Map attributes = new HashMap<>(); attributes.put("user", new ProfileBean(user, session)); sendEmailTask.sendEmail(emailTemplateProvider, attributes); } @FunctionalInterface public interface SendEmailTask { void sendEmail(EmailTemplateProvider emailTemplateProvider, Map attributes) throws EmailException; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/AccountPostLoginAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserModel; @JBossLog public class AccountPostLoginAction implements RequiredActionProvider { public static final String LAST_ACTIVITY_TIMESTAMP_ATTR = "lastActivityTimestamp"; @Override public void evaluateTriggers(RequiredActionContext context) { // Prevent multiple executions within current flow var authSession = context.getAuthenticationSession(); if (authSession.getAuthNote(getClass().getSimpleName()) != null) { return; // action was already executed } authSession.setAuthNote(getClass().getSimpleName(), "true"); log.infof("Post-processing account"); updateLastActivityTimestamp(context.getUser()); } private void updateLastActivityTimestamp(UserModel user) { user.setSingleAttribute(LAST_ACTIVITY_TIMESTAMP_ATTR, String.valueOf(Time.currentTimeMillis())); } @Override public void requiredActionChallenge(RequiredActionContext context) { // NOOP } @Override public void processAction(RequiredActionContext context) { // NOOP } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private static final AccountPostLoginAction INSTANCE = new AccountPostLoginAction(); @Override public String getId() { return "acme-account-post-processing"; } @Override public String getDisplayText() { return "Acme Account Post-Processing"; } @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/MfaChange.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; public enum MfaChange { ADD, REMOVE } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/RequestAccountDeletionActionToken.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import com.fasterxml.jackson.annotation.JsonProperty; import org.keycloak.authentication.actiontoken.DefaultActionToken; public class RequestAccountDeletionActionToken extends DefaultActionToken { public static final String TOKEN_TYPE = "acme-request-accountdeletion"; private static final String REDIRECT_URI = "acme:redirect-uri"; public RequestAccountDeletionActionToken(String userId, int absoluteExpirationInSecs, String clientId, String redirectUri) { super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null); this.issuedFor = clientId; setRedirectUri(redirectUri); } /** * Required for deserialization. */ @SuppressWarnings("unused") private RequestAccountDeletionActionToken() { } @JsonProperty(REDIRECT_URI) public String getRedirectUri() { return (String) getOtherClaims().get(REDIRECT_URI); } @JsonProperty(REDIRECT_URI) public final void setRedirectUri(String redirectUri) { if (redirectUri != null) { setOtherClaims(REDIRECT_URI, redirectUri); return; } getOtherClaims().remove(REDIRECT_URI); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/RequestAccountDeletionActionTokenHandler.java ================================================ package com.github.thomasdarimont.keycloak.custom.account; import com.github.thomasdarimont.keycloak.custom.profile.AcmeUserAttributes; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.TokenVerifier; import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler; import org.keycloak.authentication.actiontoken.ActionTokenContext; import org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory; import org.keycloak.authentication.actiontoken.TokenUtils; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import jakarta.ws.rs.core.Response; import java.net.URI; import java.time.LocalDate; import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; @JBossLog @AutoService(ActionTokenHandlerFactory.class) public class RequestAccountDeletionActionTokenHandler extends AbstractActionTokenHandler { private static final String ERROR_REQUEST_ACCOUNT_DELETION = "errorAccountDeletion"; public RequestAccountDeletionActionTokenHandler() { super(RequestAccountDeletionActionToken.TOKEN_TYPE, RequestAccountDeletionActionToken.class, ERROR_REQUEST_ACCOUNT_DELETION, EventType.DELETE_ACCOUNT, Errors.NOT_ALLOWED); } @Override public Response handleToken(RequestAccountDeletionActionToken token, ActionTokenContext tokenContext) { var authSession = tokenContext.getAuthenticationSession(); // deactivate user var authenticatedUser = authSession.getAuthenticatedUser(); authenticatedUser.setEnabled(false); authenticatedUser.setSingleAttribute(AcmeUserAttributes.ACCOUNT_DELETION_REQUESTED_AT.getAttributeName(), LocalDate.now().format(ISO_LOCAL_DATE)); log.infof("Marked user for account deletion. realm=%s userId=%s", authSession.getRealm().getName(), authenticatedUser.getId()); return Response.temporaryRedirect(URI.create(token.getRedirectUri())).build(); } @Override public TokenVerifier.Predicate[] getVerifiers(ActionTokenContext tokenContext) { // TODO add additional checks if necessary return TokenUtils.predicates(); } @Override public boolean canUseTokenRepeatedly(RequestAccountDeletionActionToken token, ActionTokenContext tokenContext) { return false; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/account/console/AcmeAccountConsoleFactory.java ================================================ package com.github.thomasdarimont.keycloak.custom.account.console; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.resource.AccountResourceProvider; import org.keycloak.services.resources.account.AccountConsole; import org.keycloak.services.resources.account.AccountConsoleFactory; import org.keycloak.theme.Theme; /** * Workaround for https://github.com/keycloak/keycloak/issues/40463 */ @JBossLog // @AutoService(AccountResourceProviderFactory.class) public class AcmeAccountConsoleFactory extends AccountConsoleFactory { @Override public void init(Config.Scope config) { super.init(config); log.info("Initializing AcmeAccountConsoleFactory"); } @Override public AccountResourceProvider create(KeycloakSession session) { RealmModel realm = session.getContext().getRealm(); ClientModel client = getAccountManagementClient(realm); Theme theme = getTheme(session); return new AccountConsole(session, client, theme); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/admin/ui/example/ExampleUiPageProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.admin.ui.example; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.ui.extend.UiPageProvider; import org.keycloak.services.ui.extend.UiPageProviderFactory; import org.keycloak.utils.KeycloakSessionUtil; import java.util.List; @JBossLog @AutoService(UiPageProviderFactory.class) public class ExampleUiPageProvider implements UiPageProvider, UiPageProviderFactory { @Override public String getId() { // Also used as lookup for messages resource bundle return "acme-admin-ui-example"; } @Override public String getHelpText() { return "An example Admin UI Page"; } @Override public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) { log.infof("Create component settings %s", model); /*List allStoredComponents = realm .getComponentsStream(realm.getId(), UiPageProvider.class.getName()) .filter(cm -> cm.getProviderId().equals(getId())).toList(); for (ComponentModel cm : allStoredComponents) { log.infof("%s", cm); }*/ } @Override public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) { log.infof("Update component settings %s", newModel); } @Override public void preRemove(KeycloakSession session, RealmModel realm, ComponentModel model) { log.infof("Remove component settings %s", model); } @Override public List getConfigProperties() { KeycloakSession keycloakSession = KeycloakSessionUtil.getKeycloakSession(); KeycloakContext context = keycloakSession.getContext(); RealmModel realm = context.getRealm(); return ProviderConfigurationBuilder.create() // .property() // .name("booleanProperty") // .label("Boolean Property") // .required(true) // .defaultValue(true) // .helpText("A boolean Property") // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .add() // .property() // .name("stringProperty") // .label("String Property") // .required(true) // .defaultValue("Default for " + realm.getName()) // .helpText("A String Property") // .type(ProviderConfigProperty.STRING_TYPE) // .add() // .build(); } @Override public void init(Config.Scope config) { log.infof("Init component settings %s", config); } @Override public void postInit(KeycloakSessionFactory factory) { log.infof("Post-init component settings %s", factory); } @Override public void close() { // NOOP } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/audit/AcmeAuditListener.java ================================================ package com.github.thomasdarimont.keycloak.custom.audit; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.account.AccountChange; import com.github.thomasdarimont.keycloak.custom.account.MfaChange; import com.github.thomasdarimont.keycloak.custom.support.CredentialUtils; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProviderFactory; import org.keycloak.events.EventListenerTransaction; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserLoginFailureModel; @JBossLog public class AcmeAuditListener implements EventListenerProvider { public static final String ID = "acme-audit-listener"; private final KeycloakSession session; private final EventListenerTransaction tx; public AcmeAuditListener(KeycloakSession session) { this.session = session; this.tx = new EventListenerTransaction(this::processAdminEventAfterTransaction, this::processUserEventAfterTransaction); session.getTransactionManager().enlistAfterCompletion(tx); } @Override public void onEvent(Event event) { tx.addEvent(event); } @Override public void onEvent(AdminEvent event, boolean includeRep) { tx.addAdminEvent(event, includeRep); } private void processUserEventAfterTransaction(Event event) { // called for each UserEvent’s log.infof("Forward to audit service: audit userEvent %s", event.getType()); try { var context = session.getContext(); var realm = context.getRealm(); var authSession = context.getAuthenticationSession(); var user = authSession == null ? null : authSession.getAuthenticatedUser(); if (user == null) { return; } switch (event.getType()) { case UPDATE_EMAIL: AccountActivity.onAccountUpdate(session, realm, user, new AccountChange("email", user.getEmail())); break; case UPDATE_TOTP: CredentialUtils.findFirstOtpCredential(user).ifPresent(credential -> // AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.ADD)); break; case REMOVE_TOTP: CredentialUtils.findFirstOtpCredential(user).ifPresent(credential -> // AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.REMOVE)); break; case REMOVE_CREDENTIAL: CredentialUtils.findFirstCredentialOfType(user, event.getDetails().get("credential_type")).ifPresent( credential -> AccountActivity.onCredentialChange(session, realm, user, credential, MfaChange.REMOVE) ); break; case UPDATE_CREDENTIAL: CredentialUtils.findFirstCredentialOfType(user, event.getDetails().get("credential_type")).ifPresent( credential -> AccountActivity.onCredentialChange(session, realm, user, credential, MfaChange.ADD) ); break; case USER_DISABLED_BY_PERMANENT_LOCKOUT: UserLoginFailureModel userLoginFailure = session.loginFailures().getUserLoginFailure(realm, user.getId()); AccountActivity.onAccountLockedOut(session, realm, user, userLoginFailure); break; } } catch (Exception ex) { log.errorf(ex, "Failed to handle userEvent %s", event.getType()); } } private void processAdminEventAfterTransaction(AdminEvent event, boolean includeRep) { // called for each AdminEvent’s // log.infof("Forward to audit service: audit adminEvent %s", event); } @Override public void close() { // called after component use } @AutoService(EventListenerProviderFactory.class) public static class Factory implements EventListenerProviderFactory { @Override public String getId() { return AcmeAuditListener.ID; } @Override // return singleton instance, create new AcmeAuditListener(session) or use lazy initialization public EventListenerProvider create(KeycloakSession session) { return new AcmeAuditListener(session); } @Override public void init(Config.Scope config) { /* configure factory */ } @Override // we could init our provider with information from other providers public void postInit(KeycloakSessionFactory factory) { /* post-process factory */ } @Override // close resources if necessary public void close() { /* release resources if necessary */ } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/authzen/AuthZen.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.authzen; import java.util.List; import java.util.Map; public class AuthZen { public record AccessRequest(Subject subject, Action action, Resource resource, Map context, List evaluations, Map options) { public AccessRequest(Subject subject, Action action, Resource resource, Map context) { this(subject, action, resource, context, null, null); } } public record AccessEvaluation(Subject subject, Action action, Resource resource, Map context) { public AccessEvaluation(Resource resource) { this(null, null, resource, null); } public AccessEvaluation(Action action, Resource resource) { this(null, action, resource, null); } } /** * See: https://openid.github.io/authzen/#section-7.1.2.1 */ public enum EvaluationOption { execute_all, deny_on_first_deny, permit_on_first_permit, } public record Subject(String type, String id, Map properties) { } public record Action(String name, Map properties) { public Action(String name) { this(name, null); } } public record Resource(String type, Object id, Map properties) { public Resource(String type) { this(type, null, null); } } public record AccessResponse(Boolean decision, Map context) { } public record AccessEvaluationsResponse(List evaluations) { } public record SearchRequest(Subject subject, Action action, Resource resource, Map context, PageRequest page) {} public record PageRequest(String token, Integer limit, Map properties){ } public record Page(String next_token, Integer count, Integer total, Map properties){ } public record SearchResponse(Page page, Map context, List results){ } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/authzen/AuthzenClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.authzen; import com.github.thomasdarimont.keycloak.custom.config.ClientConfig; import com.github.thomasdarimont.keycloak.custom.config.ConfigAccessor; import com.github.thomasdarimont.keycloak.custom.config.MapConfig; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import lombok.extern.jbosslog.JBossLog; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.util.CollectionUtil; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.RoleUtils; import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @JBossLog public class AuthzenClient { private static final Pattern COMMA_PATTERN = Pattern.compile(","); public static final String DEFAULT_AUTHZ_URL = "http://acme-opa:8181/v1/data/iam/keycloak/allow"; public static final String ACTION = "action"; public static final String DESCRIPTION = "description"; public static final String RESOURCE_TYPE = "resource_type"; public static final String RESOURCE_CLAIM_NAME = "resource_claim_name"; public static final String USE_REALM_ROLES = "useRealmRoles"; public static final String USE_CLIENT_ROLES = "useClientRoles"; public static final String USE_USER_ATTRIBUTES = "useUserAttributes"; public static final String USER_ATTRIBUTES = "userAttributes"; public static final String CONTEXT_ATTRIBUTES = "contextAttributes"; public static final String REALM_ATTRIBUTES = "realmAttributes"; public static final String CLIENT_ATTRIBUTES = "clientAttributes"; public static final String REQUEST_HEADERS = "requestHeaders"; public static final String USE_GROUPS = "useGroups"; public static final String AUTHZ_URL = "authzUrl"; public static final String AUTHZ_TYPE = "authz_type"; public static final String AUTHZ_TYPE_ACCESS = "access"; public static final String AUTHZ_TYPE_SEARCH = "search"; public AuthZen.AccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName) { var resource = createResource(config, realm, client); return checkAccess(session, config, realm, user, client, actionName, resource); } public AuthZen.AccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName, AuthZen.Resource resource) { var subject = createSubject(config, user, client); var accessContext = createAccessContext(session, config, user); var action = new AuthZen.Action(actionName); var accessRequest = new AuthZen.AccessRequest(subject, action, resource, accessContext); try { log.infof("Sending Authzen request. realm=%s user=%s client=%s actionName=%s resource=%s\n%s", // realm.getName(), user.getUsername(), client.getClientId(), actionName, resource, JsonSerialization.writeValueAsPrettyString(accessRequest)); } catch (IOException ioe) { log.warn("Failed to prepare Authzen request", ioe); } var authzUrl = config.getString(AUTHZ_URL, DEFAULT_AUTHZ_URL); var request = SimpleHttp.doPost(authzUrl, session); request.json(accessRequest); try { var accessResponse = fetchResponse(request, AuthZen.AccessResponse.class); log.infof("Received Authzen response. realm=%s user=%s client=%s\n%s", // realm.getName(), user.getUsername(), client.getClientId(), JsonSerialization.writeValueAsPrettyString(accessResponse)); return accessResponse; } catch (IOException ioe) { log.warn("Failed to process Authzen response", ioe); } return null; } public AuthZen.SearchResponse search(KeycloakSession session, MapConfig config, RealmModel realm, UserModel user, ClientModel client, String actionName, AuthZen.Resource resource) { var subject = createSubject(config, user, client); var accessContext = createAccessContext(session, config, user); var action = new AuthZen.Action(actionName); var accessRequest = new AuthZen.AccessRequest(subject, action, resource, accessContext); try { log.infof("Sending Authzen search request. realm=%s user=%s client=%s actionName=%s resource=%s\n%s", // realm.getName(), user.getUsername(), client.getClientId(), actionName, resource, JsonSerialization.writeValueAsPrettyString(accessRequest)); } catch (IOException ioe) { log.warn("Failed to prepare Authzen search request", ioe); } var authzUrl = config.getString(AUTHZ_URL, DEFAULT_AUTHZ_URL); var request = SimpleHttp.doPost(authzUrl, session); request.json(accessRequest); try { var searchResponse = fetchResponse(request, AuthZen.SearchResponse.class); log.infof("Received Authzen search response. realm=%s user=%s client=%s\n%s", // realm.getName(), user.getUsername(), client.getClientId(), JsonSerialization.writeValueAsPrettyString(searchResponse)); return searchResponse; } catch (IOException ioe) { log.warn("Failed to process Authzen search response", ioe); } return null; } protected AuthZen.Subject createSubject(ConfigAccessor config, UserModel user, ClientModel client) { var username = user.getUsername(); var realmRoles = config.getBoolean(USE_REALM_ROLES, true) ? fetchRealmRoles(user) : null; var clientRoles = config.getBoolean(USE_CLIENT_ROLES, true) ? fetchClientRoles(user, client) : null; Map userAttributes; if (config.getBoolean(USE_USER_ATTRIBUTES, true)) { userAttributes = config.isConfigured(USER_ATTRIBUTES, true) ? extractUserAttributes(user, config) : null; } else { userAttributes = null; } var groups = config.getBoolean(USE_GROUPS, true) ? fetchGroupNames(user) : null; var properties = new HashMap(); if (CollectionUtil.isNotEmpty(realmRoles)) { properties.put("realmRoles", realmRoles); } if (CollectionUtil.isNotEmpty(clientRoles)) { properties.put("clientRoles", clientRoles); } if (userAttributes != null && !userAttributes.isEmpty()) { properties.put("userAttributes", userAttributes); } if (CollectionUtil.isNotEmpty(groups)) { properties.put("groups", groups); } if (properties.isEmpty()) { properties = null; } return new AuthZen.Subject("user", username, properties); } protected AuthZen.Resource createResource(ConfigAccessor config, RealmModel realm, ClientModel client) { var realmAttributes = config.isConfigured(REALM_ATTRIBUTES, false) ? extractRealmAttributes(realm, config) : null; var clientAttributes = config.isConfigured(CLIENT_ATTRIBUTES, false) ? extractClientAttributes(client, config) : null; var properties = new HashMap(); properties.put("realmAttributes", realmAttributes); properties.put("clientAttributes", clientAttributes); properties.put("clientId", client.getClientId()); return new AuthZen.Resource("realm", realm.getName(), properties); } protected Map createAccessContext(KeycloakSession session, ConfigAccessor config, UserModel user) { var contextAttributes = config.isConfigured(CONTEXT_ATTRIBUTES, false) ? extractContextAttributes(session, user, config) : null; var headers = config.isConfigured(REQUEST_HEADERS, false) ? extractRequestHeaders(session, config) : null; Map accessContext = new HashMap<>(); if (contextAttributes != null && !contextAttributes.isEmpty()) { accessContext.put("contextAttributes", contextAttributes); } if (headers != null && !headers.isEmpty()) { accessContext.put("headers", headers); } if (!accessContext.isEmpty()) { return accessContext; } return null; } protected Map extractRequestHeaders(KeycloakSession session, ConfigAccessor config) { var headerNames = config.getValue(REQUEST_HEADERS); if (headerNames == null || headerNames.isBlank()) { return null; } var requestHeaders = session.getContext().getRequestHeaders(); var headers = new HashMap(); for (String header : COMMA_PATTERN.split(headerNames.trim())) { var value = requestHeaders.getHeaderString(header); headers.put(header, value); } if (headers.isEmpty()) { return null; } return headers; } protected Map extractContextAttributes(KeycloakSession session, UserModel user, ConfigAccessor config) { var contextAttributes = extractAttributes(user, config, CONTEXT_ATTRIBUTES, (u, attr) -> { Object value = switch (attr) { case "remoteAddress" -> session.getContext().getConnection().getRemoteAddr(); default -> null; }; return value; }, u -> null); return contextAttributes; } protected Map extractAttributes(T source, ConfigAccessor config, String attributesKey, BiFunction valueExtractor, Function> defaultValuesExtractor) { if (config == null) { return defaultValuesExtractor.apply(source); } var requestedAttributes = config.getValue(attributesKey); if (requestedAttributes == null || requestedAttributes.isBlank()) { return defaultValuesExtractor.apply(source); } var attributes = new HashMap(); for (String attribute : COMMA_PATTERN.split(requestedAttributes.trim())) { Object value = valueExtractor.apply(source, attribute); attributes.put(attribute, value); } return attributes; } protected Map extractUserAttributes(UserModel user, ConfigAccessor config) { var userAttributes = extractAttributes(user, config, USER_ATTRIBUTES, (u, attr) -> { Object value = switch (attr) { case "id" -> user.getId(); case "email" -> user.getEmail(); case "createdTimestamp" -> user.getCreatedTimestamp(); case "lastName" -> user.getLastName(); case "firstName" -> user.getFirstName(); case "federationLink" -> user.getFederationLink(); case "serviceAccountLink" -> user.getServiceAccountClientLink(); default -> user.getFirstAttribute(attr); }; return value; }, this::extractDefaultUserAttributes); return userAttributes; } protected Map extractClientAttributes(ClientModel client, ConfigAccessor config) { var clientConfig = new ClientConfig(client); return extractAttributes(client, config, CLIENT_ATTRIBUTES, (c, attr) -> clientConfig.getValue(attr), c -> null); } protected Map extractRealmAttributes(RealmModel realm, ConfigAccessor config) { var realmConfig = new RealmConfig(realm); return extractAttributes(realm, config, REALM_ATTRIBUTES, (r, attr) -> realmConfig.getValue(attr), r -> null); } protected List fetchGroupNames(UserModel user) { return user.getGroupsStream().map(GroupModel::getName).collect(Collectors.toList()); } protected List fetchClientRoles(UserModel user, ClientModel client) { Stream explicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getClientRoleMappingsStream(client)); Stream implicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream()); return Stream.concat(explicitClientRoles, implicitClientRoles) // .filter(RoleModel::isClientRole) // .map(this::normalizeRoleName) // .collect(Collectors.toList()); } protected List fetchRealmRoles(UserModel user) { // Set xxx = RoleUtils.getDeepUserRoleMappings(user); return RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream()) // .filter(r -> !r.isClientRole()).map(this::normalizeRoleName) // .collect(Collectors.toList()); } protected String normalizeRoleName(RoleModel role) { if (role.isClientRole()) { return ((ClientModel) role.getContainer()).getClientId() + ":" + role.getName(); } return role.getName(); } protected boolean getBoolean(Map config, String key, boolean defaultValue) { if (config == null) { return defaultValue; } return Boolean.parseBoolean(config.get(key)); } protected Map extractDefaultUserAttributes(UserModel user) { return Map.of("id", user.getId(), "email", user.getEmail()); } protected T fetchResponse(SimpleHttp request, Class responseType) throws IOException { try { log.debugf("Fetching url=%s", request.getUrl()); try (var response = request.asResponse()) { return response.asJson(responseType); } } catch (IOException e) { log.error("Authzen request failed", e); throw e; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/checkaccess/CheckAccessAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.checkaccess; import com.google.auto.service.AutoService; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; /** * Authenticator that can evaluate fixed policies based on client attributes from the current target client. * * Can be used as the last authenticator within an auth flow section. *

* Supported policies: *

    *
  • denyIfNotAllowed
  • *
  • allowIfNotDenied
  • *
*

* Some examples: *

For Groups: *

*

 * accessCheckGroupPolicy: denyIfNotAllowed
 * accessCheckGroupAllowAny: group1,group2
 * 
*

For Roles: *

*

 * accessCheckRolePolicy: allowIfNotDenied
 * accessCheckRoleDenyAny: role1,role2
 * 
*

For User Attributes: *

*

 * accessCheckAttributePolicy: denyIfNotAllowed
 * accessCheckAttributeAllowAny: attr1=foo,attr2
 * 
*/ @JBossLog public class CheckAccessAuthenticator implements Authenticator { public static final String ID = "acme-auth-check-access"; @Override public void authenticate(AuthenticationFlowContext context) { var session = context.getSession(); var realm = context.getRealm(); var user = context.getUser(); var authSession = context.getAuthenticationSession(); var client = authSession.getClient(); for (var check : List.of(this::checkAttributes, this::checkRoles, this::checkGroups)) { var checkResult = check.apply(session, realm, user, client); if (!checkResult.isMatched()) { continue; } log.debugf("Matched check %s allow: %s", checkResult.getName(), checkResult.isAllow()); if (checkResult.isAllow()) { context.success(); } else { context.failure(AuthenticationFlowError.ACCESS_DENIED); } return; } // TODO make default allow / deny configurable context.success(); } private CheckResult checkGroups(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client) { var allowResult = checkGroupsInternal(session, realm, client, user, "accessCheckGroupAllowAny"); var denyResult = checkGroupsInternal(session, realm, client, user, "accessCheckGroupDenyAny"); return evaluateCheck("Group", allowResult, denyResult, client); } private CheckResult checkRoles(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client) { var allowResult = checkRolesInternal(session, realm, user, client, "accessCheckRoleAllowAny"); var denyResult = checkRolesInternal(session, realm, user, client, "accessCheckRoleDenyAny"); return evaluateCheck("Role", allowResult, denyResult, client); } private CheckResult checkAttributes(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client) { var allowResult = checkAttributeInternal(user, client, "accessCheckAttributeAllowAny"); var denyResult = checkAttributeInternal(user, client, "accessCheckAttributeDenyAny"); return evaluateCheck("Attribute", allowResult, denyResult, client); } private CheckResult evaluateCheck(String check, Boolean allowResult, Boolean denyResult, ClientModel client) { var allow = false; var matched = true; if (allowResult == null && denyResult == null) { matched = false; } else { var policy = client.getAttribute("accessCheck" + check + "Policy"); if ("denyIfNotAllowed".equals(policy)) { allow = allowResult != null && allowResult; } else if ("allowIfNotDenied".equals(policy)) { allow = denyResult == null || !denyResult; } log.debugf("Evaluated check: %s with policy: %s. Outcome allow: %s ", check, policy, allow); } return new CheckResult(check, allow, matched); } private Boolean checkGroupsInternal(KeycloakSession session, RealmModel realm, ClientModel client, UserModel user, String checkAttributeName) { var checkAttribute = client.getAttribute(checkAttributeName); if (checkAttribute == null) { return null; } var groupNameEntries = checkAttribute.split(","); for (var groupNameEntry : groupNameEntries) { var groupName = groupNameEntry.trim(); // * matches all groups, even empty lists if (groupName.equals("*")) { return true; } var group = session.groups().searchForGroupByNameStream(realm, groupName, true, 0, 1).findAny().orElse(null); if (group == null) { log.debugf("group not found. realm:%s group:%s", realm.getName(), groupName); continue; } if (user.isMemberOf(group)) { return true; } } return false; } private Boolean checkRolesInternal(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, String checkAttributeName) { var checkAttribute = client.getAttribute(checkAttributeName); if (checkAttribute == null) { return null; } var roleNameEntries = checkAttribute.split(","); for (var roleNameEntry : roleNameEntries) { var roleNameCandidate = roleNameEntry.trim(); // * matches all roles, even empty lists if (roleNameCandidate.equals("*")) { return true; } var role = resolveRole(session, realm, client, roleNameCandidate); if (role == null) { continue; } if (user.hasRole(role)) { return true; } } return false; } private static RoleModel resolveRole(KeycloakSession session, RealmModel realm, ClientModel client, String roleNameEntry) { if (!roleNameEntry.contains(":")) { // realm roles can be referred to by "role" return session.roles().getRealmRole(realm, roleNameEntry); } var targetClient = client; // detected client role var clientWithRole = roleNameEntry.split(":"); var clientId = clientWithRole[0].trim(); var roleName = clientWithRole[1].trim(); if (!clientId.isEmpty()) { // other client-roles can be referred with otherClientId:clientRole targetClient = session.clients().getClientByClientId(realm, clientId); } // local client-roles can be referred to by ":clientRole" return session.roles().getClientRole(targetClient, roleName); } private static Boolean checkAttributeInternal(UserModel user, ClientModel client, String checkAttributeName) { var checkAttribute = client.getAttribute(checkAttributeName); if (checkAttribute == null) { return null; } var attributeValuePairs = checkAttribute.split(","); for (var attributeValuePair : attributeValuePairs) { var attrValuePair = attributeValuePair.split("="); var attributeName = attrValuePair[0].trim(); var attributeValue = attrValuePair.length > 1 ? attrValuePair[1].trim() : ""; var value = user.getFirstAttribute(attributeName); if (value == null) { continue; } if (attributeValue.equals("*")) { // we match every value as long as the attribute exists return true; } value = value.trim(); if (value.equals(attributeValue)) { // exact attribute match return true; } } return false; } @Override public void action(AuthenticationFlowContext context) { // NOOP } @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @AutoService(AuthenticatorFactory.class) public static class Factory extends OTPFormAuthenticatorFactory { public static final CheckAccessAuthenticator SINGLETON = new CheckAccessAuthenticator(); @Override public Authenticator create(KeycloakSession session) { return SINGLETON; } @Override public String getId() { return CheckAccessAuthenticator.ID; } @Override public String getDisplayType() { return "Acme: Check Access"; } @Override public String getHelpText() { return "Checks if the given user has access to the target application."; } @Override public List getConfigProperties() { return null; } @Override public String getReferenceCategory() { return "access"; } @Override public boolean isUserSetupAllowed() { return true; } } interface AccessCheck { CheckResult apply(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client); } @Data static class CheckResult { private final String name; private final boolean allow; private final boolean matched; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/confirmcookie/ConfirmCookieAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.confirmcookie; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.CookieAuthenticator; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ServerInfoAwareProviderFactory; import org.keycloak.services.managers.AuthenticationManager; import java.util.List; import java.util.Map; @JBossLog public class ConfirmCookieAuthenticator extends CookieAuthenticator { static final ConfirmCookieAuthenticator INSTANCE = new ConfirmCookieAuthenticator(); @Override public void authenticate(AuthenticationFlowContext context) { AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(context.getSession(), context.getRealm(), true); if (authResult == null) { context.attempted(); return; } Response response = context.form() // .createForm("login-confirm-cookie-form.ftl"); context.challenge(response); } @Override public void action(AuthenticationFlowContext context) { super.authenticate(context); } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return false; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { } @Override public void close() { } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory { @Override public String getId() { return "acme-auth-confirm-cookie"; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public String getDisplayType() { return "Acme: Confirm Cookie Authenticator"; } @Override public String getHelpText() { return "Shows a form asking to confirm cookie"; } @Override public String getReferenceCategory() { return "hello"; } @Override public boolean isConfigurable() { return true; } @Override public List getConfigProperties() { List properties = ProviderConfigurationBuilder.create() // .property().name("message").label("Message") .helpText("Message text").type(ProviderConfigProperty.STRING_TYPE) .defaultValue("hello").add() .build(); return properties; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public void postInit(KeycloakSessionFactory factory) { // called after factory is found } @Override public void init(Config.Scope config) { } @Override public void close() { } @Override public Map getOperationalInfo() { return Map.of("info", "infoValue"); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/customcookie/CustomCookieAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.customcookie; import com.google.auto.service.AutoService; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.CookieAuthenticator; import org.keycloak.authentication.authenticators.browser.CookieAuthenticatorFactory; import org.keycloak.models.KeycloakSession; public class CustomCookieAuthenticator extends CookieAuthenticator { private final KeycloakSession session; public CustomCookieAuthenticator(KeycloakSession session) { this.session = session; } @Override public void authenticate(AuthenticationFlowContext context) { super.authenticate(context); } // @AutoService(AuthenticatorFactory.class) public static class Factory extends CookieAuthenticatorFactory { @Override public Authenticator create(KeycloakSession session) { return new CustomCookieAuthenticator(session); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/debug/DebugAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.debug; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.sessions.AuthenticationSessionModel; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; @JBossLog public class DebugAuthenticator implements Authenticator { public final static String DEBUG_MESSAGE_TEMPLATE_KEY = "debugMessageTemplate"; public static final String DEFAULT_DEBUG_MESSAGE = "{alias} User{userId={userId}, username={username}, email={email}} Client{clientId={clientId}, clientUuid={clientUuid}}"; public DebugAuthenticator() { } @Override public void authenticate(AuthenticationFlowContext authenticationFlowContext) { String debugMessage = DEFAULT_DEBUG_MESSAGE; var authenticatorConfig = authenticationFlowContext.getAuthenticatorConfig(); if (authenticatorConfig != null && authenticatorConfig.getConfig() != null) { debugMessage = authenticatorConfig.getConfig().getOrDefault(DEBUG_MESSAGE_TEMPLATE_KEY, DEFAULT_DEBUG_MESSAGE); String alias = authenticatorConfig.getAlias(); if (alias == null) { alias = ""; } debugMessage = debugMessage.replace("{alias}", alias); } // authenticationFlowContext.getExecution(); // Post Broker Login after First Broker Login var authenticationSession = authenticationFlowContext.getAuthenticationSession(); String postBrokerLoginAfterFirstBrokerLogin = authenticationSession.getAuthNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); if (postBrokerLoginAfterFirstBrokerLogin != null) { debugMessage += " postBrokerLoginAfterFirstBrokerLogin=true"; } // Post Broker Login after consecutive login String postBrokerLoginAfterConsecutiveLogin = authenticationSession.getAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); if (postBrokerLoginAfterConsecutiveLogin != null) { debugMessage += " postBrokerLoginAfterConsecutiveLogin=true"; } var user = authenticationFlowContext.getUser(); if (user != null) { debugMessage = debugMessage.replaceAll(Pattern.quote("{userId}"), user.getId()); debugMessage = debugMessage.replaceAll(Pattern.quote("{username}"), user.getUsername()); debugMessage = debugMessage.replaceAll(Pattern.quote("{email}"), user.getEmail()); } var client = authenticationSession.getClient(); if (client != null) { debugMessage = debugMessage.replaceAll(Pattern.quote("{clientUuid}"), client.getClientId()); debugMessage = debugMessage.replaceAll(Pattern.quote("{clientId}"), client.getClientId()); } log.debug(debugMessage); authenticationFlowContext.success(); } @Override public void action(AuthenticationFlowContext authenticationFlowContext) { } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { return false; } @Override public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { } @Override public void close() { } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { private static final List CONFIG_PROPERTIES; static { var list = ProviderConfigurationBuilder.create() // .property().name(DEBUG_MESSAGE_TEMPLATE_KEY) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Debug Message") // .defaultValue(DEFAULT_DEBUG_MESSAGE) // .helpText("Debug Message template. Supported Parameters: {username}, {email}, {userId}, {clientId}") // .add() // .build(); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } @Override public String getId() { return "acme-debug-auth"; } @Override public Authenticator create(KeycloakSession session) { return new DebugAuthenticator(); } @Override public String getDisplayType() { return "Acme: Debug Auth Step"; } @Override public String getHelpText() { return "Prints the current step to the console."; } @Override public String getReferenceCategory() { return null; } @Override public boolean isConfigurable() { return true; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public void init(Config.Scope scope) { // NOOP } @Override public void postInit(KeycloakSessionFactory keycloakSessionFactory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/demo/SkippableRequiredAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.demo; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; import java.util.List; import java.util.Optional; public class SkippableRequiredAction implements RequiredActionProvider { public static final String PROVIDER_ID = "ACME_DEMO_SKIPPABLE_ACTION"; public static final String ACTION_SKIPPED_SESSION_NOTE = PROVIDER_ID + ":skipped"; public static final String SKIP_COUNT_USER_ATTRIBUTE = "acme-action-count"; public static final String ACTION_DONE_USER_ATTRIBUTE = "acme-action-done"; public static final String MAX_SKIP_COUNT_CONFIG_ATTRIBUTE = "max-skip-count"; @Override public void evaluateTriggers(RequiredActionContext context) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); // check if evaluate triggers was already called for this required action if (authSession.getAuthNote(PROVIDER_ID) != null) { return; } authSession.setAuthNote(PROVIDER_ID, ""); UserModel user = context.getUser(); if (!isUserActionRequired(user)) { return; } if (didUserSkipRequiredAction(context, authSession)) { return; } authSession.addRequiredAction(PROVIDER_ID); } protected boolean didUserSkipRequiredAction(RequiredActionContext context, AuthenticationSessionModel authSession) { // we remember the action skipping in the user session to have it available for every auth interaction within the current user session UserSessionModel userSession = context.getSession().sessions().getUserSession(context.getRealm(), authSession.getParentSession().getId()); return userSession != null && "true".equals(userSession.getNote(ACTION_SKIPPED_SESSION_NOTE)); } protected boolean isUserActionRequired(UserModel user) { return user.getFirstAttribute(ACTION_DONE_USER_ATTRIBUTE) == null; } @Override public void requiredActionChallenge(RequiredActionContext context) { Response challenge = createChallengeForm(context); context.challenge(challenge); } protected Response createChallengeForm(RequiredActionContext context) { LoginFormsProvider form = context.form(); boolean canSkip = isSkipActionPossible(context); form.setAttribute("canSkip", canSkip); return form.createForm("login-skippable-action.ftl"); } protected boolean isSkipActionPossible(RequiredActionContext context) { UserModel user = context.getUser(); int skipCount = Integer.parseInt(Optional.ofNullable(user.getFirstAttribute(SKIP_COUNT_USER_ATTRIBUTE)).orElse("0")); String maxSkipCountConfigValue = context.getConfig().getConfigValue(MAX_SKIP_COUNT_CONFIG_ATTRIBUTE); if (maxSkipCountConfigValue == null) { return false; } int maxSkipCount = Integer.parseInt(maxSkipCountConfigValue); return skipCount < maxSkipCount; } @Override public void processAction(RequiredActionContext context) { var formData = context.getHttpRequest().getDecodedFormParameters(); if (formData.containsKey("skip")) { if (!isSkipActionPossible(context)) { // nice try sneaky hacker Response challenge = createChallengeForm(context); context.challenge(challenge); return; } recordActionSkipped(context.getUser(), context.getAuthenticationSession()); context.success(); return; } markActionDone(context.getUser()); context.success(); } protected void markActionDone(UserModel user) { user.setSingleAttribute(ACTION_DONE_USER_ATTRIBUTE, Boolean.toString(true)); user.removeAttribute(SKIP_COUNT_USER_ATTRIBUTE); } protected void recordActionSkipped(UserModel user, AuthenticationSessionModel authSession) { int skipCount = Integer.parseInt(Optional.ofNullable(user.getFirstAttribute(SKIP_COUNT_USER_ATTRIBUTE)).orElse("0")); skipCount+=1; user.setSingleAttribute(SKIP_COUNT_USER_ATTRIBUTE, Integer.toString(skipCount)); authSession.setUserSessionNote(ACTION_SKIPPED_SESSION_NOTE, "true"); } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private static final SkippableRequiredAction INSTANCE = new SkippableRequiredAction(); @Override public String getId() { return PROVIDER_ID; } @Override public String getDisplayText() { return "Acme: Skippable Action"; } @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public List getConfigMetadata() { List configProperties = ProviderConfigurationBuilder.create() // .property() // .name(MAX_SKIP_COUNT_CONFIG_ATTRIBUTE) // .label("Max Skip") // .required(true) // .defaultValue(2) // .helpText("Maximum skip count") // .type(ProviderConfigProperty.INTEGER_TYPE) // .add() // .build(); return configProperties; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/dynamicidp/DynamicIdpAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.dynamicidp; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.Constants; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.services.Urls; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.sessions.AuthenticationSessionModel; import java.net.URI; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * Checks if the current user */ public class DynamicIdpAuthenticator implements Authenticator { @Override public void authenticate(AuthenticationFlowContext context) { var user = context.getUser(); if (user == null) { context.failure(AuthenticationFlowError.UNKNOWN_USER); return; } var realm = context.getRealm(); var session = context.getSession(); var idps = session.identityProviders().getAllStream().map(IdentityProviderModel::getAlias).collect(Collectors.toSet()); var identityProviderLinks = session.users().getFederatedIdentitiesStream(realm, user) // .filter(identity -> idps.contains(identity.getIdentityProvider())) // .toList(); if (identityProviderLinks.isEmpty()) { context.attempted(); return; } var primaryIdpLink = identityProviderLinks.getFirst(); var idp = session.identityProviders().getByIdOrAlias(primaryIdpLink.getIdentityProvider()); var authSession = context.getAuthenticationSession(); var clientSessionCode = new ClientSessionCode<>(session, realm, authSession); clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); var client = session.getContext().getClient(); var uriInfo = session.getContext().getUri().getBaseUri(); var loginUrl = Urls.identityProviderAuthnRequest(uriInfo, idp.getAlias(), realm.getName()).toString(); var uriBuilder = UriBuilder.fromUri(loginUrl); uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId()); uriBuilder.queryParam(LoginActionsService.SESSION_CODE, clientSessionCode.getOrGenerateCode()); uriBuilder.queryParam(Constants.TAB_ID, context.getUriInfo().getQueryParameters().getFirst(Constants.TAB_ID)); uriBuilder.queryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM, primaryIdpLink.getUserName()); URI targetUri = uriBuilder.build(); context.forceChallenge(Response.temporaryRedirect(targetUri).build()); } @Override public void action(AuthenticationFlowContext context) { // NOOP } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return false; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { } @Override public void close() { } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { @Override public String getId() { return "acme-dynamic-idp-selector"; } @Override public String getDisplayType() { return "Acme: Dynamic IDP Redirect"; } @Override public String getHelpText() { return "Redirect the user to it's primary IdP if connected"; } @Override public String getReferenceCategory() { return "idp"; } @Override public Authenticator create(KeycloakSession session) { return new DynamicIdpAuthenticator(); } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return Collections.emptyList(); } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/hello/HelloAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.hello; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ServerInfoAwareProviderFactory; import java.util.List; import java.util.Map; @JBossLog public class HelloAuthenticator implements Authenticator { static final HelloAuthenticator INSTANCE = new HelloAuthenticator(); @Override public void authenticate(AuthenticationFlowContext context) { // entrypoint // check auth // "force challenge if necessary" var authConfig = context.getAuthenticatorConfig(); String message = authConfig == null ? "Hello" : authConfig.getConfig().getOrDefault("message", "Hello"); String username = context.getAuthenticationSession().getAuthenticatedUser().getUsername(); log.infof("%s %s%n", message, username); context.getEvent().detail("message", message); context.success(); } @Override public void action(AuthenticationFlowContext context) { // handle user input // check input // mark as success // or on failure -> force challenge again } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return false; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { } @Override public void close() { } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory { @Override public String getId() { return "acme-auth-hello"; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public String getDisplayType() { return "Acme: Hello Authenticator"; } @Override public String getHelpText() { return "Prints a greeting for the user to the console"; } @Override public String getReferenceCategory() { return "hello"; } @Override public boolean isConfigurable() { return true; } @Override public List getConfigProperties() { List properties = ProviderConfigurationBuilder.create() // .property().name("message").label("Message") .helpText("Message text").type(ProviderConfigProperty.STRING_TYPE) .defaultValue("hello").add() .build(); return properties; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public void postInit(KeycloakSessionFactory factory) { // called after factory is found } @Override public void init(Config.Scope config) { // spi-authenticator-acme-auth-hello-message // config.get("message"); // called when provider factory is used } @Override public void close() { } @Override public Map getOperationalInfo() { return Map.of("info", "infoValue"); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/idpselection/AcmeDynamicIdpLookupUsernameForm.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.idpselection; import com.github.thomasdarimont.keycloak.custom.support.ConfigUtils; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.freemarker.model.IdentityProviderBean; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; /** * Custom {@link Authenticator} that combines the {@link org.keycloak.authentication.authenticators.browser.UsernameForm} * with {@link org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator}. */ @JBossLog public class AcmeDynamicIdpLookupUsernameForm extends UsernamePasswordForm { public static final String EMAIL_DOMAIN_REGEX_IDP_CONFIG_PROPERTY = "acmeEmailDomainRegex"; public static final String LOOKUP_REALM_NAME_CONFIG_PROPERTY = "lookupRealmName"; public static final String LOOKUP_REALM_IDP_ALIAS_CONFIG_PROPERTY = "lookupRealmIdpAlias"; @Override public void authenticate(AuthenticationFlowContext context) { if (context.getUser() != null) { // We can skip the form when user is re-authenticating. Unless current user has some IDP set, so he can re-authenticate with that IDP IdentityProviderBean identityProviderBean = new IdentityProviderBean(context.getSession(), context.getRealm(), null, context); List identityProviders = identityProviderBean.getProviders(); if (identityProviders.isEmpty()) { context.success(); return; } } super.authenticate(context); } @Override public void action(AuthenticationFlowContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); if (formData.containsKey("cancel")) { context.cancelLogin(); return; } var session = context.getSession(); var realm = context.getRealm(); boolean localUserFound = validateForm(context, formData); if (localUserFound) { // local apps user found in current realm UserModel localUser = context.getUser(); if (!enabledUser(context, localUser)) { return; } // check user for associated identity providers List connectedIdentityProviders = session.users().getFederatedIdentitiesStream(realm, localUser).toList(); // there is only one linked identity provider if (connectedIdentityProviders.size() == 1) { // redirect to the associated account FederatedIdentityModel idpIdentity = connectedIdentityProviders.get(0); redirect(context, idpIdentity.getIdentityProvider(), localUser.getEmail()); return; } else { // TODO handle user with zero or > 1 associated idps // TODO determine the primary IdP for users // log.debugf("Multiple IdPs found for user: %s", localUser.getUsername()); // var identityProviders = new ArrayList(); // for (FederatedIdentityModel idpIdentity : connectedIdentityProviders) { // // copy IdentityProviderModel to remove the hideOnLoginPage config // IdentityProviderModel identityProviderByAlias = new IdentityProviderModel(realm.getIdentityProviderByAlias(idpIdentity.getIdentityProvider())); // identityProviderByAlias.getConfig().remove("hideOnLoginPage"); // identityProviders.add(identityProviderByAlias); // } // Response response = context.form() // .setAttribute("customSocial", new IdentityProviderBean(realm, session, identityProviders, context.getUriInfo().getRequestUri())) // .createForm("login-idp-selection.ftl"); // context.forceChallenge(response); // we could not determine a target IdP, thus we fail the authentication context.clearUser(); context.attempted(); return; } } var authenticatorConfig = ConfigUtils.getConfig(context.getAuthenticatorConfig(), Map.of()); // local user NOT found String username = formData.getFirst(AuthenticationManager.FORM_USERNAME); if (username == null || username.isBlank()) { context.clearUser(); context.attempted(); return; } // try lookup in lookup realm String lookupRealmName = authenticatorConfig.get(LOOKUP_REALM_NAME_CONFIG_PROPERTY); String lookupRealmIdpAlias = authenticatorConfig.get(LOOKUP_REALM_IDP_ALIAS_CONFIG_PROPERTY); UserModel lookupRealmUser = findUserInLookupRealm(session, lookupRealmName, username); if (lookupRealmUser != null) { // local user found in lookup-realm, redirect user to lookup-realm for login log.infof("redirect user to %s via %s", lookupRealmName, lookupRealmIdpAlias); redirect(context, lookupRealmIdpAlias, lookupRealmUser.getEmail()); return; } // no local user in lookup-realm found, try to identity target IdP by email String targetIdpAlias = resolveTargetIdpByEmailDomain(realm, username, lookupRealmIdpAlias); if (targetIdpAlias != null) { // redirect user to targetIdp redirect(context, targetIdpAlias, username); return; } // we could not found a target IdP, thus we fail the authentication here // fall through here, we just propagate the user not found error to the form } private String resolveTargetIdpByEmailDomain(RealmModel realm, String email, String lookupRealmIdpAlias) { if (!Validation.isEmailValid(email)) { // not an email address return null; } String domain = email.split("@")[1].strip(); String idpAliasForEmail = realm.getIdentityProvidersStream().filter(idp -> { Map config = idp.getConfig(); if (lookupRealmIdpAlias.equals(idp.getAlias())) { return false; } if (config == null) { return false; } String idpEmailDomainRegex = config.get(EMAIL_DOMAIN_REGEX_IDP_CONFIG_PROPERTY); return domain.matches(idpEmailDomainRegex); })// .findFirst() // .map(IdentityProviderModel::getAlias) // .orElse(null); return idpAliasForEmail; } @Override protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { return validateUser(context, formData); } /** * Initiate a login through the Identity provider with the given providerId and loginHint. * * @param context * @param providerId * @param loginHint */ private void redirect(AuthenticationFlowContext context, String providerId, String loginHint) { // adapted from org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator.redirect Optional idp = context.getRealm().getIdentityProvidersStream() // .filter(IdentityProviderModel::isEnabled) // .filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias())) // .findFirst(); if (idp.isEmpty()) { log.warnf("Identity Provider not found or not enabled for realm. realm=%s provider=%s", context.getRealm().getName(), providerId); context.attempted(); return; } String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode(); String clientId = context.getAuthenticationSession().getClient().getClientId(); String tabId = context.getAuthenticationSession().getTabId(); URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId, null, loginHint); Response response = Response.seeOther(location).build(); log.debugf("Redirecting to %s", providerId); context.forceChallenge(response); } @Override protected Response challenge(AuthenticationFlowContext context, MultivaluedMap formData) { LoginFormsProvider forms = context.form(); if (!formData.isEmpty()) { forms.setFormData(formData); } return forms.createLoginUsername(); } @Override protected Response createLoginForm(LoginFormsProvider form) { return form.createLoginUsername(); } @Override protected String getDefaultChallengeMessage(AuthenticationFlowContext context) { if (context.getRealm().isLoginWithEmailAllowed()) { return Messages.INVALID_USERNAME_OR_EMAIL; } return Messages.INVALID_USERNAME; } protected UserModel findUserInLookupRealm(KeycloakSession session, String lookupRealmName, String email) { var localRealm = session.realms().getRealmByName(lookupRealmName); var localUser = session.users().getUserByEmail(localRealm, email); return localUser; } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { @Override public String getId() { return "acme-auth-username-idp-select"; } @Override public String getDisplayType() { return "Dynamic Idp Selection based on email domain."; } @Override public String getReferenceCategory() { return "lookup"; } @Override public String getHelpText() { return "Redirects a user to a local user realm or the appropriate IdP for login"; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public boolean isConfigurable() { return true; } @Override public List getConfigProperties() { var configProperties = ProviderConfigurationBuilder.create() .property() .name(LOOKUP_REALM_IDP_ALIAS_CONFIG_PROPERTY) .label("Lookup Realm IdP Alias") .helpText("IdP Alias in current realm that points to lookup realm") .type(ProviderConfigProperty.STRING_TYPE) .defaultValue("") .add() .property() .name(LOOKUP_REALM_NAME_CONFIG_PROPERTY) .label("Lookup Realm Name") .helpText("Name of lookup realm") .type(ProviderConfigProperty.STRING_TYPE) .defaultValue("") .add() .build() ; return configProperties; } @Override public Authenticator create(KeycloakSession session) { return new AcmeDynamicIdpLookupUsernameForm(); } @Override public void init(Config.Scope config) { // called when component is "created" // access to provider configuration } @Override public void postInit(KeycloakSessionFactory factory) { // called after component is discovered } @Override public void close() { // clear up state } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/magiclink/MagicLinkAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.magiclink; import com.github.thomasdarimont.keycloak.custom.support.RealmUtils; import com.github.thomasdarimont.keycloak.custom.support.UserUtils; import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j public class MagicLinkAuthenticator implements Authenticator { public static final String ID = "acme-magic-link"; private static final String MAGIC_LINK_KEY = "magic-link-key"; private static final String QUERY_PARAM = "acme_magic_link_key"; @Override public void authenticate(AuthenticationFlowContext context) { var sessionKey = context.getAuthenticationSession().getAuthNote(MAGIC_LINK_KEY); if (sessionKey == null) { var user = context.getUser(); if (user == null) { // to avoid account enumeration, we show the success page anyways. displayMagicLinkPage(context); return; } sendMagicLink(context); return; } var requestKey = context.getHttpRequest().getUri().getQueryParameters().getFirst(QUERY_PARAM); if (requestKey == null) { displayMagicLinkPage(context); return; } context.getEvent().detail("authenticator", ID); if (requestKey.equals(sessionKey)) { context.success(); } else { context.failure(AuthenticationFlowError.INVALID_CREDENTIALS); context.getEvent().detail("error", "magicSessionKey mismatch"); } } @Override public void action(AuthenticationFlowContext context) { // NOOP } private void sendMagicLink(AuthenticationFlowContext context) { var magicLinkSessionKey = KeycloakModelUtils.generateId(); context.getAuthenticationSession().setAuthNote(MAGIC_LINK_KEY, magicLinkSessionKey); var emailTemplateProvider = context.getSession().getProvider(EmailTemplateProvider.class); emailTemplateProvider.setRealm(context.getRealm()); emailTemplateProvider.setUser(context.getUser()); var magicLink = generateMagicLink(context, magicLinkSessionKey); // for further processing we need a mutable map here Map msgParams = new HashMap<>(); msgParams.put("userDisplayName", UserUtils.deriveDisplayName(context.getUser())); msgParams.put("link", magicLink); var subjectParams = List.of(RealmUtils.getDisplayName(context.getRealm())); try { emailTemplateProvider.send("acmeMagicLinkEmailSubject", subjectParams, "acme-magic-link.ftl", msgParams); displayMagicLinkPage(context); } catch (EmailException e) { log.error("Could not send magic link per email.", e); context.failure(AuthenticationFlowError.INTERNAL_ERROR); } } private String generateMagicLink(AuthenticationFlowContext context, String magicLinkSessionKey) { // TODO generate an Application initiated Action link to allow opening the link on with other devices. return KeycloakUriBuilder.fromUri(context.getRefreshExecutionUrl()).queryParam(QUERY_PARAM, magicLinkSessionKey).build().toString(); } private void displayMagicLinkPage(AuthenticationFlowContext context) { var form = context.form().setAttribute("skipLink", true); form.setInfo("acmeMagicLinkText"); context.challenge(form.createForm("login-magic-link.ftl")); } @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { private static final MagicLinkAuthenticator INSTANCE = new MagicLinkAuthenticator(); @Override public String getId() { return ID; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public String getDisplayType() { return "Acme: MagicLink"; } @Override public String getHelpText() { return "Allows the user to login with a link sent via email."; } @Override public String getReferenceCategory() { return "passwordless"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return null; } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/MfaInfo.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor public class MfaInfo { private final String type; private final String label; } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/EmailCodeAuthenticatorForm.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowException; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.CredentialValidator; import org.keycloak.common.util.SecretGenerator; import org.keycloak.credential.CredentialProvider; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.services.messages.Messages; import java.util.HashMap; import java.util.List; import java.util.Map; @JBossLog public class EmailCodeAuthenticatorForm implements Authenticator, CredentialValidator { static final String ID = "acme-email-code-form"; public static final String EMAIL_CODE = "emailCode"; public static final int LENGTH = 8; private final KeycloakSession session; public EmailCodeAuthenticatorForm(KeycloakSession session) { this.session = session; } @Override public void authenticate(AuthenticationFlowContext context) { challenge(context, null); } private void challenge(AuthenticationFlowContext context, FormMessage errorMessage) { generateAndSendEmailCode(context); LoginFormsProvider form = context.form().setExecution(context.getExecution().getId()); if (errorMessage != null) { form.setErrors(List.of(errorMessage)); } form.setAttribute("codeLength", LENGTH + 1); form.setAttribute("tryAutoSubmit", true); form.setAttribute("codePattern", "\\d{4}-\\d{4}"); Response response = form.createForm("email-code-form.ftl"); context.challenge(response); } private void generateAndSendEmailCode(AuthenticationFlowContext context) { if (context.getAuthenticationSession().getAuthNote(EMAIL_CODE) != null) { // skip sending email code return; } var emailCode = SecretGenerator.getInstance().randomString(LENGTH, SecretGenerator.DIGITS); sendEmailWithCode(context.getRealm(), context.getUser(), toDisplayCode(emailCode)); context.getAuthenticationSession().setAuthNote(EMAIL_CODE, emailCode); } private String toDisplayCode(String emailCode) { return new StringBuilder(emailCode).insert(LENGTH / 2, "-").toString(); } private String fromDisplayCode(String code) { return code.replace("-", ""); } @Override public void action(AuthenticationFlowContext context) { var formData = context.getHttpRequest().getDecodedFormParameters(); if (formData.containsKey("resend")) { resetEmailCode(context); challenge(context, null); return; } if (formData.containsKey("cancel")) { resetEmailCode(context); context.resetFlow(); return; } var givenEmailCode = fromDisplayCode(formData.getFirst(EMAIL_CODE)); var valid = validateCode(context, givenEmailCode); // TODO add brute-force protection for email code auth context.getEvent().realm(context.getRealm()).user(context.getUser()).detail("authenticator", ID); if (!valid) { context.getEvent().event(EventType.LOGIN_ERROR).error(Errors.INVALID_USER_CREDENTIALS); challenge(context, new FormMessage(Messages.INVALID_ACCESS_CODE)); return; } resetEmailCode(context); context.getEvent().event(EventType.LOGIN).success(); context.success(); } private void resetEmailCode(AuthenticationFlowContext context) { context.getAuthenticationSession().removeAuthNote(EMAIL_CODE); } private boolean validateCode(AuthenticationFlowContext context, String givenCode) { var emailCode = context.getAuthenticationSession().getAuthNote(EMAIL_CODE); return emailCode.equals(givenCode); } @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return user.credentialManager().isConfiguredFor(EmailCodeCredentialModel.TYPE); } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } private void sendEmailWithCode(RealmModel realm, UserModel user, String code) { if (user.getEmail() == null) { log.warnf("Could not send access code email due to missing email. realm=%s user=%s", realm.getId(), user.getUsername()); throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_USER); } Map mailBodyAttributes = new HashMap<>(); mailBodyAttributes.put("username", user.getUsername()); mailBodyAttributes.put("code", code); var realmName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName(); List subjectParams = List.of(realmName); try { var emailProvider = session.getProvider(EmailTemplateProvider.class); emailProvider.setRealm(realm); emailProvider.setUser(user); // Don't forget to add the code-email.ftl (html and text) template to your theme. emailProvider.send("emailCodeSubject", subjectParams, "code-email.ftl", mailBodyAttributes); } catch (EmailException eex) { log.errorf(eex, "Failed to send access code email. realm=%s user=%s", realm.getId(), user.getUsername()); } } @Override public EmailCodeCredentialProvider getCredentialProvider(KeycloakSession session) { // needed to access CredentialTypeMetadata for selecting authenticator options return (EmailCodeCredentialProvider)session.getProvider(CredentialProvider.class, EmailCodeCredentialProvider.ID); } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { @Override public String getDisplayType() { return "Acme: Email Code Form"; } @Override public String getReferenceCategory() { return EmailCodeCredentialModel.TYPE; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public String getHelpText() { return "Email code authenticator."; } @Override public List getConfigProperties() { return null; } @Override public void close() { // NOOP } @Override public Authenticator create(KeycloakSession session) { return new EmailCodeAuthenticatorForm(session); } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public String getId() { return EmailCodeAuthenticatorForm.ID; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/EmailCodeCredentialModel.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode; import org.keycloak.credential.CredentialModel; public class EmailCodeCredentialModel extends CredentialModel { public static final String TYPE = "mfa-email-code"; public EmailCodeCredentialModel() { setType(TYPE); setUserLabel("Email OTP"); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/EmailCodeCredentialProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.account.MfaChange; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.credential.CredentialTypeMetadataContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @JBossLog public class EmailCodeCredentialProvider implements CredentialProvider, CredentialInputValidator { public static final String ID = "acme-mfa-email-code"; private final KeycloakSession session; public EmailCodeCredentialProvider(KeycloakSession session) { this.session = session; } @Override public boolean supportsCredentialType(String credentialType) { return EmailCodeCredentialModel.TYPE.equals(credentialType); } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().isPresent(); } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { return false; } @Override public String getType() { return EmailCodeCredentialModel.TYPE; } @Override public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) { if (!(credentialModel instanceof EmailCodeCredentialModel)) { return null; } user.credentialManager().createStoredCredential(credentialModel); return credentialModel; } @Override public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) { var credential = user.credentialManager().getStoredCredentialById(credentialId); var deleted = user.credentialManager().removeStoredCredentialById(credentialId); if (deleted) { AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.REMOVE); } return deleted; } @Override public CredentialModel getCredentialFromModel(CredentialModel model) { if (!getType().equals(model.getType())) { return null; } return model; } @Override public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) { var builder = CredentialTypeMetadata.builder(); builder.type(getType()); builder.category(CredentialTypeMetadata.Category.TWO_FACTOR); builder.createAction(RegisterEmailCodeRequiredAction.ID); builder.removeable(true); builder.displayName("mfa-email-code-display-name"); builder.helpText("mfa-email-code-help-text"); // builder.updateAction(GenerateBackupCodeAction.ID); // TODO configure proper FA icon for email-code auth builder.iconCssClass("kcAuthenticatorMfaEmailCodeClass"); return builder.build(session); } @SuppressWarnings("rawtypes") @AutoService(CredentialProviderFactory.class) public static class Factory implements CredentialProviderFactory { @Override public CredentialProvider create(KeycloakSession session) { return new EmailCodeCredentialProvider(session); } @Override public String getId() { return EmailCodeCredentialProvider.ID; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/emailcode/RegisterEmailCodeRequiredAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.account.MfaChange; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @JBossLog public class RegisterEmailCodeRequiredAction implements RequiredActionProvider { public static final String ID = "acme-register-email-code"; @Override public void evaluateTriggers(RequiredActionContext context) { // NOOP } @Override public InitiatedActionSupport initiatedActionSupport() { // we want to trigger that action via kc_actions URL parameter in the auth url return InitiatedActionSupport.SUPPORTED; } @Override public void requiredActionChallenge(RequiredActionContext context) { var session = context.getSession(); var user = context.getUser(); var realm = context.getRealm(); var credentialManager = user.credentialManager(); credentialManager.getStoredCredentialsByTypeStream(EmailCodeCredentialModel.TYPE).forEach(cm -> credentialManager.removeStoredCredentialById(cm.getId())); var model = new EmailCodeCredentialModel(); model.setCreatedDate(Time.currentTimeMillis()); var credential = user.credentialManager().createStoredCredential(model); if (credential != null) { AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.ADD); } context.success(); } @Override public void processAction(RequiredActionContext context) { // NOOP } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private static final RequiredActionProvider INSTANCE = new RegisterEmailCodeRequiredAction(); @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } @Override public String getId() { return RegisterEmailCodeRequiredAction.ID; } @Override public String getDisplayText() { return "Acme: Register MFA via E-Mail code"; } @Override public boolean isOneTimeAction() { return true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/otp/AcmeOTPFormAuthenticator.java ================================================ /* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.thomasdarimont.keycloak.custom.auth.mfa.otp; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.ManageTrustedDeviceAction; import com.google.auto.service.AutoService; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.FlowStatus; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator; import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import jakarta.ws.rs.core.MultivaluedMap; import java.util.List; public class AcmeOTPFormAuthenticator extends OTPFormAuthenticator { public static final String ID = "acme-auth-otp-form"; @Override public void validateOTP(AuthenticationFlowContext context) { super.validateOTP(context); if (FlowStatus.SUCCESS.equals(context.getStatus())) { MultivaluedMap formParams = context.getHttpRequest().getDecodedFormParameters(); if (formParams.containsKey("register-trusted-device")) { context.getUser().addRequiredAction(ManageTrustedDeviceAction.ID); } } } @AutoService(AuthenticatorFactory.class) public static class Factory extends OTPFormAuthenticatorFactory { public static final AcmeOTPFormAuthenticator SINGLETON = new AcmeOTPFormAuthenticator(); @Override public Authenticator create(KeycloakSession session) { return SINGLETON; } @Override public String getId() { return AcmeOTPFormAuthenticator.ID; } @Override public String getDisplayType() { return "Acme: OTP Form"; } @Override public String getHelpText() { return "Validates a OTP on a separate OTP form."; } @Override public List getConfigProperties() { return null; } @Override public String getReferenceCategory() { return OTPCredentialModel.TYPE; } @Override public boolean isUserSetupAllowed() { return true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/setup/SelectMfaMethodAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.setup; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory; import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory; import org.keycloak.credential.CredentialModel; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ServerInfoAwareProviderFactory; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @JBossLog public class SelectMfaMethodAuthenticator implements Authenticator { private static final Set DEFAULT_MFA_CREDENTIAL_TYPES = new LinkedHashSet<>(List.of(WebAuthnCredentialModel.TYPE_TWOFACTOR, OTPCredentialModel.TYPE)); private static final Map DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP = Map.ofEntries(Map.entry(WebAuthnCredentialModel.TYPE_TWOFACTOR, WebAuthnRegisterFactory.PROVIDER_ID), Map.entry(OTPCredentialModel.TYPE, UserModel.RequiredAction.CONFIGURE_TOTP.name())); private static final SelectMfaMethodAuthenticator INSTANCE = new SelectMfaMethodAuthenticator(); public static final String MFA_CREDENTIAL_TYPES_KEY = "mfaCredentialTypes"; public static final String MFA_CREDENTIAL_TYPES_REQUIRED_ACTION_MAP_KEY = "mfaCredentialTypesRequiredActionMap"; @Override public void authenticate(AuthenticationFlowContext context) { Set mfaCredentialTypes = getConfiguredMfaCredentialTypes(context); // check if user has a MFA credential //context.getUser().credentialManager().getStoredCredentialsByTypeStream() if (isMfaCredentialConfiguredForCurrentUser(context.getUser(), mfaCredentialTypes)) { context.success(); return; } // compute available mfa methods // generate form LoginFormsProvider form = context.form(); form.setAttribute("mfaMethods", mfaCredentialTypes); Response selectMfaResponse = form.createForm("login-select-mfa-method.ftl"); context.forceChallenge(selectMfaResponse); } @Override public void action(AuthenticationFlowContext context) { // process mfa selection // issue proper required action to configure mfa MultivaluedMap formParams = context.getHttpRequest().getDecodedFormParameters(); // TODO handle invalid mfa method... String mfaMethod = formParams.getFirst("mfaMethod"); Map mapping = getConfiguredMfaCredentialTypesRequiredActionsMapping(context); String requiredActionId = mapping.get(mfaMethod); // TODO handle invalid providerid context.getAuthenticationSession().addRequiredAction(requiredActionId); context.success(); } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return false; } boolean isMfaCredentialConfiguredForCurrentUser(UserModel user, Set mfaCredentialTypes) { return user.credentialManager().getStoredCredentialsStream() // .map(CredentialModel::getType) // .anyMatch(mfaCredentialTypes::contains); } private static Set getConfiguredMfaCredentialTypes(AuthenticationFlowContext context) { AuthenticatorConfigModel configModel = context.getAuthenticatorConfig(); if (configModel == null) { return DEFAULT_MFA_CREDENTIAL_TYPES; } Map config = configModel.getConfig(); if (config == null) { return DEFAULT_MFA_CREDENTIAL_TYPES; } return Stream.of(config.get(MFA_CREDENTIAL_TYPES_KEY).split(",")).map(String::strip).collect(Collectors.toCollection(LinkedHashSet::new)); } private static Map getConfiguredMfaCredentialTypesRequiredActionsMapping(AuthenticationFlowContext context) { AuthenticatorConfigModel configModel = context.getAuthenticatorConfig(); if (configModel == null) { return DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP; } Map config = configModel.getConfig(); if (config == null) { return DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP; } return stringToMap((String) config.get(MFA_CREDENTIAL_TYPES_REQUIRED_ACTION_MAP_KEY)); } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } private static String mapToString(Map map) { var list = new ArrayList(); for (var entry : map.entrySet()) { list.add(entry.getKey().trim() + ":" + entry.getValue().trim()); } return String.join(",", list); } private static Map stringToMap(String string) { var items = string.split(","); Map map = new LinkedHashMap<>(); for (var item : items) { String[] keyValue = item.split(":"); map.put(keyValue[0], keyValue[1]); } return map; } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory { @Override public String getId() { return "acme-auth-select-mfa"; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public String getDisplayType() { return "Acme: Select MFA Method"; } @Override public String getHelpText() { return "Prompts the user to select an MFA Method"; } @Override public String getReferenceCategory() { return "mfa"; } @Override public boolean isConfigurable() { return true; } @Override public List getConfigProperties() { List properties = ProviderConfigurationBuilder.create() // .property() // .name(MFA_CREDENTIAL_TYPES_KEY) // .label("MFA Credential Types") // .helpText("Comma separated credential Types to treat as MFA credentials. Defaults to " + DEFAULT_MFA_CREDENTIAL_TYPES) // .type(ProviderConfigProperty.STRING_TYPE) // .defaultValue(String.join(",", DEFAULT_MFA_CREDENTIAL_TYPES)) // .add() .property() // .name(MFA_CREDENTIAL_TYPES_REQUIRED_ACTION_MAP_KEY) // .label("Required Action Mapping") // .helpText("Comma separated mapping of MFA Credential Types to their Required Action. Format: credentialType:requiredActionProviderId. Defaults to " + mapToString(DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP)) // .type(ProviderConfigProperty.STRING_TYPE) // .defaultValue(mapToString(DEFAULT_MFA_CREDENTIAL_TYPE_TO_REQUIRED_ACTION_MAP)) // .add() .build(); return properties; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public void postInit(KeycloakSessionFactory factory) { // called after factory is found } @Override public void init(Config.Scope config) { // spi-authenticator-acme-auth-hello-message // config.get("message"); // called when provider factory is used } @Override public void close() { } @Override public Map getOperationalInfo() { return Map.of("info", "infoValue"); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/PhoneNumberUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms; import java.util.Objects; public class PhoneNumberUtils { public static String abbreviatePhoneNumber(String phoneNumber) { Objects.requireNonNull(phoneNumber, "phoneNumber must not be null"); // +49178****123 if (phoneNumber.length() > 6) { // if only show the first 6 and last 3 digits of the phone number return phoneNumber.substring(0, 6) + "***" + phoneNumber.replaceAll(".*(\\d{3})$", "$1"); } return phoneNumber; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/SmsAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClientFactory; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials.SmsCredentialModel; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.ManageTrustedDeviceAction; import com.github.thomasdarimont.keycloak.custom.support.ConfigUtils; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.credential.CredentialModel; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.provider.ServerInfoAwareProviderFactory; import org.keycloak.representations.IDToken; import org.keycloak.sessions.AuthenticationSessionModel; import jakarta.ws.rs.core.Response; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @JBossLog public class SmsAuthenticator implements Authenticator { static final String TEMPLATE_LOGIN_SMS = "login-sms.ftl"; public static final int VERIFY_CODE_LENGTH = 6; public static final int CODE_TTL = 300; static final String CONFIG_CODE_LENGTH = "length"; static final String CONFIG_MAX_ATTEMPTS = "attempts"; static final String CONFIG_CODE_TTL = "ttl"; static final String CONFIG_SENDER = "sender"; static final String CONFIG_CLIENT = "client"; static final String CONFIG_PHONENUMBER_PATTERN = "phoneNumberPattern"; static final String CONFIG_USE_WEBOTP = "useWebOtp"; public static final String AUTH_NOTE_CODE = "smsCode"; static final String AUTH_NOTE_ATTEMPTS = "smsAttempts"; static final String ERROR_SMS_AUTH_INVALID_NUMBER = "smsAuthInvalidNumber"; static final String ERROR_SMS_AUTH_CODE_EXPIRED = "smsAuthCodeExpired"; static final String ERROR_SMS_AUTH_CODE_INVALID = "smsAuthCodeInvalid"; static final String ERROR_SMS_AUTH_SMS_NOT_SENT = "smsAuthSmsNotSent"; static final String ERROR_SMS_AUTH_ATTEMPTS_EXCEEDED = "smsAuthAttemptsExceeded"; @Override public void authenticate(AuthenticationFlowContext context) { if (context.getAuthenticationSession().getAuthNote(AUTH_NOTE_CODE) != null) { // avoid sending resending code on reload context.challenge(generateLoginForm(context, context.form()).createForm(TEMPLATE_LOGIN_SMS)); return; } UserModel user = context.getUser(); String phoneNumber = extractPhoneNumber(context.getSession(), context.getRealm(), user); AuthenticatorConfigModel authenticatorConfig = context.getAuthenticatorConfig(); boolean validPhoneNumberFormat = validatePhoneNumberFormat(phoneNumber, authenticatorConfig); if (!validPhoneNumberFormat) { context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, generateErrorForm(context, ERROR_SMS_AUTH_INVALID_NUMBER) .createErrorPage(Response.Status.INTERNAL_SERVER_ERROR)); return; } // TODO check for phoneNumberVerified sendCodeAndChallenge(context, user, phoneNumber, false); } protected String extractPhoneNumber(KeycloakSession session, RealmModel realm, UserModel user) { Optional maybeSmsCredential = user.credentialManager().getStoredCredentialsByTypeStream(SmsCredentialModel.TYPE).findFirst(); if (maybeSmsCredential.isEmpty()) { return null; } CredentialModel credentialModel = maybeSmsCredential.get(); SmsCredentialModel smsModel = new SmsCredentialModel(credentialModel); smsModel.readCredentialData(); String phoneNumber = smsModel.getPhoneNumber(); if (phoneNumber == null && Boolean.parseBoolean(user.getFirstAttribute(IDToken.PHONE_NUMBER_VERIFIED))) { // we use the verified phone-number from the user attributes as a fallback phoneNumber = user.getFirstAttribute(IDToken.PHONE_NUMBER); } return phoneNumber; } protected void sendCodeAndChallenge(AuthenticationFlowContext context, UserModel user, String phoneNumber, boolean resend) { log.infof("Sending code via SMS. resend=%s", resend); boolean codeSent = sendSmsWithCode(context, user, phoneNumber); if (!codeSent) { Response errorPage = generateErrorForm(context, null) .setError(ERROR_SMS_AUTH_SMS_NOT_SENT, "Sms Client") .createErrorPage(Response.Status.INTERNAL_SERVER_ERROR); context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, errorPage); return; } context.challenge(generateLoginForm(context, context.form()) .setAttribute("resend", resend) .setInfo("smsSentInfo", PhoneNumberUtils.abbreviatePhoneNumber(phoneNumber)) .createForm(TEMPLATE_LOGIN_SMS)); } protected LoginFormsProvider generateLoginForm(AuthenticationFlowContext context, LoginFormsProvider form) { return form.setAttribute("realm", context.getRealm()); } protected boolean sendSmsWithCode(AuthenticationFlowContext context, UserModel user, String phoneNumber) { AuthenticatorConfigModel configModel = context.getAuthenticatorConfig(); int length = Integer.parseInt(ConfigUtils.getConfigValue(configModel, CONFIG_CODE_LENGTH, "6")); int ttl = Integer.parseInt(ConfigUtils.getConfigValue(configModel, CONFIG_CODE_TTL, "300")); Map clientConfig = ConfigUtils.getConfig(configModel, Collections.singletonMap("client", SmsClientFactory.MOCK_SMS_CLIENT)); boolean useWebOtp = Boolean.parseBoolean(ConfigUtils.getConfigValue(configModel, CONFIG_USE_WEBOTP, "true")); AuthenticationSessionModel authSession = context.getAuthenticationSession(); KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); return createSmsCodeSender(context).sendVerificationCode(session, realm, user, phoneNumber, clientConfig, length, ttl, useWebOtp, authSession); } protected SmsCodeSender createSmsCodeSender(AuthenticationFlowContext context) { return new SmsCodeSender(); } protected boolean validatePhoneNumberFormat(String phoneNumber, AuthenticatorConfigModel configModel) { if (phoneNumber == null) { return false; } String pattern = ConfigUtils.getConfigValue(configModel, CONFIG_PHONENUMBER_PATTERN, ".*"); return phoneNumber.matches(pattern); } @Override public void action(AuthenticationFlowContext context) { var formParams = context.getHttpRequest().getDecodedFormParameters(); if (formParams.containsKey("resend")) { UserModel user = context.getUser(); String phoneNumber = extractPhoneNumber(context.getSession(), context.getRealm(), user); sendCodeAndChallenge(context, user, phoneNumber, true); return; } String codeInput = formParams.getFirst("code"); AuthenticationSessionModel authSession = context.getAuthenticationSession(); AuthenticatorConfigModel configModel = context.getAuthenticatorConfig(); int attempts = Integer.parseInt(Optional.ofNullable(authSession.getAuthNote(AUTH_NOTE_ATTEMPTS)).orElse("0")); int maxAttempts = Integer.parseInt(ConfigUtils.getConfigValue(configModel, CONFIG_MAX_ATTEMPTS, "5")); if (attempts >= maxAttempts) { log.info("To many invalid attempts."); Response errorPage = generateErrorForm(context, ERROR_SMS_AUTH_ATTEMPTS_EXCEEDED) .createErrorPage(Response.Status.BAD_REQUEST); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, errorPage); return; } String codeExpected = authSession.getAuthNote(AUTH_NOTE_CODE); String codeExpireAt = authSession.getAuthNote("codeExpireAt"); if (codeExpected == null || codeExpireAt == null) { context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, context.form().createErrorPage(Response.Status.INTERNAL_SERVER_ERROR)); return; } boolean valid = codeInput.equals(codeExpected); if (!valid) { Response errorPage = generateErrorForm(context, null) .setErrors(List.of(new FormMessage("code", ERROR_SMS_AUTH_CODE_INVALID))) .setAttribute("showResend", "") .createForm(TEMPLATE_LOGIN_SMS); handleFailure(context, AuthenticationFlowError.INVALID_CREDENTIALS, errorPage); return; } if (isCodeExpired(codeExpireAt)) { Response errorPage = generateErrorForm(context, null) .setErrors(List.of(new FormMessage("code", ERROR_SMS_AUTH_CODE_EXPIRED))) .setAttribute("showResend", "") .createErrorPage(Response.Status.BAD_REQUEST); handleFailure(context, AuthenticationFlowError.EXPIRED_CODE, errorPage); return; } if (formParams.containsKey("register-trusted-device")) { context.getUser().addRequiredAction(ManageTrustedDeviceAction.ID); } context.success(); } protected void handleFailure(AuthenticationFlowContext context, AuthenticationFlowError error, Response errorPage) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); int attempts = Integer.parseInt(Optional.ofNullable(authSession.getAuthNote(AUTH_NOTE_ATTEMPTS)).orElse("0")); attempts++; authSession.setAuthNote(AUTH_NOTE_ATTEMPTS, "" + attempts); context.failureChallenge(error, errorPage); } protected boolean isCodeExpired(String codeExpireAt) { return Long.parseLong(codeExpireAt) < System.currentTimeMillis(); } protected LoginFormsProvider generateErrorForm(AuthenticationFlowContext context, String error) { LoginFormsProvider form = context.form(); generateLoginForm(context, form); if (error != null) { form.setError(error); } return form; } @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { boolean configuredFor = user.credentialManager().isConfiguredFor(SmsCredentialModel.TYPE); // we only support 2FA with SMS for users with Phone Numbers return configuredFor && extractPhoneNumber(session, realm, user) != null; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory, ServerInfoAwareProviderFactory { public static final SmsAuthenticator INSTANCE = new SmsAuthenticator(); private static final List CONFIG_PROPERTIES; static { List list = ProviderConfigurationBuilder .create() .property().name(SmsAuthenticator.CONFIG_CODE_LENGTH) .type(ProviderConfigProperty.STRING_TYPE) .label("Code length") .defaultValue(VERIFY_CODE_LENGTH) .helpText("The length of the generated Code.") .add() .property().name(SmsAuthenticator.CONFIG_CODE_TTL) .type(ProviderConfigProperty.STRING_TYPE) .label("Time-to-live") .defaultValue(CODE_TTL) .helpText("The time to live in seconds for the code to be valid.") .add() .property().name(SmsAuthenticator.CONFIG_MAX_ATTEMPTS) .type(ProviderConfigProperty.STRING_TYPE) .label("Max Attempts") .defaultValue("5") .helpText("Max attempts for Code.") .add() .property().name(SmsAuthenticator.CONFIG_SENDER) .type(ProviderConfigProperty.STRING_TYPE) .label("Sender") .defaultValue("$realmDisplayName") .helpText("Denotes the message sender of the SMS. Defaults to $realmDisplayName") .add() .property().name(SmsAuthenticator.CONFIG_CLIENT) .type(ProviderConfigProperty.LIST_TYPE) .options(SmsClientFactory.MOCK_SMS_CLIENT) .label("Client") .defaultValue(SmsClientFactory.MOCK_SMS_CLIENT) .helpText("Denotes the client to send the SMS") .add() .property().name(SmsAuthenticator.CONFIG_PHONENUMBER_PATTERN) .type(ProviderConfigProperty.STRING_TYPE) .label("Phone Number Pattern") .defaultValue("(\\+49).*") .helpText("Regex Pattern for validation of Phone Numbers") .add() .property().name(SmsAuthenticator.CONFIG_USE_WEBOTP) .type(ProviderConfigProperty.BOOLEAN_TYPE) .label("Use Web OTP") .defaultValue(true) .helpText("Appends the Web OTP fragment '@domain #code' after a newline to the sms message.") .add() .build(); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } @Override public String getId() { return "acme-sms-authenticator"; } @Override public String getDisplayType() { return "Acme: SMS Authentication"; } @Override public String getHelpText() { return "Validates a code sent via SMS."; } @Override public String getReferenceCategory() { return SmsCredentialModel.TYPE; } @Override public boolean isConfigurable() { return true; } @Override public boolean isUserSetupAllowed() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } @Override public Map getOperationalInfo() { return Collections.singletonMap("availableClients", SmsClientFactory.getAvailableClientNames().toString()); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/SmsCodeSender.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClient; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClientFactory; import lombok.extern.jbosslog.JBossLog; import org.keycloak.common.util.SecretGenerator; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakUriInfo; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.theme.Theme; import org.keycloak.urls.UrlType; import java.util.Locale; import java.util.Map; @JBossLog public class SmsCodeSender { public boolean sendVerificationCode(KeycloakSession session, RealmModel realm, UserModel user, String phoneNumber, Map smsClientConfig, int codeLength, int codeTtl, boolean useWebOtp, AuthenticationSessionModel authSession) { String code = generateCode(codeLength); authSession.setAuthNote(SmsAuthenticator.AUTH_NOTE_CODE, code); authSession.setAuthNote("codeExpireAt", computeExpireAt(codeTtl)); KeycloakContext context = session.getContext(); String domain = resolveDomain(context); String sender = resolveSender(realm, smsClientConfig); try { Theme theme = session.theme().getTheme(Theme.Type.LOGIN); Locale locale = context.resolveLocale(user); String smsAuthText = theme.getMessages(locale).getProperty("smsAuthText"); String smsText = generateSmsText(codeTtl, code, smsAuthText, domain, useWebOtp); SmsClient smsClient = createSmsClient(smsClientConfig); smsClient.send(sender, phoneNumber, smsText); } catch (Exception e) { log.errorf(e, "Could not send sms"); return false; } return true; } protected String generateCode(int length) { return SecretGenerator.getInstance().randomString(length, SecretGenerator.DIGITS); } protected String resolveDomain(KeycloakContext context) { KeycloakUriInfo uri = context.getUri(UrlType.FRONTEND); return uri.getBaseUri().getHost(); } protected SmsClient createSmsClient(Map config) { String smsClientName = config.get(SmsAuthenticator.CONFIG_CLIENT); return SmsClientFactory.createClient(smsClientName, config); } protected String generateSmsText(int ttlSeconds, String code, String smsAuthText, String domain, boolean useWebOtp) { int ttlMinutes = Math.floorDiv(ttlSeconds, 60); String smsAuthMessage = String.format(smsAuthText, code, ttlMinutes); if (!useWebOtp) { return smsAuthMessage; } return appendWebOtpFragment(code, domain, smsAuthMessage); } protected String appendWebOtpFragment(String code, String domain, String smsAuthFragment) { String webOtpFragment = String.format("@%s #%s", domain, code); return smsAuthFragment + "\n\n" + webOtpFragment; } protected String computeExpireAt(int ttlSeconds) { return Long.toString(System.currentTimeMillis() + (ttlSeconds * 1000L)); } protected String resolveSender(RealmModel realm, Map clientConfig) { String sender = clientConfig.getOrDefault("sender", "keycloak"); if ("$realmDisplayName".equals(sender.trim())) { sender = realm.getDisplayName(); } return sender; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/client/SmsClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client; public interface SmsClient { void send(String sender, String receiver, String message); } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/client/SmsClientFactory.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.mock.MockSmsClient; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.Set; public class SmsClientFactory { public static final String MOCK_SMS_CLIENT = "mock"; public static SmsClient createClient(String name, Map config) { Objects.requireNonNull(name); switch (name) { case MOCK_SMS_CLIENT: return new MockSmsClient(config); default: throw new IllegalArgumentException("SMS Client " + name + " not supported."); } } public static Set getAvailableClientNames() { return new LinkedHashSet<>(Arrays.asList(MOCK_SMS_CLIENT)); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/client/mock/MockSmsClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.mock; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClient; import lombok.extern.jbosslog.JBossLog; import java.util.Map; @JBossLog public class MockSmsClient implements SmsClient { private final Map config; public MockSmsClient(Map config) { this.config = config; } @Override public void send(String sender, String receiver, String message) { log.infof("##### Sending SMS.%nsender='%s' phoneNumber='%s' message='%s'", sender, receiver, message); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/credentials/SmsCredentialModel.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials; import lombok.extern.jbosslog.JBossLog; import org.keycloak.credential.CredentialModel; import org.keycloak.representations.IDToken; import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.HashMap; import java.util.Map; @JBossLog public class SmsCredentialModel extends CredentialModel { public static final String TYPE = "mfa-sms"; private String phoneNumber; public SmsCredentialModel() { this(null); } public SmsCredentialModel(CredentialModel credentialModel) { setType(TYPE); if (credentialModel != null) { this.setId(credentialModel.getId()); this.setCreatedDate(credentialModel.getCreatedDate()); this.setCredentialData(credentialModel.getCredentialData()); this.setSecretData(credentialModel.getSecretData()); } } public String getPhoneNumber() { return phoneNumber; } public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } public void writeCredentialData() { Map credentialData = new HashMap<>(); credentialData.put(IDToken.PHONE_NUMBER, phoneNumber); try { setCredentialData(JsonSerialization.writeValueAsString(credentialData)); } catch (IOException e) { log.errorf(e, "Could not serialize SMS credentialData"); } } public void readCredentialData() { try { Map map = JsonSerialization.readValue(getCredentialData(), Map.class); setPhoneNumber((String) map.get(IDToken.PHONE_NUMBER)); } catch (IOException e) { log.errorf(e, "Could not deserialize SMS Credential data"); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/credentials/SmsCredentialProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.PhoneNumberUtils; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.updatephone.UpdatePhoneNumberRequiredAction; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.credential.CredentialTypeMetadataContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.IDToken; @JBossLog public class SmsCredentialProvider implements CredentialProvider, CredentialInputValidator { public static final String ID = "acme-mfa-sms"; private final KeycloakSession session; public SmsCredentialProvider(KeycloakSession session) { this.session = session; } @Override public boolean supportsCredentialType(String credentialType) { return SmsCredentialModel.TYPE.equals(credentialType); } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().isPresent(); } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { return false; } @Override public String getType() { return SmsCredentialModel.TYPE; } @Override public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) { if (!(credentialModel instanceof SmsCredentialModel)) { return null; } SmsCredentialModel model = (SmsCredentialModel) credentialModel; String phoneNumber = extractPhoneNumber(model, user); model.setType(SmsCredentialModel.TYPE); model.setCreatedDate(Time.currentTimeMillis()); model.setUserLabel("SMS @ " + PhoneNumberUtils.abbreviatePhoneNumber(phoneNumber)); model.writeCredentialData(); user.credentialManager().createStoredCredential(model); return model; } private String extractPhoneNumber(SmsCredentialModel model, UserModel user) { if (model.getPhoneNumber() != null) { return model.getPhoneNumber(); } return user.getFirstAttribute(IDToken.PHONE_NUMBER); } @Override public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) { return user.credentialManager().removeStoredCredentialById(credentialId); } @Override public CredentialModel getCredentialFromModel(CredentialModel model) { if (!getType().equals(model.getType())) { return null; } return model; } @Override public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) { CredentialTypeMetadata.CredentialTypeMetadataBuilder builder = CredentialTypeMetadata.builder(); builder.type(getType()); builder.category(CredentialTypeMetadata.Category.TWO_FACTOR); builder.createAction(UpdatePhoneNumberRequiredAction.ID); builder.removeable(true); builder.displayName("mfa-sms-display-name"); builder.helpText("mfa-sms-help-text"); // builder.updateAction(GenerateBackupCodeAction.ID); // TODO configure proper FA icon for sms auth builder.iconCssClass("kcAuthenticatorMfaSmsClass"); return builder.build(session); } @SuppressWarnings("rawtypes") @AutoService(CredentialProviderFactory.class) public static class Factory implements CredentialProviderFactory { @Override public CredentialProvider create(KeycloakSession session) { return new SmsCredentialProvider(session); } @Override public String getId() { return SmsCredentialProvider.ID; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/mfa/sms/updatephone/UpdatePhoneNumberRequiredAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.updatephone; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.account.MfaChange; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.PhoneNumberUtils; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.SmsAuthenticator; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.SmsCodeSender; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.client.SmsClientFactory; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials.SmsCredentialModel; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.sessions.AuthenticationSessionModel; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import java.util.List; import java.util.Map; import java.util.function.Consumer; @JBossLog public class UpdatePhoneNumberRequiredAction implements RequiredActionProvider { public static final String ID = "acme-update-phonenumber"; private static final String PHONE_NUMBER_FIELD = "mobile"; private static final String PHONE_NUMBER_ATTRIBUTE = "phoneNumber"; private static final String PHONE_NUMBER_VERIFIED_ATTRIBUTE = "phoneNumberVerified"; private static final String PHONE_NUMBER_AUTH_NOTE = ID + "-number"; private static final String FORM_ACTION_UPDATE = "update"; private static final String FORM_ACTION_VERIFY = "verify"; @Override public InitiatedActionSupport initiatedActionSupport() { // whether we can refer to that action via kc_actions URL parameter return InitiatedActionSupport.SUPPORTED; } @Override public void evaluateTriggers(RequiredActionContext context) { // check whether we need to show the update custom info form. AuthenticationSessionModel authSession = context.getAuthenticationSession(); if (!ID.equals(authSession.getClientNotes().get(Constants.KC_ACTION))) { // only show update form if we explicitly asked for the required action execution return; } UserModel user = context.getUser(); if (user.getFirstAttribute(PHONE_NUMBER_ATTRIBUTE) == null) { user.addRequiredAction(ID); } } @Override public void requiredActionChallenge(RequiredActionContext context) { // Show form context.challenge(createForm(context, null)); } protected Response createForm(RequiredActionContext context, Consumer formCustomizer) { LoginFormsProvider form = context.form(); UserModel user = context.getUser(); form.setAttribute("username", user.getUsername()); String phoneNumber = user.getFirstAttribute(PHONE_NUMBER_ATTRIBUTE); form.setAttribute("currentMobile", phoneNumber == null ? "" : phoneNumber); if (formCustomizer != null) { formCustomizer.accept(form); } AuthenticationSessionModel authSession = context.getAuthenticationSession(); if (authSession.getAuthNote(PHONE_NUMBER_AUTH_NOTE) != null) { // we are already sent a code return form.createForm("update-phone-number-form.ftl"); } // use form from src/main/resources/theme-resources/templates/ return form.createForm("update-phone-number-form.ftl"); } @Override public void processAction(RequiredActionContext context) { // TODO trigger phone number verification via SMS // user submitted the form MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); EventBuilder event = context.getEvent(); AuthenticationSessionModel authSession = context.getAuthenticationSession(); RealmModel realm = context.getRealm(); UserModel user = context.getUser(); KeycloakSession session = context.getSession(); event.event(EventType.UPDATE_PROFILE); String phoneNumber = formData.getFirst(PHONE_NUMBER_FIELD); EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PROFILE_ERROR).client(authSession.getClient()).user(authSession.getAuthenticatedUser()); if (formData.getFirst(FORM_ACTION_UPDATE) != null) { if (!isValidPhoneNumber(phoneNumber)) { Response challenge = createForm(context, form -> { form.addError(new FormMessage(PHONE_NUMBER_FIELD, "Invalid Input")); }); context.challenge(challenge); errorEvent.error(Errors.INVALID_INPUT); return; } LoginFormsProvider form = context.form(); form.setAttribute("currentMobile", phoneNumber); boolean useWebOtp = true; boolean result = createSmsSender(context).sendVerificationCode(session, realm, user, phoneNumber, Map.of("client", SmsClientFactory.MOCK_SMS_CLIENT), SmsAuthenticator.VERIFY_CODE_LENGTH, SmsAuthenticator.CODE_TTL, useWebOtp, authSession); if (!result) { log.warnf("Failed to send sms message. realm=%s user=%s", realm.getName(), user.getId()); } authSession.setAuthNote(PHONE_NUMBER_AUTH_NOTE, phoneNumber); form.setInfo("smsSentInfo", phoneNumber); context.challenge(form.createForm("verify-phone-number-form.ftl")); return; } if (formData.getFirst(FORM_ACTION_VERIFY) != null) { String phoneNumberFromAuthNote = authSession.getAuthNote(PHONE_NUMBER_AUTH_NOTE); String expectedCode = context.getAuthenticationSession().getAuthNote(SmsAuthenticator.AUTH_NOTE_CODE); // TODO check max failed attempts String actualCode = formData.getFirst("code"); if (!expectedCode.equals(actualCode)) { LoginFormsProvider form = context.form(); form.setAttribute("currentMobile", phoneNumberFromAuthNote); form.setErrors(List.of(new FormMessage("code", "error-invalid-code"))); context.challenge(form.createForm("verify-phone-number-form.ftl")); return; } user.setSingleAttribute(PHONE_NUMBER_ATTRIBUTE, phoneNumberFromAuthNote); user.setSingleAttribute(PHONE_NUMBER_VERIFIED_ATTRIBUTE, "true"); user.removeRequiredAction(ID); afterPhoneNumberVerified(realm, user, session, phoneNumberFromAuthNote); context.success(); return; } context.failure(); } protected void afterPhoneNumberVerified(RealmModel realm, UserModel user, KeycloakSession session, String phoneNumberFromAuthNote) { // TODO split this up into a separate required action, e.g. UpdateMfaSmsCodeRequiredAction updateSmsMfaCredential(realm, user, session, phoneNumberFromAuthNote); } protected void updateSmsMfaCredential(RealmModel realm, UserModel user, KeycloakSession session, String phoneNumber) { var credentialManager = user.credentialManager(); credentialManager.getStoredCredentialsByTypeStream(SmsCredentialModel.TYPE).forEach(cm -> credentialManager.removeStoredCredentialById(cm.getId())); SmsCredentialModel model = new SmsCredentialModel(); model.setPhoneNumber(phoneNumber); model.setType(SmsCredentialModel.TYPE); model.setCreatedDate(Time.currentTimeMillis()); model.setUserLabel("SMS @ " + PhoneNumberUtils.abbreviatePhoneNumber(phoneNumber)); model.writeCredentialData(); var credential = user.credentialManager().createStoredCredential(model); if (credential != null) { AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.ADD); } } private static boolean isValidPhoneNumber(String phoneNumber) { if (phoneNumber == null) { return false; } String phone = phoneNumber.trim(); // TODO use libphonenumber to validate phone number here return phone.length() > 3; } protected SmsCodeSender createSmsSender(RequiredActionContext context) { return new SmsCodeSender(); } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private static final RequiredActionProvider INSTANCE = new UpdatePhoneNumberRequiredAction(); @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } @Override public String getId() { return UpdatePhoneNumberRequiredAction.ID; } @Override public String getDisplayText() { return "Acme: Update Mobile Phonenumber"; } @Override public boolean isOneTimeAction() { return true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/net/NetworkAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.net; import com.google.auto.service.AutoService; import com.google.common.annotations.VisibleForTesting; import io.netty.handler.ipfilter.IpFilterRuleType; import io.netty.handler.ipfilter.IpSubnetFilterRule; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.http.HttpRequest; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.services.messages.Messages; import java.net.InetSocketAddress; import java.util.Collections; import java.util.List; import java.util.Map; /** * {@link Authenticator} that can check the remote IP address of the incoming request against a list of allowed networks. *

* The list of allowed networks can be configured via the AuthenticatorConfig or via a client attribute. *

*

* This authenticator can be used in the following contexts *

    *
  • Browser Flow
  • *
  • Direct Grant Flow
  • *
*/ @JBossLog public class NetworkAuthenticator implements Authenticator { static final NetworkAuthenticator INSTANCE = new NetworkAuthenticator(); public static final String PROVIDER_ID = "acme-network-authenticator"; public static final String REMOTE_IP_HEADER_PROPERTY = "remoteIpHeader"; public static final String ALLOWED_NETWORKS_PROPERTY = "allowedNetworks"; public static final String X_FORWARDED_FOR = "X-Forwarded-For"; public static final String ACME_ALLOWED_NETWORKS_CLIENT_ATTRIBUTE = "acmeAllowedNetworks"; /** * Authenticates within Browser and Direct Grant flow authentication flows. * * @param context */ @Override public void authenticate(AuthenticationFlowContext context) { var realm = context.getRealm(); var authSession = context.getAuthenticationSession(); var client = authSession.getClient(); var allowedNetworks = resolveAllowedNetworks(context.getAuthenticatorConfig(), client); if (allowedNetworks == null) { // skip check since we don't have any network restrictions configured log.debugf("Skip check for source IP based on network. realm=%s, client=%s", // realm.getName(), client.getClientId()); context.success(); return; } var remoteIp = resolveRemoteIp( // context.getAuthenticatorConfig(), // context.getHttpRequest(), // context.getConnection().getRemoteAddr() // ); if (remoteIp == null) { context.attempted(); log.warnf("Could not determine remoteIp, step marked as attempted. realm=%s, client=%s", // realm.getName(), client.getClientId()); return; } var ipAllowed = isAccessAllowed(allowedNetworks, remoteIp, realm, client); if (ipAllowed) { log.debugf("Allowed source IP based on allowed networks. realm=%s, client=%s, IP=%s", // realm.getName(), client.getClientId(), remoteIp); context.success(); return; } log.debugf("Rejected source IP based on allowed networks. realm=%s, client=%s, IP=%s", // realm.getName(), client.getClientId(), remoteIp); var challengeResponse = errorResponse(context, Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_request", "Access denied", authSession.getAuthNote("auth_type")); context.failure(AuthenticationFlowError.ACCESS_DENIED, challengeResponse); } @VisibleForTesting boolean isAccessAllowed(String allowedNetworks, String remoteIp, RealmModel realm, ClientModel client) { var ipAllowed = false; for (String allowedNetwork : allowedNetworks.split(",")) { ipAllowed = isRemoteIpAllowed(allowedNetwork, remoteIp); if (ipAllowed) { log.debugf("Matched source IP based on allowed network. realm=%s, client=%s, IP=%s, network=%s", // realm.getName(), client.getClientId(), remoteIp, allowedNetwork); break; } else { log.tracef("Rejected source IP based on allowed network. realm=%s, client=%s, IP=%s, network=%s", // realm.getName(), client.getClientId(), remoteIp, allowedNetwork); } } return ipAllowed; } /** * Extracts the allowed networks as comma separated String from the AuthenticatorConfig or the client attribute. * * @param config * @param client * @return */ @VisibleForTesting String resolveAllowedNetworks(AuthenticatorConfigModel config, ClientModel client) { var allowedNetworks = getAllowedNetworksForClient(client); if (isAllowedNetworkConfigured(allowedNetworks)) { return allowedNetworks; } allowedNetworks = getAllowedNetworksForAuthenticator(config); if (isAllowedNetworkConfigured(allowedNetworks)) { return allowedNetworks; } return null; } public Response errorResponse(AuthenticationFlowContext flowContext, int status, String error, String errorDescription, String authType) { if ("code".equals(authType)) { // auth code implies browser flow, so we need to render a form here var form = flowContext.form().setExecution(flowContext.getExecution().getId()); form.setError(Messages.ACCESS_DENIED); return form.createErrorPage(Response.Status.FORBIDDEN); } // client authentication or direct grant flow OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription); return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build(); } private boolean isAllowedNetworkConfigured(String allowedNetworks) { return allowedNetworks != null && !allowedNetworks.isBlank(); } @VisibleForTesting private String getAllowedNetworksForAuthenticator(AuthenticatorConfigModel authenticatorConfig) { if (authenticatorConfig == null) { return null; } var config = authenticatorConfig.getConfig(); if (config == null) { return null; } return config.get(ALLOWED_NETWORKS_PROPERTY); } @VisibleForTesting String getAllowedNetworksForClient(ClientModel client) { return client.getAttribute(ACME_ALLOWED_NETWORKS_CLIENT_ATTRIBUTE); } @VisibleForTesting boolean isRemoteIpAllowed(String allowedNetwork, String remoteIp) { boolean allowed = false; if (allowedNetwork.contains("/")) { /* CIDR notation, e.g: 192.168.178.0/24 - Allow access from a subnet 192.168.178.10/32 - Allow access from a single IP */ var ipAndCidrRange = allowedNetwork.split("/"); var ip = ipAndCidrRange[0]; int cidrRange = Integer.parseInt(ipAndCidrRange[1]); var rule = new IpSubnetFilterRule(ip, cidrRange, IpFilterRuleType.ACCEPT); allowed = rule.matches(new InetSocketAddress(remoteIp, 1 /* unsed */)); } else { /* explicit IP addresses, e.g: 192.168.178.10 - Allow access from a single IP */ allowed = remoteIp.equals(allowedNetwork.trim()); } return allowed; } @VisibleForTesting String resolveRemoteIp(AuthenticatorConfigModel authenticatorConfig, HttpRequest httpRequest, String remoteAddress) { var remoteIpHeaderName = getRemoteIpHeaderName(authenticatorConfig); var httpHeaders = httpRequest.getHttpHeaders(); if (X_FORWARDED_FOR.equals(remoteIpHeaderName)) { // see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For // X-Forwarded-For: , , String xForwardedForHeaderValue = httpHeaders.getHeaderString(X_FORWARDED_FOR); if (xForwardedForHeaderValue != null) { String[] ipAddresses = xForwardedForHeaderValue.split(","); // take the first IP address return ipAddresses[0].trim(); } } // TODO add support for Standard Forwarded Header var remoteIpFromHeader = httpHeaders.getHeaderString(remoteIpHeaderName); if (remoteIpFromHeader != null) { return remoteIpFromHeader; } return remoteAddress; } @VisibleForTesting String getRemoteIpHeaderName(AuthenticatorConfigModel authenticatorConfig) { if (authenticatorConfig == null) { return X_FORWARDED_FOR; } Map config = authenticatorConfig.getConfig(); if (config == null) { return X_FORWARDED_FOR; } String remoteIpHeaderName = config.get(REMOTE_IP_HEADER_PROPERTY); if (remoteIpHeaderName == null || remoteIpHeaderName.isBlank()) { return X_FORWARDED_FOR; } return remoteIpHeaderName; } @Override public void action(AuthenticationFlowContext flowContext) { // NOOP } @Override public boolean requiresUser() { // no resolved user needed return false; } @Override public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { return false; } @Override public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { // NOOP } @Override public void close() { // NOOP } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { static final List CONFIG_PROPERTIES; static { var list = ProviderConfigurationBuilder.create() // .property().name(REMOTE_IP_HEADER_PROPERTY) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Remote IP Header") // .defaultValue(X_FORWARDED_FOR) // .helpText("Header which contains the actual remote IP of a user agent. If empty the remote address will be resolved from the TCP connection. If the headername is X-Forwarded-For the header value is split on ',' and the first values is used as the remote address.") // .add() // .property().name(ALLOWED_NETWORKS_PROPERTY) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Allowed networks") // .defaultValue(null) // .helpText("Comma separated list of allowed networks. This supports CIDR network ranges and single IP adresses. If left empty ALL networks are allowed. Configuration can be overriden via client attribute acmeAllowedNetworks. Examples: 192.168.178.0/24, 192.168.178.12/32, 192.168.178.13") // .add() // .build(); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } @Override public String getId() { return PROVIDER_ID; } @Override public String getDisplayType() { return "Acme: Network Authenticator"; } @Override public String getReferenceCategory() { return "network"; } @Override public String getHelpText() { return "Controls access by checking the network address of the incoming request."; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope scope) { } @Override public void postInit(KeycloakSessionFactory keycloakSessionFactory) { } @Override public boolean isConfigurable() { return true; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public void close() { } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaAccessResponse.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.opa; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen; import lombok.Data; import lombok.NoArgsConstructor; import java.util.HashMap; import java.util.Map; @Data @NoArgsConstructor public class OpaAccessResponse { private AuthZen.AccessResponse result; private Map additionalData; public OpaAccessResponse(AuthZen.AccessResponse result) { this.result = result; } @JsonIgnore public boolean isAllowed() { return result != null && result.decision(); } public String getHint() { if (result == null) { return null; } if (result.context() == null) { return null; } Object hint = result.context().get("hint"); if (!(hint instanceof String)) { return null; } return (String) hint; } @JsonAnySetter public void handleUnknownProperty(String key, Object value) { if (additionalData == null) { additionalData = new HashMap<>(); } this.additionalData.put(key, value); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.opa; import com.github.thomasdarimont.keycloak.custom.config.MapConfig; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.messages.Messages; import java.util.Collections; import java.util.List; @JBossLog public class OpaAuthenticator implements Authenticator { private final KeycloakSession session; private final OpaClient opaClient; public OpaAuthenticator(KeycloakSession session, OpaClient opaClient) { this.session = session; this.opaClient = opaClient; } @Override public void authenticate(AuthenticationFlowContext context) { var realm = context.getRealm(); var user = context.getUser(); var authSession = context.getAuthenticationSession(); var authenticatorConfig = context.getAuthenticatorConfig(); var config = authenticatorConfig != null ? authenticatorConfig.getConfig() : null; var access = opaClient.checkAccess(session, new MapConfig(config), realm, user, authSession.getClient(), OpaClient.OPA_ACTION_LOGIN); if (!access.isAllowed()) { var loginForm = session.getProvider(LoginFormsProvider.class); var hint = access.getHint(); if (hint == null) { hint = Messages.ACCESS_DENIED; } loginForm.setError(hint, user.getUsername()); context.failure(AuthenticationFlowError.ACCESS_DENIED, loginForm.createErrorPage(Response.Status.FORBIDDEN)); return; } context.success(); } @Override public void action(AuthenticationFlowContext context) { // NOOP } @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @AutoService(AuthenticatorFactory.class) public static class OpaAuthenticatorFactory implements AuthenticatorFactory { protected static final List CONFIG_PROPERTIES; static { var list = ProviderConfigurationBuilder.create() // .property().name(OpaClient.OPA_AUTHZ_URL) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Authz Server Policy URL") // .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) // .helpText("URL of OPA Authz Server Policy Resource") // .add() // .property().name(OpaClient.OPA_USER_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("User Attributes") // .defaultValue(null) // .helpText("Comma separated list of user attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_REALM_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Realm Attributes") // .defaultValue(null) // .helpText("Comma separated list of realm attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_CONTEXT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Context Attributes") // .defaultValue(null) // .helpText("Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress") // .add() // .property().name(OpaClient.OPA_REQUEST_HEADERS) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Request Headers") // .defaultValue(null) // .helpText("Comma separated list of request headers to send with authz requests.") // .add() // .property().name(OpaClient.OPA_CLIENT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Client Attributes") // .defaultValue(null) // .helpText("Comma separated list of client attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_REALM_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use realm roles") // .defaultValue("true") // .helpText("If enabled, realm roles will be sent with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_CLIENT_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use client roles") // .defaultValue("true") // .helpText("If enabled, client roles will be sent with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_GROUPS) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use groups") // .defaultValue("true") // .helpText("If enabled, group information will be sent with authz requests.") // .add() // .build(); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } protected OpaClient opaClient; public String getId() { return "acme-opa-authenticator"; } @Override public String getDisplayType() { return "Acme: OPA Authentication"; } @Override public String getReferenceCategory() { return "opa"; } @Override public String getHelpText() { return "Validates access based on an OPA policy."; } @Override public boolean isConfigurable() { return !CONFIG_PROPERTIES.isEmpty(); } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public Authenticator create(KeycloakSession session) { return new OpaAuthenticator(session, opaClient); } @Override public void init(Config.Scope config) { this.opaClient = new OpaClient(); } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaCheckAccessAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.opa; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.messages.Messages; import java.util.Collections; import java.util.List; /** * Required Action that evaluates an OPA Policy to check if access to target client is allowed for the current user. */ public class OpaCheckAccessAction implements RequiredActionProvider { public static final String ID = "acme-opa-check-access"; public static final String ACTION_ALREADY_EXECUTED_MARKER = ID; public static final String REALM_ATTRIBUTE_PREFIX = "acme_opa_chk_"; private final OpaClient opaClient; private static final List CONFIG_PROPERTIES; static { var list = ProviderConfigurationBuilder.create() // .property().name(OpaClient.OPA_AUTHZ_URL) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Authz Server Policy URL") // .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) // .helpText("URL of OPA Authz Server Policy Resource") // .add() // .property().name(OpaClient.OPA_USER_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("User Attributes") // .defaultValue(null) // .helpText("Comma separated list of user attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_REALM_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Realm Attributes") // .defaultValue(null) // .helpText("Comma separated list of realm attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_CONTEXT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Context Attributes") // .defaultValue(null) // .helpText("Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress") // .add() // .property().name(OpaClient.OPA_REQUEST_HEADERS) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Request Headers") // .defaultValue(null) // .helpText("Comma separated list of request headers to send with authz requests.") // .add() // .property().name(OpaClient.OPA_CLIENT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Client Attributes") // .defaultValue(null) // .helpText("Comma separated list of client attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_REALM_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use realm roles") // .defaultValue("true") // .helpText("If enabled, realm roles will be sent with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_CLIENT_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use client roles") // .defaultValue("true") // .helpText("If enabled, client roles will be sent with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_GROUPS) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use groups") // .defaultValue("true") // .helpText("If enabled, group information will be sent with authz requests.") // .add() // .build(); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } public OpaCheckAccessAction(OpaClient opaClient) { this.opaClient = opaClient; } @Override public void evaluateTriggers(RequiredActionContext context) { var authSession = context.getAuthenticationSession(); if (authSession.getAuthNote(ACTION_ALREADY_EXECUTED_MARKER) != null) { return; } authSession.setAuthNote(ACTION_ALREADY_EXECUTED_MARKER, "true"); authSession.addRequiredAction(ID); } @Override public void requiredActionChallenge(RequiredActionContext context) { var realm = context.getRealm(); var user = context.getUser(); var session = context.getSession(); var authSession = context.getAuthenticationSession(); var config = new RealmConfig(realm, REALM_ATTRIBUTE_PREFIX); // realm attributes are looked up with prefix var access = opaClient.checkAccess(session, config, realm, user, authSession.getClient(), OpaClient.OPA_ACTION_CHECK_ACCESS); if (access.isAllowed()) { context.success(); return; } // deny access var loginForm = session.getProvider(LoginFormsProvider.class); var hint = access.getHint(); if (hint == null) { hint = Messages.ACCESS_DENIED; } loginForm.setError(hint, user.getUsername()); context.challenge(loginForm.createErrorPage(Response.Status.FORBIDDEN)); return; } @Override public void processAction(RequiredActionContext context) { // NOOP } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private OpaClient opaClient; @Override public RequiredActionProvider create(KeycloakSession session) { return new OpaCheckAccessAction(opaClient); } @Override public void init(Config.Scope config) { // NOOP this.opaClient = new OpaClient(); } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public List getConfigMetadata() { return CONFIG_PROPERTIES; } @Override public void close() { // NOOP } @Override public String getId() { return OpaCheckAccessAction.ID; } @Override public String getDisplayText() { return "Acme: OPA Check Access"; } @Override public boolean isOneTimeAction() { return true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/opa/OpaClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.opa; import com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen; import com.github.thomasdarimont.keycloak.custom.config.ClientConfig; import com.github.thomasdarimont.keycloak.custom.config.ConfigAccessor; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import lombok.extern.jbosslog.JBossLog; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.util.CollectionUtil; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.RoleUtils; import org.keycloak.services.messages.Messages; import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @JBossLog public class OpaClient { private static final Pattern COMMA_PATTERN = Pattern.compile(","); public static final String OPA_ACTION_LOGIN = "login"; public static final String OPA_ACTION_CHECK_ACCESS = "check_access"; public static final String DEFAULT_OPA_AUTHZ_URL = "http://acme-opa:8181/v1/data/iam/keycloak/allow"; public static final String OPA_ACTION = "action"; public static final String OPA_DESCRIPTION = "description"; public static final String OPA_RESOURCE_TYPE = "resource_type"; public static final String OPA_RESOURCE_CLAIM_NAME = "resource_claim_name"; public static final String OPA_USE_REALM_ROLES = "useRealmRoles"; public static final String OPA_USE_CLIENT_ROLES = "useClientRoles"; public static final String OPA_USER_ATTRIBUTES = "userAttributes"; public static final String OPA_CONTEXT_ATTRIBUTES = "contextAttributes"; public static final String OPA_REALM_ATTRIBUTES = "realmAttributes"; public static final String OPA_CLIENT_ATTRIBUTES = "clientAttributes"; public static final String OPA_REQUEST_HEADERS = "requestHeaders"; public static final String OPA_USE_GROUPS = "useGroups"; public static final String OPA_AUTHZ_URL = "authzUrl"; public OpaAccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName) { var resource = createResource(config, realm, client); return checkAccess(session, config, realm, user, client, actionName, resource); } public OpaAccessResponse checkAccess(KeycloakSession session, ConfigAccessor config, RealmModel realm, UserModel user, ClientModel client, String actionName, AuthZen.Resource resource) { var subject = createSubject(config, user, client); var accessContext = createAccessContext(session, config, user); var action = new AuthZen.Action(actionName); var accessRequest = new AuthZen.AccessRequest(subject, action, resource, accessContext); try { log.infof("Sending OPA check access request. realm=%s user=%s client=%s actionName=%s resource=%s\n%s", // realm.getName(), user.getUsername(), client.getClientId(), actionName, resource, JsonSerialization.writeValueAsPrettyString(accessRequest)); } catch (IOException ioe) { log.warn("Failed to prepare check access request", ioe); } var authzUrl = config.getString(OPA_AUTHZ_URL, DEFAULT_OPA_AUTHZ_URL); var request = SimpleHttp.doPost(authzUrl, session); request.json(Map.of("input", accessRequest)); var accessResponse = fetchResponse(request); try { log.infof("Received OPA authorization response. realm=%s user=%s client=%s\n%s", // realm.getName(), user.getUsername(), client.getClientId(), JsonSerialization.writeValueAsPrettyString(accessResponse)); } catch (IOException ioe) { log.warn("Failed to process received check access response", ioe); } return accessResponse; } protected AuthZen.Subject createSubject(ConfigAccessor config, UserModel user, ClientModel client) { var username = user.getUsername(); var realmRoles = config.getBoolean(OPA_USE_REALM_ROLES, true) ? fetchRealmRoles(user) : null; var clientRoles = config.getBoolean(OPA_USE_CLIENT_ROLES, true) ? fetchClientRoles(user, client) : null; var userAttributes = config.isConfigured(OPA_USER_ATTRIBUTES, true) ? extractUserAttributes(user, config) : null; var groups = config.getBoolean(OPA_USE_GROUPS, true) ? fetchGroupNames(user) : null; var properties = new HashMap(); if (CollectionUtil.isNotEmpty(realmRoles)) { properties.put("realmRoles", realmRoles); } if (CollectionUtil.isNotEmpty(clientRoles)) { properties.put("clientRoles", clientRoles); } if (userAttributes != null && !userAttributes.isEmpty()) { properties.put("userAttributes", userAttributes); } if (CollectionUtil.isNotEmpty(groups)) { properties.put("groups", groups); } return new AuthZen.Subject("user", username, properties); } protected AuthZen.Resource createResource(ConfigAccessor config, RealmModel realm, ClientModel client) { var realmAttributes = config.isConfigured(OPA_REALM_ATTRIBUTES, false) ? extractRealmAttributes(realm, config) : null; var clientAttributes = config.isConfigured(OPA_CLIENT_ATTRIBUTES, false) ? extractClientAttributes(client, config) : null; var properties = new HashMap(); properties.put("realmAttributes", realmAttributes); properties.put("clientAttributes", clientAttributes); properties.put("clientId", client.getClientId()); return new AuthZen.Resource("realm", realm.getName(), properties); } protected Map createAccessContext(KeycloakSession session, ConfigAccessor config, UserModel user) { var contextAttributes = config.isConfigured(OPA_CONTEXT_ATTRIBUTES, false) ? extractContextAttributes(session, user, config) : null; var headers = config.isConfigured(OPA_REQUEST_HEADERS, false) ? extractRequestHeaders(session, config) : null; Map accessContext = new HashMap<>(); accessContext.put("contextAttributes", contextAttributes); accessContext.put("headers", headers); return accessContext; } protected Map extractRequestHeaders(KeycloakSession session, ConfigAccessor config) { var headerNames = config.getValue(OPA_REQUEST_HEADERS); if (headerNames == null || headerNames.isBlank()) { return null; } var requestHeaders = session.getContext().getRequestHeaders(); var headers = new HashMap(); for (String header : COMMA_PATTERN.split(headerNames.trim())) { var value = requestHeaders.getHeaderString(header); headers.put(header, value); } if (headers.isEmpty()) { return null; } return headers; } protected Map extractContextAttributes(KeycloakSession session, UserModel user, ConfigAccessor config) { var contextAttributes = extractAttributes(user, config, OPA_CONTEXT_ATTRIBUTES, (u, attr) -> { Object value = switch (attr) { case "remoteAddress" -> session.getContext().getConnection().getRemoteAddr(); default -> null; }; return value; }, u -> null); return contextAttributes; } protected Map extractAttributes(T source, ConfigAccessor config, String attributesKey, BiFunction valueExtractor, Function> defaultValuesExtractor) { if (config == null) { return defaultValuesExtractor.apply(source); } var requestedAttributes = config.getValue(attributesKey); if (requestedAttributes == null || requestedAttributes.isBlank()) { return defaultValuesExtractor.apply(source); } var attributes = new HashMap(); for (String attribute : COMMA_PATTERN.split(requestedAttributes.trim())) { Object value = valueExtractor.apply(source, attribute); attributes.put(attribute, value); } return attributes; } protected Map extractUserAttributes(UserModel user, ConfigAccessor config) { var userAttributes = extractAttributes(user, config, OPA_USER_ATTRIBUTES, (u, attr) -> { Object value = switch (attr) { case "id" -> user.getId(); case "email" -> user.getEmail(); case "createdTimestamp" -> user.getCreatedTimestamp(); case "lastName" -> user.getLastName(); case "firstName" -> user.getFirstName(); case "federationLink" -> user.getFederationLink(); case "serviceAccountLink" -> user.getServiceAccountClientLink(); default -> user.getFirstAttribute(attr); }; return value; }, this::extractDefaultUserAttributes); return userAttributes; } protected Map extractClientAttributes(ClientModel client, ConfigAccessor config) { var clientConfig = new ClientConfig(client); return extractAttributes(client, config, OPA_CLIENT_ATTRIBUTES, (c, attr) -> clientConfig.getValue(attr), c -> null); } protected Map extractRealmAttributes(RealmModel realm, ConfigAccessor config) { var realmConfig = new RealmConfig(realm); return extractAttributes(realm, config, OPA_REALM_ATTRIBUTES, (r, attr) -> realmConfig.getValue(attr), r -> null); } protected List fetchGroupNames(UserModel user) { return user.getGroupsStream().map(GroupModel::getName).collect(Collectors.toList()); } protected List fetchClientRoles(UserModel user, ClientModel client) { Stream explicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getClientRoleMappingsStream(client)); Stream implicitClientRoles = RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream()); return Stream.concat(explicitClientRoles, implicitClientRoles) // .filter(RoleModel::isClientRole) // .map(this::normalizeRoleName) // .collect(Collectors.toList()); } protected List fetchRealmRoles(UserModel user) { // Set xxx = RoleUtils.getDeepUserRoleMappings(user); return RoleUtils.expandCompositeRolesStream(user.getRealmRoleMappingsStream()) // .filter(r -> !r.isClientRole()).map(this::normalizeRoleName) // .collect(Collectors.toList()); } protected String normalizeRoleName(RoleModel role) { if (role.isClientRole()) { return ((ClientModel) role.getContainer()).getClientId() + ":" + role.getName(); } return role.getName(); } protected boolean getBoolean(Map config, String key, boolean defaultValue) { if (config == null) { return defaultValue; } return Boolean.parseBoolean(config.get(key)); } protected Map extractDefaultUserAttributes(UserModel user) { return Map.of("id", user.getId(), "email", user.getEmail()); } protected OpaAccessResponse fetchResponse(SimpleHttp request) { try { log.debugf("Fetching url=%s", request.getUrl()); try (var response = request.asResponse()) { return response.asJson(OpaAccessResponse.class); } } catch (IOException e) { log.error("OPA access request failed", e); return new OpaAccessResponse(new AuthZen.AccessResponse(false, Map.of("hint", Messages.ACCESS_DENIED))); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/passwordform/FederationAwarePasswordForm.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.passwordform; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.PasswordForm; import org.keycloak.authentication.authenticators.browser.PasswordFormFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** * Augments {@link PasswordForm} with additional handling of federated users. */ public class FederationAwarePasswordForm extends PasswordForm { public FederationAwarePasswordForm(KeycloakSession session) { super(session); } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { // TODO create keycloak issue for PasswordForm failing for federated users KEYCLOAK-XXX if (user.getFederationLink() != null) { // always allow password auth for federated users return true; } return super.configuredFor(session, realm, user); } @JBossLog @AutoService(AuthenticatorFactory.class) public static class Factory extends PasswordFormFactory { @Override public Authenticator create(KeycloakSession session) { return new FederationAwarePasswordForm(session); } @Override public void postInit(KeycloakSessionFactory factory) { log.info("Overriding custom Keycloak PasswordFormFactory"); super.postInit(factory); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/TrustedDeviceCookie.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice; import com.github.thomasdarimont.keycloak.custom.support.CookieUtils; import org.keycloak.http.HttpRequest; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import java.util.Optional; public class TrustedDeviceCookie { public static final String COOKIE_NAME = Optional.ofNullable(System.getenv("KEYCLOAK_AUTH_TRUSTED_DEVICE_COOKIE_NAME")).orElse("ACME_KEYCLOAK_DEVICE"); public static void removeDeviceCookie(KeycloakSession session, RealmModel realm) { // maxAge = 1 triggers legacy cookie removal CookieUtils.addCookie(COOKIE_NAME, "", session, realm, 1); } public static void addDeviceCookie(String deviceTokenString, int maxAge, KeycloakSession session, RealmModel realm) { CookieUtils.addCookie(COOKIE_NAME, deviceTokenString, session, realm, maxAge); } public static TrustedDeviceToken parseDeviceTokenFromCookie(HttpRequest httpRequest, KeycloakSession session) { String cookieValue = CookieUtils.parseCookie(COOKIE_NAME, httpRequest); if(cookieValue == null) { return null; } // decodes and validates device cookie return session.tokens().decode(cookieValue, TrustedDeviceToken.class); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/TrustedDeviceName.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.support.UserAgentParser; import org.keycloak.http.HttpRequest; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; import ua_parser.OS; import ua_parser.UserAgent; import jakarta.ws.rs.core.HttpHeaders; public class TrustedDeviceName { private static final PolicyFactory TEXT_ONLY_SANITIZATION_POLICY = new HtmlPolicyBuilder().toFactory(); public static String generateDeviceName(HttpRequest request) { String userAgentString = request.getHttpHeaders().getHeaderString(HttpHeaders.USER_AGENT); String deviceType = guessDeviceTypeFromUserAgentString(userAgentString); // TODO generate a better device name based on the user agent UserAgent userAgent = UserAgentParser.parseUserAgent(userAgentString); if (userAgent == null) { // user agent not parsable, return just device type as a fallback. return deviceType; } String osNamePart = guessOsFromUserAgentString(userAgentString); String browserFamily = userAgent.family; String generatedDeviceName = osNamePart + " - " + browserFamily + " " + deviceType; return sanitizeDeviceName(generatedDeviceName); } private static String guessOsFromUserAgentString(String userAgentString) { OS os = UserAgentParser.parseOperationSystem(userAgentString); if (os == null) { return "Computer"; } return os.family; } private static String guessDeviceTypeFromUserAgentString(String userAgentString) { // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop // best effort guess to detect mobile device type. if (userAgentString.contains("iPad")) { return "iPad"; } if (userAgentString.contains("iPhone")) { return "iPhone"; } if (userAgentString.contains("Mobi")) { return "Mobile Browser"; } return "Browser"; } public static String sanitizeDeviceName(String deviceNameInput) { String deviceName = deviceNameInput; if (deviceName == null || deviceName.isEmpty()) { deviceName = "Browser"; } else if (deviceName.length() > 32) { deviceName = deviceName.substring(0, 32); } deviceName = TEXT_ONLY_SANITIZATION_POLICY.sanitize(deviceName); deviceName = deviceName.trim(); return deviceName; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/TrustedDeviceToken.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice; import com.fasterxml.jackson.annotation.JsonProperty; import org.keycloak.TokenCategory; import org.keycloak.representations.JsonWebToken; public class TrustedDeviceToken extends JsonWebToken { @JsonProperty("device_id") private String deviceId; @Override public TokenCategory getCategory() { return TokenCategory.INTERNAL; } public String getDeviceId() { return deviceId; } public void setDeviceId(String deviceId) { this.deviceId = deviceId; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/action/ManageTrustedDeviceAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.account.MfaChange; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceName; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialModel; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialProvider; import com.github.thomasdarimont.keycloak.custom.support.RequiredActionUtils; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialProvider; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.http.HttpRequest; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.sessions.AuthenticationSessionModel; import jakarta.ws.rs.core.MultivaluedMap; import java.math.BigInteger; import java.security.SecureRandom; @JBossLog public class ManageTrustedDeviceAction implements RequiredActionProvider { public static final String ID = "acme-manage-trusted-device"; // TODO move to centralized configuration public static final int NUMBER_OF_DAYS_TO_TRUST_DEVICE = Integer.getInteger("keycloak.auth.trusteddevice.trustdays", 120); private static final boolean HEADLESS_TRUSTED_DEVICE_REGISTRATION_ENABLED = Boolean.parseBoolean(System.getProperty("keycloak.auth.trusteddevice.headless", "true")); @Override public InitiatedActionSupport initiatedActionSupport() { return InitiatedActionSupport.SUPPORTED; } @Override public void evaluateTriggers(RequiredActionContext context) { // NOOP } @Override public void requiredActionChallenge(RequiredActionContext context) { RealmModel realm = context.getRealm(); UserModel user = context.getUser(); if (HEADLESS_TRUSTED_DEVICE_REGISTRATION_ENABLED) { // derive trusted device from ser agent KeycloakSession session = context.getSession(); // automatically generated device name based on Browser and OS. String deviceName = TrustedDeviceName.generateDeviceName(context.getHttpRequest()); registerNewTrustedDevice(session, realm, user, deviceName, null); afterTrustedDeviceRegistration(context, new TrustedDeviceInfo(deviceName)); return; } String username = user.getUsername(); String deviceName = TrustedDeviceName.generateDeviceName(context.getHttpRequest()); LoginFormsProvider form = context.form(); form.setAttribute("username", username); form.setAttribute("device", deviceName); context.challenge(form.createForm("manage-trusted-device-form.ftl")); } @Override public void processAction(RequiredActionContext context) { if (RequiredActionUtils.isCancelApplicationInitiatedAction(context)) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); AuthenticationManager.setKcActionStatus(ManageTrustedDeviceAction.ID, RequiredActionContext.KcActionStatus.CANCELLED, authSession); context.success(); return; } KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); UserModel user = context.getUser(); HttpRequest httpRequest = context.getHttpRequest(); MultivaluedMap formParams = httpRequest.getDecodedFormParameters(); // register trusted device if (formParams.containsKey("remove-other-trusted-devices")) { log.info("Remove all trusted device registrations"); removeTrustedDevices(context); } var receivedTrustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(httpRequest, session); if (formParams.containsKey("dont-trust-device")) { log.info("Remove trusted device registration"); TrustedDeviceCredentialModel trustedDeviceModel = TrustedDeviceCredentialModel.lookupTrustedDevice(user, receivedTrustedDeviceToken); if (trustedDeviceModel != null) { boolean deleted = session.getProvider(CredentialProvider.class, TrustedDeviceCredentialProvider.ID).deleteCredential(realm, user, trustedDeviceModel.getId()); if (deleted) { AccountActivity.onTrustedDeviceChange(session, realm, user, new TrustedDeviceInfo(trustedDeviceModel.getUserLabel()), MfaChange.REMOVE); } } } if (formParams.containsKey("trust-device")) { String deviceName = TrustedDeviceName.sanitizeDeviceName(formParams.getFirst("device")); registerNewTrustedDevice(session, realm, user, deviceName, receivedTrustedDeviceToken); afterTrustedDeviceRegistration(context, new TrustedDeviceInfo(deviceName)); } // remove required action if present context.getUser().removeRequiredAction(ID); context.success(); } private void afterTrustedDeviceRegistration(RequiredActionContext context, TrustedDeviceInfo trustedDeviceInfo) { // remove required action if present context.getUser().removeRequiredAction(ID); context.success(); EventBuilder event = context.getEvent(); event.event(EventType.CUSTOM_REQUIRED_ACTION); event.detail("action_id", ID); event.detail("register_trusted_device", "true"); event.success(); AccountActivity.onTrustedDeviceChange(context.getSession(), context.getRealm(), context.getUser(), trustedDeviceInfo, MfaChange.ADD); } private void registerNewTrustedDevice(KeycloakSession session, RealmModel realm, UserModel user, String deviceName, TrustedDeviceToken receivedTrustedDeviceToken) { TrustedDeviceCredentialModel currentTrustedDevice = TrustedDeviceCredentialModel.lookupTrustedDevice(user, receivedTrustedDeviceToken); if (currentTrustedDevice == null) { log.info("Register new trusted device"); } else { log.info("Update existing trusted device"); } int numberOfDaysToTrustDevice = NUMBER_OF_DAYS_TO_TRUST_DEVICE; //FIXME make name of days to remember deviceToken configurable String deviceId = currentTrustedDevice == null ? null : currentTrustedDevice.getDeviceId(); TrustedDeviceToken newTrustedDeviceToken = createDeviceToken(deviceId, numberOfDaysToTrustDevice); if (currentTrustedDevice == null) { var tdcm = new TrustedDeviceCredentialModel(null, deviceName, newTrustedDeviceToken.getDeviceId()); var cp = session.getProvider(CredentialProvider.class, TrustedDeviceCredentialProvider.ID); cp.createCredential(realm, user, tdcm); } else { // update label name for existing device user.credentialManager().updateCredentialLabel(currentTrustedDevice.getId(), deviceName); } String deviceTokenString = session.tokens().encode(newTrustedDeviceToken); int maxAge = numberOfDaysToTrustDevice * 24 * 60 * 60; TrustedDeviceCookie.addDeviceCookie(deviceTokenString, maxAge, session, realm); log.info("Registered trusted device"); } private void removeTrustedDevices(RequiredActionContext context) { var user = context.getUser(); var scm = user.credentialManager(); scm.getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE).forEach(cm -> scm.removeStoredCredentialById(cm.getId())); } protected TrustedDeviceToken createDeviceToken(String deviceId, int numberOfDaysToTrustDevice) { // TODO enhance generated device id with information from httpRequest, e.g. browser fingerprint String currentDeviceId = deviceId; // generate a unique but short device id if (currentDeviceId == null) { currentDeviceId = BigInteger.valueOf(new SecureRandom().nextLong()).toString(36); } TrustedDeviceToken trustedDeviceToken = new TrustedDeviceToken(); long iat = Time.currentTime(); long exp = iat + (long) numberOfDaysToTrustDevice * 24 * 60 * 60; trustedDeviceToken.iat(iat); trustedDeviceToken.exp(exp); trustedDeviceToken.setDeviceId(currentDeviceId); return trustedDeviceToken; } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { public static final ManageTrustedDeviceAction INSTANCE = new ManageTrustedDeviceAction(); @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } @Override public String getId() { return ManageTrustedDeviceAction.ID; } @Override public String getDisplayText() { return "Acme: Manage Trusted Device"; } @Override public boolean isOneTimeAction() { return true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/action/TrustedDeviceInfo.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action; public class TrustedDeviceInfo { private final String deviceName; public TrustedDeviceInfo(String deviceName) { this.deviceName = deviceName; } public String getDeviceName() { return deviceName; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/auth/TrustedDeviceAuthenticator.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.auth; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialModel; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialProvider; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.CredentialValidator; import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialProvider; import org.keycloak.http.HttpRequest; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.Collections; import java.util.List; @JBossLog public class TrustedDeviceAuthenticator implements Authenticator, CredentialValidator { static final String ID = "acme-auth-trusted-device"; public static TrustedDeviceCredentialModel lookupTrustedDeviceCredentialModelFromCookie(KeycloakSession session, RealmModel realm, UserModel user, HttpRequest httpRequest) { if (user == null) { return null; } var trustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(httpRequest, session); if (trustedDeviceToken == null) { return null; } if (Time.currentTime() >= trustedDeviceToken.getExp()) { // token expired return null; } var credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE).filter(cm -> cm.getSecretData().equals(trustedDeviceToken.getDeviceId())).findAny().orElse(null); if (credentialModel == null) { return null; } return new TrustedDeviceCredentialModel(credentialModel.getId(), credentialModel.getUserLabel(), credentialModel.getSecretData()); } @Override public void authenticate(AuthenticationFlowContext context) { var trustedDeviceCredentialModel = lookupTrustedDeviceCredentialModelFromCookie( // context.getSession(), // context.getRealm(), // context.getAuthenticationSession().getAuthenticatedUser(), // context.getHttpRequest() // ); if (trustedDeviceCredentialModel == null) { log.info("Unknown device detected!"); context.attempted(); return; } log.info("Found trusted device."); context.getEvent().detail("trusted_device", "true"); context.getEvent().detail("trusted_device_id", trustedDeviceCredentialModel.getDeviceId()); context.success(); } @Override public void action(AuthenticationFlowContext context) { // NOOP } @Override public boolean requiresUser() { return true; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return user.credentialManager().isConfiguredFor(TrustedDeviceCredentialModel.TYPE); } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @Override public TrustedDeviceCredentialProvider getCredentialProvider(KeycloakSession session) { return (TrustedDeviceCredentialProvider) session.getProvider(CredentialProvider.class, TrustedDeviceCredentialProvider.ID); } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { private static final TrustedDeviceAuthenticator INSTANCE = new TrustedDeviceAuthenticator(); @Override public String getId() { return TrustedDeviceAuthenticator.ID; } @Override public String getDisplayType() { return "Acme: Trusted Device Authenticator"; } @Override public String getHelpText() { return "Trusted Device to suppress MFA"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public String getReferenceCategory() { return TrustedDeviceCredentialModel.TYPE; } @Override public List getConfigProperties() { return Collections.emptyList(); } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/credentials/TrustedDeviceCredentialInput.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials; import org.keycloak.credential.CredentialInput; public class TrustedDeviceCredentialInput implements CredentialInput { private final String credentialId; private final String type; private final String challengeResponse; public TrustedDeviceCredentialInput(String credentialId, String type, String challengeResponse) { this.credentialId = credentialId; this.type = type; this.challengeResponse = challengeResponse; } @Override public String getCredentialId() { return credentialId; } @Override public String getType() { return type; } @Override public String getChallengeResponse() { return challengeResponse; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/credentials/TrustedDeviceCredentialModel.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken; import org.keycloak.credential.CredentialModel; import org.keycloak.models.UserModel; public class TrustedDeviceCredentialModel extends CredentialModel { public static final String TYPE = "acme-trusted-device"; private TrustedDeviceToken trustedDeviceToken; private String deviceId; public TrustedDeviceCredentialModel(String deviceName, TrustedDeviceToken trustedDeviceToken) { this.setUserLabel(deviceName); this.trustedDeviceToken = trustedDeviceToken; } public TrustedDeviceCredentialModel(String id, String deviceName, String deviceId) { this.setId(id); this.setUserLabel(deviceName); this.deviceId = deviceId; } @Override public String getType() { return TYPE; } public TrustedDeviceToken getDeviceToken() { return trustedDeviceToken; } public String getDeviceId() { return deviceId; } public static TrustedDeviceCredentialModel lookupTrustedDevice(UserModel user, TrustedDeviceToken trustedDeviceToken) { if (user == null) { return null; } if (trustedDeviceToken == null) { return null; } var credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE) .filter(cm -> cm.getSecretData().equals(trustedDeviceToken.getDeviceId())) .findAny().orElse(null); if (credentialModel == null) { return null; } return new TrustedDeviceCredentialModel(credentialModel.getId(), credentialModel.getUserLabel(), credentialModel.getSecretData()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/credentials/TrustedDeviceCredentialProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.ManageTrustedDeviceAction; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.credential.CredentialTypeMetadataContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.utils.KeycloakSessionUtil; @JBossLog public class TrustedDeviceCredentialProvider implements CredentialProvider, CredentialInputValidator { public static final String ID = "custom-trusted-device"; private final KeycloakSession session; public TrustedDeviceCredentialProvider(KeycloakSession session) { this.session = session; } @Override public String getType() { return TrustedDeviceCredentialModel.TYPE; } @Override public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) { CredentialModel trustedDeviceCredentialModel = createTrustedDeviceCredentialModel((TrustedDeviceCredentialModel) credentialModel); var cm = user.credentialManager(); var storedCredential = cm.createStoredCredential(trustedDeviceCredentialModel); // The execution order of the credential backed authenticators is controlled by the order of the stored credentials // not only by the order of the authenticator. There fore, we need to move the new device-credential right after the password credential. cm.getStoredCredentialsByTypeStream(PasswordCredentialModel.TYPE) .findFirst() .ifPresent(passwordModel -> cm.moveStoredCredentialTo(storedCredential.getId(), passwordModel.getId())); return trustedDeviceCredentialModel; } protected CredentialModel createTrustedDeviceCredentialModel(TrustedDeviceCredentialModel trustedDeviceCredentialModel) { CredentialModel model = new CredentialModel(); model.setType(getType()); model.setCreatedDate(Time.currentTimeMillis()); // TODO make userlabel configurable model.setUserLabel(trustedDeviceCredentialModel.getUserLabel()); model.setSecretData(trustedDeviceCredentialModel.getDeviceId()); model.setCredentialData(null); return model; } @Override public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) { var cm = user.credentialManager(); var credentialModel = cm.getStoredCredentialById(credentialId); boolean deleted = deleteMatchingDeviceCookieIfPresent(realm, credentialModel); if (deleted) { log.infof("Removed trusted device cookie for user. realm=%s userId=%s", realm.getName(), user.getId()); } return cm.removeStoredCredentialById(credentialId); } /** * Try to delete device cookie if present * * @param realm * @param credentialModel * @return */ private boolean deleteMatchingDeviceCookieIfPresent(RealmModel realm, CredentialModel credentialModel) { var httpRequest = KeycloakSessionUtil.getKeycloakSession().getContext().getHttpRequest(); if (httpRequest == null) { return false; } TrustedDeviceToken trustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(httpRequest, session); if (trustedDeviceToken == null || !trustedDeviceToken.getDeviceId().equals(credentialModel.getSecretData())) { return false; } // request comes from browser with device cookie that needs to be deleted TrustedDeviceCookie.removeDeviceCookie(session, realm); return true; } @Override public CredentialModel getCredentialFromModel(CredentialModel model) { if (!getType().equals(model.getType())) { return null; } return model; } @Override public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) { var builder = CredentialTypeMetadata.builder(); builder.type(getType()); builder.category(CredentialTypeMetadata.Category.TWO_FACTOR); // TODO make backup code removal configurable builder.removeable(true); builder.displayName("trusted-device-display-name"); builder.helpText("trusted-device-help-text"); // Note, that we can only have either a create or update action builder.updateAction(ManageTrustedDeviceAction.ID); // we use the update action to remove or "untrust" a device. // builder.createAction(ManageTrustedDeviceAction.ID); // TODO configure proper FA icon for backup codes builder.iconCssClass("kcAuthenticatorTrustedDeviceClass"); return builder.build(session); } @Override public boolean supportsCredentialType(String credentialType) { return TrustedDeviceCredentialModel.TYPE.equals(credentialType); } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().orElse(null) != null; } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { if (!(credentialInput instanceof TrustedDeviceCredentialInput)) { return false; } var tdci = (TrustedDeviceCredentialInput) credentialInput; var deviceId = tdci.getChallengeResponse(); var credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(TrustedDeviceCredentialModel.TYPE) .filter(cm -> cm.getSecretData().equals(deviceId)) .findAny().orElse(null); return credentialModel != null; } @SuppressWarnings("rawtypes") @AutoService(CredentialProviderFactory.class) public static class Factory implements CredentialProviderFactory { @Override public CredentialProvider create(KeycloakSession session) { return new TrustedDeviceCredentialProvider(session); } @Override public String getId() { return TrustedDeviceCredentialProvider.ID; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/trusteddevice/support/UserAgentParser.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.support; import lombok.extern.jbosslog.JBossLog; import ua_parser.OS; import ua_parser.Parser; import ua_parser.UserAgent; @JBossLog public class UserAgentParser { private static final Parser USER_AGENT_PARSER; static { Parser parser = null; try { parser = new Parser(); } catch (Exception e) { log.errorf(e, "Could not initialize user_agent parser"); } USER_AGENT_PARSER = parser; } public static UserAgent parseUserAgent(String userAgentString) { if (USER_AGENT_PARSER == null) { return null; } return USER_AGENT_PARSER.parseUserAgent(userAgentString); } public static OS parseOperationSystem(String userAgentString) { if (USER_AGENT_PARSER == null) { return null; } return USER_AGENT_PARSER.parseOS(userAgentString); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/userpasswordform/AcmeCaptchaUsernamePasswordForm.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.userpasswordform; import com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha.FriendlyCaptcha; import com.github.thomasdarimont.keycloak.custom.support.LocaleUtils; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; /** * UsernamePasswordForm with a friendlycaptcha */ public class AcmeCaptchaUsernamePasswordForm extends UsernamePasswordForm { public static final String ID = "acme-captcha-username-password-form"; public static final String FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE = "captchaTriggered"; public static final String FRIENDLY_CAPTCHA_CHECK_SOLVED_AUTH_NOTE = "captchaSolved"; @Override protected Response challenge(AuthenticationFlowContext context, MultivaluedMap formData) { addCaptcha(context); return super.challenge(context, formData); } @Override protected Response challenge(AuthenticationFlowContext context, String error, String field) { addCaptcha(context); return super.challenge(context, error, field); } @Override protected Response challenge(AuthenticationFlowContext context, String error) { addCaptcha(context); return super.challenge(context, error); } private void addCaptcha(AuthenticationFlowContext context) { var captcha = new FriendlyCaptcha(context.getSession()); if (!captcha.isEnabled()) { return; } // var realm = context.getRealm(); // if (!realm.isBruteForceProtected()) { // return; // } // // var attemptedUsername = context.getAuthenticationSession().getAuthNote(UsernamePasswordForm.ATTEMPTED_USERNAME); // if (attemptedUsername == null) { // return; // } // // var session = context.getSession(); // var user = session.users().getUserByUsername(realm, attemptedUsername); // if (user == null) { // return; // } // // var userLoginFailures = session.loginFailures().getUserLoginFailure(realm, user.getId()); // if (userLoginFailures == null) { // return; // } // // // show friendly captcha only after 2-failed login attempts... // int maxNumFailuresForCaptcha = 2; // first attempt is not recorded, so existence of userLoginFailures counts as 1 therefor +1 // if (userLoginFailures.getNumFailures() + 1 < maxNumFailuresForCaptcha) { // return; // } context.getAuthenticationSession().setAuthNote(FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE, "true"); var locale = LocaleUtils.extractLocaleWithFallbackToRealmLocale(context.getHttpRequest(), context.getRealm()); captcha.configureForm(context.form(), locale); } @Override protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { if (!checkCaptcha(context, formData)) { return false; } return super.validateForm(context, formData); } private boolean checkCaptcha(AuthenticationFlowContext context, MultivaluedMap formData) { var session = context.getSession(); var captcha = new FriendlyCaptcha(session); if (!captcha.isEnabled()) { return true; } var authSession = context.getAuthenticationSession(); boolean captchaTriggered = Boolean.parseBoolean(authSession.getAuthNote(FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE)); if (!captchaTriggered) { return true; } var verificationResult = captcha.verifySolution(formData); if (!verificationResult.isSuccessful()) { context.getEvent().error("captcha-failed"); var response = challenge(context, FriendlyCaptcha.FRIENDLY_CAPTCHA_SOLUTION_INVALID_MESSAGE, disabledByBruteForceFieldError()); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, response); return false; } authSession.removeAuthNote(FRIENDLY_CAPTCHA_CHECK_TRIGGERED_AUTH_NOTE); authSession.setAuthNote(FRIENDLY_CAPTCHA_CHECK_SOLVED_AUTH_NOTE, "true"); return true; } @AutoService(AuthenticatorFactory.class) public static class Factory implements AuthenticatorFactory { private static final AcmeCaptchaUsernamePasswordForm INSTANCE = new AcmeCaptchaUsernamePasswordForm(); @Override public String getId() { return ID; } @Override public Authenticator create(KeycloakSession session) { return INSTANCE; } @Override public String getDisplayType() { return "Acme: Captcha Username Password Form"; } @Override public String getHelpText() { return "Username Password Form with Captcha."; } @Override public String getReferenceCategory() { return "password"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return null; } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/auth/verifyemailcode/VerifyEmailCodeAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.auth.verifyemailcode; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowException; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.SecretGenerator; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionConfigModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; import org.keycloak.sessions.AuthenticationSessionModel; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @JBossLog @AutoService(RequiredActionFactory.class) public class VerifyEmailCodeAction implements RequiredActionProvider, RequiredActionFactory { public static final String PROVIDER_ID = "ACME_VERIFY_EMAIL_CODE"; public static final String EMAIL_CODE_FORM = "email-code-form.ftl"; public static final String EMAIL_CODE_NOTE = "emailCode"; @Override public void evaluateTriggers(RequiredActionContext context) { if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) { context.getUser().addRequiredAction(PROVIDER_ID); log.debug("User is required to verify email"); } } @Override public void requiredActionChallenge(RequiredActionContext context) { requiredActionChallenge(context, null); } public void requiredActionChallenge(RequiredActionContext context, FormMessage errorMessage) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); if (context.getUser().isEmailVerified()) { context.success(); authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY); return; } String email = context.getUser().getEmail(); if (Validation.isBlank(email)) { context.ignore(); return; } LoginFormsProvider form = context.form(); authSession.setClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW, null); VerifyEmailCodeActionConfig config = new VerifyEmailCodeActionConfig(context.getConfig()); // Do not allow resending e-mail by simple page refresh, i.e. when e-mail sent, it should be resent properly via email-verification endpoint if (!Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), email)) { authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, email); EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, email); generateAndSendEmailCode(context, config); } if (errorMessage != null) { form.setErrors(List.of(errorMessage)); } form.setAttribute("codePattern", config.getCodePattern()); form.setAttribute("tryAutoSubmit", config.isTryAutoSubmit()); Response challenge = form.createForm(EMAIL_CODE_FORM); context.challenge(challenge); } @Override public void processAction(RequiredActionContext context) { log.debugf("Re-sending email requested for user: %s", context.getUser().getUsername()); // This will allow user to re-send email again context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY); var formData = context.getHttpRequest().getDecodedFormParameters(); if (formData.containsKey("resend")) { resetEmailCode(context); requiredActionChallenge(context); return; } if (formData.containsKey("cancel")) { resetEmailCode(context); return; } var givenEmailCode = fromDisplayCode(formData.getFirst(EMAIL_CODE_NOTE)); var valid = validateCode(context, givenEmailCode); // TODO add brute-force protection for email code auth context.getEvent().realm(context.getRealm()).user(context.getUser()).detail("action", PROVIDER_ID); if (!valid) { context.getEvent().event(EventType.VERIFY_EMAIL_ERROR).error(Errors.INVALID_USER_CREDENTIALS); requiredActionChallenge(context, new FormMessage(Messages.INVALID_ACCESS_CODE)); return; } context.getUser().setEmailVerified(true); resetEmailCode(context); context.getEvent().event(EventType.VERIFY_EMAIL).success(); context.success(); } protected void generateAndSendEmailCode(RequiredActionContext context, VerifyEmailCodeActionConfig config) { if (context.getAuthenticationSession().getAuthNote(EMAIL_CODE_NOTE) != null) { // skip sending email code return; } var emailCode = SecretGenerator.getInstance().randomString(config.getCodeLength(), SecretGenerator.DIGITS); sendEmailWithCode(context, toDisplayCode(emailCode, config)); context.getAuthenticationSession().setAuthNote(EMAIL_CODE_NOTE, emailCode); } protected String toDisplayCode(String emailCode, VerifyEmailCodeActionConfig config) { return new StringBuilder(emailCode).insert(config.getCodeLength() / 2, "-").toString(); } protected String fromDisplayCode(String code) { return code.replace("-", ""); } protected void resetEmailCode(RequiredActionContext context) { context.getAuthenticationSession().removeAuthNote(EMAIL_CODE_NOTE); } protected boolean validateCode(RequiredActionContext context, String givenCode) { var emailCode = context.getAuthenticationSession().getAuthNote(EMAIL_CODE_NOTE); return emailCode.equals(givenCode); } protected void sendEmailWithCode(RequiredActionContext context, String code) { RealmModel realm = context.getRealm(); UserModel user = context.getUser(); KeycloakSession session = context.getSession(); if (user.getEmail() == null) { log.warnf("Could not send access code email due to missing email. realm=%s user=%s", realm.getId(), user.getUsername()); throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_USER); } Map mailBodyAttributes = new HashMap<>(); mailBodyAttributes.put("username", user.getUsername()); mailBodyAttributes.put("code", code); var realmName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName(); List subjectParams = List.of(realmName); try { var emailProvider = session.getProvider(EmailTemplateProvider.class); emailProvider.setRealm(realm); emailProvider.setUser(user); // Don't forget to add the code-email.ftl (html and text) template to your theme. emailProvider.send("emailCodeSubject", subjectParams, "code-email.ftl", mailBodyAttributes); } catch (EmailException eex) { log.errorf(eex, "Failed to send access code email. realm=%s user=%s", realm.getId(), user.getUsername()); } } @Override public void close() { } @Override public String getId() { return PROVIDER_ID; } @Override public String getDisplayText() { return "Acme: Verify Email Code"; } @Override public RequiredActionProvider create(KeycloakSession session) { return this; } @Override public void init(Config.Scope config) { // this.config = new VerifyEmailCodeActionConfig(config); } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public List getConfigMetadata() { List configProperties = ProviderConfigurationBuilder.create() // .property() // .name("code-length") // .label("Code Length") // .required(true) // .defaultValue(8) // .helpText("Length of email code") // .type(ProviderConfigProperty.INTEGER_TYPE) // .add() // .property() // .name("code-pattern") // .label("Code Pattern String") // .required(true) // .defaultValue("\\d{4}-\\d{4}") // .helpText("Format pattern to render the email code. Use \\d as a placeholder for a digit") // .type(ProviderConfigProperty.STRING_TYPE) // .add() // .property() // .name("try-auto-submit") // .label("Try auto submit") // .required(true) // .defaultValue(false) // .helpText("Submits the form if the input is complete") // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .add() // .build(); return configProperties; } @Data public static class VerifyEmailCodeActionConfig { private int codeLength; private String codePattern; private boolean tryAutoSubmit; public VerifyEmailCodeActionConfig(RequiredActionConfigModel config) { this.codeLength = Integer.parseInt(config.getConfigValue("code-length", "8")); this.codePattern = config.getConfigValue("code-pattern", "\\d{4}-\\d{4}"); this.tryAutoSubmit = Boolean.parseBoolean(config.getConfigValue("try-auto-submit", "false")); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/authz/filter/AcmeAccessFilter.java ================================================ package com.github.thomasdarimont.keycloak.custom.authz.filter; import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.UriInfo; import lombok.extern.jbosslog.JBossLog; import org.keycloak.common.util.Encode; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.admin.AdminAuth; import org.keycloak.services.resources.admin.AdminRoot; import java.io.IOException; import java.net.URI; @JBossLog //@Provider // uncomment this to activate the filter public class AcmeAccessFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext requestContext) throws IOException { UriInfo uriInfo = requestContext.getUriInfo(); URI requestUri = uriInfo.getRequestUri(); String path = requestContext.getUriInfo().getPath(); // TODO add custom filter logic here } /** * Taken from {@link AdminRoot#authenticateRealmAdminRequest(HttpHeaders)} * * @param session * @param headers * @return */ private AdminAuth authenticateRealmAdminRequest(KeycloakSession session, HttpHeaders headers) { String tokenString = AppAuthManager.extractAuthorizationHeaderToken(headers); if (tokenString == null) throw new NotAuthorizedException("Bearer"); AccessToken token; try { JWSInput input = new JWSInput(tokenString); token = input.readJsonContent(AccessToken.class); } catch (JWSInputException e) { throw new NotAuthorizedException("Bearer token format error"); } String realmName = Encode.decodePath(token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1)); RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); if (realm == null) { throw new NotAuthorizedException("Unknown realm in token"); } session.getContext().setRealm(realm); AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session) .setRealm(realm) .setConnection(session.getContext().getConnection()) .setHeaders(headers) .authenticate(); if (authResult == null) { log.debug("Token not valid"); throw new NotAuthorizedException("Bearer"); } return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), authResult.getClient()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/authz/policies/AcmeImpersonationPolicyProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.authz.policies; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.MultivaluedMap; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.attribute.Attributes; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; import org.keycloak.authorization.policy.evaluation.Evaluation; import org.keycloak.authorization.policy.provider.PolicyProvider; import org.keycloak.authorization.policy.provider.PolicyProviderFactory; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.utils.KeycloakSessionUtil; import java.util.List; @JBossLog public class AcmeImpersonationPolicyProvider implements PolicyProvider { private final KeycloakSession session; private final AuthorizationProvider authorization; public AcmeImpersonationPolicyProvider(KeycloakSession session, AuthorizationProvider authorization) { this.session = session; this.authorization = authorization; } @Override public void evaluate(Evaluation evaluation) { log.info("Evaluate"); List requestedScopeNames = ((DefaultEvaluation) evaluation).getParentPolicy().getScopes().stream().map(Scope::getName).toList(); boolean userImpersonation = requestedScopeNames.size() == 1 && requestedScopeNames.contains("user-impersonated"); // UserPermissions.USER_IMPERSONATED_SCOPE is currently not public... boolean adminImpersonation = !userImpersonation; Attributes attributes = evaluation.getContext().getIdentity().getAttributes(); String fromUserId = attributes.getValue("sub").asString(0); String fromUsername = attributes.getValue("preferred_username").asString(0); KeycloakContext keycloakContext = session.getContext(); MultivaluedMap formParameters = keycloakContext.getHttpRequest().getDecodedFormParameters(); String toUserId = formParameters.getFirst(OAuth2Constants.REQUESTED_SUBJECT); String requestedTokenType = formParameters.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE); RealmModel realm = keycloakContext.getRealm(); UserModel sourceUser = session.users().getUserById(realm, fromUserId); UserModel targetUser = session.users().getUserById(realm, toUserId); log.debugf("Check user impersonation. realm=%s impersonator=%s targetUsername=%s", realm.getName(), sourceUser.getUsername(), targetUser.getUsername()); if (isImpersonationAllowed(realm, sourceUser, targetUser)) { log.debugf("User impersonation granted. realm=%s impersonator=%s targetUsername=%s", realm.getName(), sourceUser.getUsername(), targetUser.getUsername()); evaluation.grant(); } else { log.debugf("User impersonation denied. realm=%s impersonator=%s targetUsername=%s", realm.getName(), sourceUser.getUsername(), targetUser.getUsername()); evaluation.deny(); } } protected boolean isImpersonationAllowed(RealmModel realm, UserModel sourceUser, UserModel targetUser) { // TODO implement your custom impersonation logic here return true; } @Override public void close() { // NOOP } public static class AcmeImpersonationPolicyRepresentation extends JSPolicyRepresentation { // JSPolicyRepresentation to inherit the code option } @AutoService(PolicyProviderFactory.class) public static class Factory implements PolicyProviderFactory { @Override public String getId() { return "acme-impersonation-policy"; } @Override public String getName() { return "Acme: Impersonation"; } @Override public String getGroup() { return "Custom"; } @Override public PolicyProvider create(KeycloakSession session) { return create(session, null); } @Override public PolicyProvider create(AuthorizationProvider authorization) { return create(KeycloakSessionUtil.getKeycloakSession(), authorization); } public PolicyProvider create(KeycloakSession session, AuthorizationProvider authorization) { return new AcmeImpersonationPolicyProvider(session, authorization); } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public AcmeImpersonationPolicyRepresentation toRepresentation(Policy policy, AuthorizationProvider authorization) { var rep = new AcmeImpersonationPolicyRepresentation(); rep.setId(policy.getId()); rep.setName(policy.getName()); rep.setDescription(policy.getDescription()); rep.setDecisionStrategy(policy.getDecisionStrategy()); rep.setCode(policy.getConfig().get("code")); rep.setType(policy.getType()); return rep; } @Override public void onUpdate(Policy policy, AcmeImpersonationPolicyRepresentation representation, AuthorizationProvider authorization) { policy.setDecisionStrategy(representation.getDecisionStrategy()); policy.setDescription(policy.getDescription()); policy.setLogic(policy.getLogic()); } @Override public Class getRepresentationType() { return AcmeImpersonationPolicyRepresentation.class; } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/ClientConfig.java ================================================ package com.github.thomasdarimont.keycloak.custom.config; import lombok.RequiredArgsConstructor; import org.keycloak.models.ClientModel; @RequiredArgsConstructor public class ClientConfig implements ConfigAccessor { private final ClientModel client; @Override public String getType() { return "Client"; } @Override public String getSource() { return client.getClientId(); } public String getValue(String key) { return client.getAttribute(key); } public boolean containsKey(String key) { return client.getAttributes().containsKey(key); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/ConfigAccessor.java ================================================ package com.github.thomasdarimont.keycloak.custom.config; import lombok.RequiredArgsConstructor; import java.util.function.Function; import static java.util.function.Function.identity; public interface ConfigAccessor { String getType(); String getSource(); boolean containsKey(String key); String getValue(String key); default T getValueOrDefault(String key, T defaultValue, Function converter) { String value = getValue(key); if (value == null) { return defaultValue; } return converter.apply(value); } default T getValue(String key, Function converter) { String value = getValue(key); if (value == null) { throw new MissingKeyException(getType(), getSource(), key); } return converter.apply(value); } default String getString(String key, String defaultValue) { return getValueOrDefault(key, defaultValue, identity()); } default String getString(String key) { return getValue(key, identity()); } default Integer getInt(String key, Integer defaultValue) { return getValueOrDefault(key, defaultValue, Integer::parseInt); } default int getInt(String key) { return getValue(key, Integer::parseInt); } default > T getEnum(Class enumType, String key, T defaultValue) { return getValueOrDefault(key, defaultValue, s -> Enum.valueOf(enumType, s)); } default > T getEnum(Class enumType, String key) { return getValue(key, s -> Enum.valueOf(enumType, s)); } default Long getLong(String key, Long defaultValue) { return getValueOrDefault(key, defaultValue, Long::parseLong); } default long getLong(String key) { return getValue(key, Long::parseLong); } default Boolean getBoolean(String key, Boolean defaultValue) { return getValueOrDefault(key, defaultValue, Boolean::parseBoolean); } default boolean getBoolean(String key) { return getValue(key, Boolean::parseBoolean); } /** * Check if the value is present and non-null and not an empty string. * * @param key * @param defaultValue * @return */ default boolean isConfigured(String key, boolean defaultValue) { if (!containsKey(key)) { return defaultValue; } String value = getValue(key); if (value == null || value.isBlank()) { return defaultValue; } return true; } @RequiredArgsConstructor class MissingKeyException extends RuntimeException { private final String type; private final String source; private final String key; @Override public String getMessage() { return String.format("Missing %s Config Key. %s=%s, key=%s", type, type, source, key); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/MapConfig.java ================================================ package com.github.thomasdarimont.keycloak.custom.config; import java.util.Collections; import java.util.Map; public class MapConfig implements ConfigAccessor { private final Map config; public MapConfig(Map config) { this.config = config == null ? Collections.emptyMap() : config; } @Override public String getType() { return "Map"; } @Override public String getSource() { return "configMap"; } @Override public boolean containsKey(String key) { return config.containsKey(key); } @Override public String getValue(String key) { return config.get(key); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/config/RealmConfig.java ================================================ package com.github.thomasdarimont.keycloak.custom.config; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.keycloak.models.RealmModel; @Getter @AllArgsConstructor @RequiredArgsConstructor public class RealmConfig implements ConfigAccessor { private final RealmModel realm; private String prefix; @Override public String getType() { return "Realm"; } @Override public String getSource() { return realm.getName(); } public String getValue(String key) { return realm.getAttribute(prefixed(key)); } public boolean containsKey(String key) { return realm.getAttributes().containsKey(prefixed(key)); } private String prefixed(String key) { return prefix == null ? key : prefix + key; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ConsentSelectionAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.consent; import com.google.auto.service.AutoService; import lombok.Builder; import lombok.Data; import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.IDToken; import org.keycloak.sessions.AuthenticationSessionModel; import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import static java.util.stream.Collectors.toList; @AutoService(RequiredActionFactory.class) public class ConsentSelectionAction implements RequiredActionProvider, RequiredActionFactory { private static final boolean REQUIRE_UPDATE_PROFILE_AFTER_CONSENT_UPDATE = false; private static final String AUTH_SESSION_CONSENT_CHECK_MARKER = "checked"; private Map> getScopeFieldMapping() { var map = new HashMap>(); map.put(OAuth2Constants.SCOPE_PHONE, List.of(new ScopeField("phoneNumber", "tel", u -> u.getFirstAttribute("phoneNumber")))); // map.put(OAuth2Constants.SCOPE_EMAIL, List.of(new ScopeField(IDToken.EMAIL, "email", UserModel::getEmail))); // // Dedicated client scope: name map.put("name", List.of( // new ScopeField(IDToken.GIVEN_NAME, "text", UserModel::getFirstName), // new ScopeField(IDToken.FAMILY_NAME, "text", UserModel::getLastName) // )); // Dedicated client scope: name map.put("firstname", List.of(new ScopeField("firstName", "text", UserModel::getFirstName))); // // Dedicated client scope: address map.put("address", List.of( // new ScopeField("address.country", "text", u -> u.getFirstAttribute("address.country")), // new ScopeField("address.city", "text", u -> u.getFirstAttribute("address.city")), // new ScopeField("address.street", "text", u -> u.getFirstAttribute("address.street")), // new ScopeField("address.zip", "text", u -> u.getFirstAttribute("address.zip")) // )); return Collections.unmodifiableMap(map); } @Override public String getId() { return "acme-dynamic-consent"; } @Override public String getDisplayText() { return "Acme: Dynamic Consent selection"; } @Override public RequiredActionProvider create(KeycloakSession session) { return this; } @Override public InitiatedActionSupport initiatedActionSupport() { // whether we can refer to that action via kc_actions URL parameter return InitiatedActionSupport.SUPPORTED; } @Override public void evaluateTriggers(RequiredActionContext context) { var authSession = context.getAuthenticationSession(); var user = context.getUser(); // For Keycloak versions up to 18.0.2 evaluateTriggers is called multiple times, // since we need to perform this check only once per auth session, we use a marker // to remember whether the check already took place. if (AUTH_SESSION_CONSENT_CHECK_MARKER.equals(authSession.getClientNote(getId()))) { return; } var missingConsents = getScopeInfo(context.getSession(), authSession, user); var prompt = context.getUriInfo().getQueryParameters().getFirst(OAuth2Constants.PROMPT); var explicitConsentRequested = OIDCLoginProtocol.PROMPT_VALUE_CONSENT.equals(prompt); var consentMissingForRequiredScopes = !missingConsents.getMissingRequired().isEmpty(); var consentMissingForOptionalScopes = !missingConsents.getMissingOptional().isEmpty(); var consentSelectionRequired = explicitConsentRequested || consentMissingForRequiredScopes || consentMissingForOptionalScopes; if (consentSelectionRequired) { authSession.addRequiredAction(getId()); authSession.setClientNote(getId(), AUTH_SESSION_CONSENT_CHECK_MARKER); if (consentMissingForRequiredScopes && REQUIRE_UPDATE_PROFILE_AFTER_CONSENT_UPDATE) { authSession.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); } } else { authSession.removeRequiredAction(getId()); } } @Override public void requiredActionChallenge(RequiredActionContext context) { // Show form context.challenge(createForm(context, null)); } protected Response createForm(RequiredActionContext context, Consumer formCustomizer) { var form = context.form(); var user = context.getUser(); form.setAttribute(UserModel.USERNAME, user.getUsername()); var authSession = context.getAuthenticationSession(); Function fun = f -> new ScopeFieldBean(f, user); var scopeInfo = getScopeInfo(context.getSession(), authSession, user); var grantedRequired = scopeInfo.getGrantedRequired(); var grantedOptional = scopeInfo.getGrantedOptional(); var missingRequired = scopeInfo.getMissingRequired(); var missingOptional = scopeInfo.getMissingOptional(); var scopeFieldMapping = getScopeFieldMapping(); var scopes = new ArrayList(); for (var currentScopes : List.of(grantedRequired, missingRequired, grantedOptional, missingOptional)) { for (var scope : currentScopes) { var fields = scopeFieldMapping.getOrDefault(scope.getName(), List.of()).stream().map(fun).collect(toList()); var optional = currentScopes == grantedOptional || currentScopes == missingOptional; var granted = currentScopes == grantedRequired || currentScopes == grantedOptional; scopes.add(new ScopeBean(scope, optional, granted, fields)); } } scopes.sort(ScopeBean.DEFAULT_ORDER); form.setAttribute("scopes", scopes); if (formCustomizer != null) { formCustomizer.accept(form); } // use form from src/main/resources/theme-resources/templates/ return form.createForm("select-consent-form.ftl"); } @Override public void processAction(RequiredActionContext context) { // handle consent selection from user var formParameters = context.getSession().getContext().getHttpRequest().getDecodedFormParameters(); var authSession = context.getAuthenticationSession(); var session = context.getSession(); var users = context.getSession().users(); var realm = context.getRealm(); var event = context.getEvent(); var client = authSession.getClient(); var user = context.getUser(); event.client(client).user(user).event(EventType.GRANT_CONSENT); if (formParameters.getFirst("cancel") != null) { // User choose NOT to update consented scopes event.error(Errors.CONSENT_DENIED); // return to the application without consent update UserConsentModel consentModel = session.users().getConsentByClient(realm, user.getId(), client.getId()); if (consentModel == null) { // No consents given: Deny access to application context.failure(); return; } var currentGrantedScopes = consentModel.getGrantedClientScopes(); if (currentGrantedScopes.isEmpty()) { // No consents given: Deny access to application context.failure(); return; } var currentGrantedScopesIds = currentGrantedScopes.stream().map(ClientScopeModel::getId).collect(Collectors.toSet()); var currentGrantedScopeNames = currentGrantedScopes.stream().map(ClientScopeModel::getName).collect(Collectors.joining(" ")); context.getAuthenticationSession().setClientScopes(currentGrantedScopesIds); context.getAuthenticationSession().setClientNote(OAuth2Constants.SCOPE, "openid " + currentGrantedScopeNames); // Allow access to application (with original consented scopes) context.success(); return; } var scopeSelection = formParameters.get("scopeSelection"); var scopeInfo = getScopeInfo(session, authSession, user); var scopesToAskForConsent = new HashSet(); for (var scopes : List.of( // scopeInfo.getGrantedRequired(), // scopeInfo.getMissingRequired(), // scopeInfo.getGrantedOptional(), // scopeInfo.getMissingOptional())) { for (var scope : scopes) { if (scopeSelection.contains(scope.getName())) { scopesToAskForConsent.add(scope); } } } if (!scopesToAskForConsent.isEmpty()) { // TODO find a way to merge the existing consent with the new consent instead of replacing the existing consent var consentByClient = users.getConsentByClient(realm, user.getId(), client.getId()); if (consentByClient != null) { users.revokeConsentForClient(realm, user.getId(), client.getId()); } consentByClient = new UserConsentModel(client); scopesToAskForConsent.forEach(consentByClient::addGrantedClientScope); users.addConsent(realm, user.getId(), consentByClient); var grantedScopeNames = consentByClient.getGrantedClientScopes().stream().map(ClientScopeModel::getName).collect(Collectors.toList()); grantedScopeNames.add(0, OAuth2Constants.SCOPE_OPENID); var scope = String.join(" ", grantedScopeNames); // TODO find a better way to propagate the selected scopes authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); event.detail(OAuth2Constants.SCOPE, scope).success(); } // TODO ensure that required scopes are always consented authSession.removeRequiredAction(getId()); context.success(); } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } private ScopeInfo getScopeInfo(KeycloakSession session, AuthenticationSessionModel authSession, UserModel user) { var client = authSession.getClient(); var requestedScopes = computeRequestedScopes(authSession, client); var consentByClient = session.users().getConsentByClient(authSession.getRealm(), user.getId(), client.getId()); var missingRequired = new HashSet<>(requestedScopes.getRequired().values()); var missingOptional = new HashSet<>(requestedScopes.getOptional().values()); var grantedRequired = Collections.emptySet(); var grantedOptional = Collections.emptySet(); if (consentByClient != null) { grantedRequired = new HashSet<>(requestedScopes.getRequired().values()); grantedOptional = new HashSet<>(requestedScopes.getOptional().values()); grantedRequired.retainAll(consentByClient.getGrantedClientScopes()); grantedOptional.retainAll(consentByClient.getGrantedClientScopes()); missingRequired.removeAll(consentByClient.getGrantedClientScopes()); missingOptional.removeAll(consentByClient.getGrantedClientScopes()); } return ScopeInfo.builder() // .grantedRequired(grantedRequired) // .grantedOptional(grantedOptional) // .missingRequired(missingRequired) // .missingOptional(missingOptional) // .build(); } private RequestedScopes computeRequestedScopes(AuthenticationSessionModel authSession, ClientModel client) { var defaultClientScopes = client.getClientScopes(true); var optionalClientScopes = client.getClientScopes(false); var requestedRequired = new HashMap(); var requestedOptional = new HashMap(); for (var scopeId : authSession.getClientScopes()) { var foundInDefaultScope = false; for (var scope : defaultClientScopes.values()) { if (scope.getId().equals(scopeId)) { requestedRequired.put(scope.getName(), scope); foundInDefaultScope = true; break; } } if (!foundInDefaultScope) { for (var scope : optionalClientScopes.values()) { if (scope.getId().equals(scopeId)) { requestedOptional.put(scope.getName(), scope); break; } } } } return new RequestedScopes(requestedRequired, requestedOptional); } @Data @Builder static class ScopeInfo { private final Set grantedRequired; private final Set grantedOptional; private final Set missingRequired; private final Set missingOptional; } @Data static class RequestedScopes { private final Map required; private final Map optional; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ScopeBean.java ================================================ package com.github.thomasdarimont.keycloak.custom.consent; import lombok.RequiredArgsConstructor; import org.keycloak.models.ClientScopeModel; import java.util.Collections; import java.util.Comparator; import java.util.List; @RequiredArgsConstructor public class ScopeBean { public static final Comparator DEFAULT_ORDER; static { DEFAULT_ORDER = Comparator.comparing(ScopeBean::getGuiOrder); } private final ClientScopeModel scopeModel; private final boolean optional; private final boolean granted; private final List scopeFields; public boolean isOptional() { return optional; } public boolean isGranted() { return granted; } public String getGuiOrder() { String guiOrder = getScopeModel().getGuiOrder(); if (guiOrder != null) { return guiOrder; } return getName(); } public ClientScopeModel getScopeModel() { return scopeModel; } public String getName() { return scopeModel.getName(); } public String getDescription() { return scopeModel.getDescription(); } public List getFields() { return Collections.unmodifiableList(scopeFields); } @Override public String toString() { return scopeModel.getName(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ScopeField.java ================================================ package com.github.thomasdarimont.keycloak.custom.consent; import lombok.Data; import org.keycloak.models.UserModel; import java.util.function.Function; @Data public class ScopeField { private final String name; private final String type; private final Function valueAccessor; } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/consent/ScopeFieldBean.java ================================================ package com.github.thomasdarimont.keycloak.custom.consent; import lombok.Data; import org.keycloak.models.UserModel; @Data public class ScopeFieldBean { private final ScopeField scopeField; private final UserModel user; public String getName() { return scopeField.getName(); } public String getType() { return scopeField.getType(); } public String getValue() { return scopeField.getValueAccessor().apply(user); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/context/ContextSelectionAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.context; import com.github.thomasdarimont.keycloak.custom.support.UserSessionUtils; import com.google.auto.service.AutoService; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.sessions.AuthenticationSessionModel; import java.util.List; import java.util.function.Consumer; /** * Example for prompting a user for a context selection after authentication. * The context selection key will then be stored in the user session and can be exposed to clients. * Context selection key could be the key of a business entity (tenant, project, etc.). *

*/ @JBossLog public class ContextSelectionAction implements RequiredActionProvider { public static final String ID = "acme-context-selection-action"; private static final String CONTEXT_KEY = "acme.context.key"; private static final String CONTEXT_SELECTION_PARAM = "contextSelectionKey"; private static final String CONTEXT_FORM_ATTRIBUTE = "context.selection.key"; /** * Allows explicit usage via auth url parameter kc_action=acme-context-selection-action * * @return */ @Override public InitiatedActionSupport initiatedActionSupport() { return InitiatedActionSupport.SUPPORTED; } @Override public void evaluateTriggers(RequiredActionContext context) { var authSession = context.getAuthenticationSession(); // prevents repeated required action execution within the same authentication session if (authSession.getAuthNote(ID) != null) { return; } authSession.setAuthNote(ID, "true"); // mark this action as applied // TODO check if context selection is required // TODO allow to accept contextSelectionKey via URL Parameter // handle dynamic context selection for legacy apps with grant_type=password var formParams = context.getHttpRequest().getDecodedFormParameters(); if (OAuth2Constants.PASSWORD.equals(formParams.getFirst(OAuth2Constants.GRANT_TYPE))) { // allow to accept contextSelectionKey via form post Parameter if (formParams.containsKey(CONTEXT_SELECTION_PARAM)) { var contextKey = formParams.getFirst(CONTEXT_SELECTION_PARAM); if (isValidContextKey(context, contextKey)) { authSession.setUserSessionNote(CONTEXT_KEY, contextKey); } else { // contextSelectionKey provided with invalid value context.failure(); } } authSession.removeRequiredAction(ID); return; } // handle dynamic context selection for standard flow // check if context selection already happened in another user session? var userSession = UserSessionUtils.getUserSessionFromAuthenticationSession(context.getSession(), context.getAuthenticationSession()); // Note, if the user just authenticated there is no user session yet. if (userSession != null) { var userSessionNotes = userSession.getNotes(); if (userSessionNotes.containsKey(CONTEXT_KEY)) { authSession.removeRequiredAction(ID); return; } } // add this required action to the auth session to force execution after authentication authSession.addRequiredAction(ID); } private boolean isValidContextKey(RequiredActionContext context, String contextKey) { var options = computeContextOptions(context); var foundValidContextKey = false; for (var option : options) { if (option.getKey().equals(contextKey)) { foundValidContextKey = true; break; } } return foundValidContextKey; } @Override public void requiredActionChallenge(RequiredActionContext context) { showContextSelectionForm(context, null); } private void showContextSelectionForm(RequiredActionContext context, Consumer formCustomizer) { var allowedContextOptions = computeContextOptions(context); // TODO handle case when options are empty var currentContextItem = getCurrentContextItem(context, allowedContextOptions); // show context selection form var form = context.form() // .setAttribute("username", context.getUser().getUsername()) // .setAttribute("currentContext", currentContextItem) // .setAttribute("contextOptions", allowedContextOptions); // allow to customize form, e.g. to add custom error messages if (formCustomizer != null) { formCustomizer.accept(form); } // Note, see template in internal-modern theme var response = form.createForm("context-selection.ftl"); context.challenge(response); } private static ContextItem getCurrentContextItem(RequiredActionContext context, List allowedContextOptions) { var userSession = UserSessionUtils.getUserSessionFromAuthenticationSession(context.getSession(), context.getAuthenticationSession()); var currentContextKey = userSession != null ? userSession.getNote(CONTEXT_KEY) : null; if (currentContextKey == null) { return null; } return allowedContextOptions.stream().filter(item -> item.getKey().equals(currentContextKey)).findAny().orElse(null); } private List computeContextOptions(RequiredActionContext actionContext) { // note, here one would call custom logic to populate the eligible context options return List.of( // new ContextItem("key1", "Context 1"), // new ContextItem("key2", "Context 2"), // new ContextItem("key3", "Context 3") // ); } @Override public void initiatedActionCanceled(KeycloakSession session, AuthenticationSessionModel authSession) { // TODO clarify if context selection can be cancelled // NOOP } @Override public void processAction(RequiredActionContext context) { var formData = context.getSession().getContext().getHttpRequest().getDecodedFormParameters(); if (formData.containsKey("cancel")) { context.success(); return; } if (!formData.containsKey(CONTEXT_FORM_ATTRIBUTE)) { // TODO show empty selection is not allowed error showContextSelectionForm(context, null); return; } var selectedContextKey = formData.getFirst(CONTEXT_FORM_ATTRIBUTE); // check if selected context key is allowed var allowedContextOptions = computeContextOptions(context); if (allowedContextOptions.stream().filter(item -> item.getKey().equals(selectedContextKey)).findAny().isEmpty()) { // TODO show value is not allowed error showContextSelectionForm(context, null); return; } // propagate selected context to user session log.infof("Switching user context. realm=%s userId=%s contextKey=%s", // context.getRealm().getName(), context.getUser().getId(), selectedContextKey); context.getAuthenticationSession().setUserSessionNote(CONTEXT_KEY, selectedContextKey); context.success(); } @Override public void close() { // NOOP } @Data public static class ContextItem { private final String key; private final String label; } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private static final ContextSelectionAction INSTANCE = new ContextSelectionAction(); @Override public String getId() { return ID; } @Override public String getDisplayText() { return "Acme: User Context Selection"; } @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/email/AcmeEmailSenderProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.email; import com.google.auto.service.AutoService; import org.keycloak.Config; import org.keycloak.email.DefaultEmailAuthenticator; import org.keycloak.email.DefaultEmailSenderProvider; import org.keycloak.email.DefaultEmailSenderProviderFactory; import org.keycloak.email.EmailAuthenticator; import org.keycloak.email.EmailException; import org.keycloak.email.EmailSenderProvider; import org.keycloak.email.EmailSenderProviderFactory; import org.keycloak.email.PasswordAuthEmailAuthenticator; import org.keycloak.email.TokenAuthEmailAuthenticator; import org.keycloak.models.KeycloakSession; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class AcmeEmailSenderProvider extends DefaultEmailSenderProvider { private final KeycloakSession session; public AcmeEmailSenderProvider(KeycloakSession session, Map authenticators) { super(session, authenticators); this.session = session; } @Override public void send(Map config, String address, String subject, String textBody, String htmlBody) throws EmailException { // adjust "from" via config object super.send(config, address, subject, textBody, htmlBody); } // @AutoService(EmailSenderProviderFactory.class) public static class Factory extends DefaultEmailSenderProviderFactory { private final Map emailAuthenticators = new ConcurrentHashMap<>(); @Override public EmailSenderProvider create(KeycloakSession session) { return new AcmeEmailSenderProvider(session, emailAuthenticators); } @Override public void init(Config.Scope config) { emailAuthenticators.put(EmailAuthenticator.AuthenticatorType.NONE, new DefaultEmailAuthenticator()); emailAuthenticators.put(EmailAuthenticator.AuthenticatorType.BASIC, new PasswordAuthEmailAuthenticator()); emailAuthenticators.put(EmailAuthenticator.AuthenticatorType.TOKEN, new TokenAuthEmailAuthenticator()); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CorsUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import org.keycloak.http.HttpRequest; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.utils.WebOriginsUtils; import jakarta.ws.rs.core.Response; import org.keycloak.services.cors.Cors; import java.net.URI; import java.util.Set; public class CorsUtils { private static final String FALLBACK_CLIENT_ID = "app-minispa"; public static Cors addCorsHeaders(KeycloakSession session, // HttpRequest request, // Set allowedHttpMethods, // String clientId // ) { var client = resolveClient(session, clientId); var allowedOrigins = WebOriginsUtils.resolveValidWebOrigins(session, client); Cors cors = Cors.builder(); var originHeaderValue = request.getHttpHeaders().getHeaderString("origin"); if (originHeaderValue != null) { var requestOrigin = URI.create(originHeaderValue).toString(); if (allowedOrigins.contains(requestOrigin)) { cors.allowedOrigins(requestOrigin); // } } var methods = allowedHttpMethods.toArray(new String[0]); return cors.auth().allowedMethods(methods).preflight(); } private static ClientModel resolveClient(KeycloakSession session, String clientId) { // TODO only allow custom clients here var realm = session.getContext().getRealm(); String clientIdToUse; if (clientId != null) { clientIdToUse = clientId; } else { clientIdToUse = new RealmConfig(realm).getString("customAccountEndpointsClient", FALLBACK_CLIENT_ID); } return realm.getClientByClientId(clientIdToUse); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CustomAdminResourceProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints; import com.github.thomasdarimont.keycloak.custom.endpoints.admin.CustomAdminResource; import com.github.thomasdarimont.keycloak.custom.endpoints.admin.UserProvisioningResource.UserProvisioningConfig; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory; import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator; import java.util.regex.Pattern; @JBossLog public class CustomAdminResourceProvider implements AdminRealmResourceProvider { public static final String ID = "custom-admin-resources"; private final UserProvisioningConfig privisioningConfig; public CustomAdminResourceProvider(UserProvisioningConfig privisioningConfig) { this.privisioningConfig = privisioningConfig; } @Override public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) { return new CustomAdminResource(session, realm, auth, adminEvent, privisioningConfig); } @Override public void close() { } @AutoService(AdminRealmResourceProviderFactory.class) public static class Factory implements AdminRealmResourceProviderFactory { @Override public String getId() { return ID; } private CustomAdminResourceProvider customAdminResource; @Override public AdminRealmResourceProvider create(KeycloakSession session) { return customAdminResource; } @Override public void init(Config.Scope config) { Config.Scope scope = config.scope("users", "provisioning"); String realmRole = "user-modifier-acme"; String attributePatternString = "(.*)"; if (scope != null) { String customRealmRole = scope.get("required-realm-role"); if (customRealmRole != null) { realmRole = customRealmRole; } String customAttributePatternString = scope.get("managed-attribute-pattern"); if (customAttributePatternString != null) { attributePatternString = customAttributePatternString; } } var privisioningConfig = new UserProvisioningConfig(realmRole, Pattern.compile(attributePatternString)); customAdminResource = new CustomAdminResourceProvider(privisioningConfig); } @Override public void postInit(KeycloakSessionFactory factory) { log.info("### Register custom admin resources"); } @Override public void close() { } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CustomResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import com.github.thomasdarimont.keycloak.custom.endpoints.account.AcmeAccountResource; import com.github.thomasdarimont.keycloak.custom.endpoints.admin.AdminSettingsResource; import com.github.thomasdarimont.keycloak.custom.endpoints.applications.ApplicationsInfoResource; import com.github.thomasdarimont.keycloak.custom.endpoints.branding.BrandingResource; import com.github.thomasdarimont.keycloak.custom.endpoints.credentials.UserCredentialsInfoResource; import com.github.thomasdarimont.keycloak.custom.endpoints.demo.DemosResource; import com.github.thomasdarimont.keycloak.custom.endpoints.idp.IdpApplications; import com.github.thomasdarimont.keycloak.custom.endpoints.migration.TokenMigrationResource; import com.github.thomasdarimont.keycloak.custom.endpoints.migration.UserImportMigrationResource; import com.github.thomasdarimont.keycloak.custom.endpoints.offline.OfflineSessionPropagationResource; import com.github.thomasdarimont.keycloak.custom.endpoints.profile.UserProfileResource; import com.github.thomasdarimont.keycloak.custom.endpoints.settings.UserSettingsResource; import jakarta.ws.rs.GET; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.cors.Cors; import org.keycloak.services.managers.AuthenticationManager; import java.util.HashMap; import java.util.Map; /** * {@code * curl -v http://localhost:8080/auth/realms/acme-apps/custom-resources/ping | jq -C . * } */ public class CustomResource { private final KeycloakSession session; private final AccessToken token; public CustomResource(KeycloakSession session, AccessToken accessToken) { this.session = session; this.token = accessToken; } @GET @Path("ping") @Produces(MediaType.APPLICATION_JSON) public Response ping() { KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); Map payload = new HashMap<>(); payload.put("realm", realm.getName()); payload.put("user", token == null ? "anonymous" : token.getPreferredUsername()); payload.put("timestamp", System.currentTimeMillis()); payload.put("greeting", new RealmConfig(realm).getString("acme_greeting", "Greetings!")); return Response.ok(payload).build(); } @OPTIONS public Response preflight() { return Cors.builder().preflight().add(Response.ok()); } @Path("me/settings") public UserSettingsResource settings() { return new UserSettingsResource(session, token); } @Path("me/credentials") public UserCredentialsInfoResource credentials() { return new UserCredentialsInfoResource(session, token); } @Path("me/applications") public ApplicationsInfoResource applications() { return new ApplicationsInfoResource(session, token); } @Path("me/profile") public UserProfileResource profile() { return new UserProfileResource(session, token); } @Path("me/account") public AcmeAccountResource account() { return new AcmeAccountResource(session, token); } @Path("mobile/session-propagation") public OfflineSessionPropagationResource sessionPropagation() { return new OfflineSessionPropagationResource(session, token); } /** * https://id.acme.test:8443/auth/realms/workshop/custom-resources/branding/css * @return */ @Path("branding") public BrandingResource branding() { return new BrandingResource(session); } /** * https://id.acme.test:8443/auth/realms/acme-internal/custom-resources/admin/settings * * @return */ @Path("admin/settings") public AdminSettingsResource adminSettings() { KeycloakContext context = session.getContext(); var authResult = AuthenticationManager.authenticateIdentityCookie(session, context.getRealm(), true); if (authResult == null) { throw new ErrorResponseException("access_denied", "Admin auth required", Response.Status.FORBIDDEN); } var localRealmAdminRole = context.getRealm().getClientByClientId("realm-management").getRole("realm-admin"); if (!authResult.getUser().hasRole(localRealmAdminRole)) { var loginForm = session.getProvider(LoginFormsProvider.class); throw new WebApplicationException(loginForm.createErrorPage(Response.Status.FORBIDDEN)); } return new AdminSettingsResource(session, authResult); } /** * http://localhost:8080/auth/realms/acme-token-migration/custom-resources/migration/token * * @return */ @Path("migration/token") public TokenMigrationResource tokenMigration() { return new TokenMigrationResource(session, token); } @Path("migration/users") public UserImportMigrationResource userMigration() { return new UserImportMigrationResource(session, token); } @Path("idp/applications") public IdpApplications idpApplications() { return new IdpApplications(session); } @Path("demos") public DemosResource demoResource() { return new DemosResource(session); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/CustomResourceProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints; import com.github.thomasdarimont.keycloak.custom.support.AuthUtils; import com.google.auto.service.AutoService; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authorization.util.Tokens; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProviderFactory; import org.keycloak.services.resources.admin.AdminAuth; import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.fgap.AdminPermissions; import org.keycloak.utils.KeycloakSessionUtil; import java.util.Optional; import java.util.regex.Pattern; @JBossLog @RequiredArgsConstructor public class CustomResourceProvider implements RealmResourceProvider { public static final String ID = "custom-resources"; private static final Pattern ALLOWED_REALM_NAMES_PATTERN = Pattern.compile( Optional.ofNullable(System.getenv("KEYCLOAK_CUSTOM_ENDPOINT_REALM_PATTERN")) .orElse("(acme-.*|workshop.*|company.*)")); @Override public Object getResource() { KeycloakSession session = KeycloakSessionUtil.getKeycloakSession(); AccessToken accessToken = Tokens.getAccessToken(session); // check access // if (accessToken == null) { // throw new NotAuthorizedException("Invalid Token", Response.status(UNAUTHORIZED).build()); // } else if (!ScopeUtils.hasScope("custom.api", accessToken.getScope())) { // throw new ForbiddenException("No Access", Response.status(FORBIDDEN).build()); // } RealmModel realm = session.getContext().getRealm(); if (realm == null) { return null; } boolean allowedRealm = ALLOWED_REALM_NAMES_PATTERN.matcher(realm.getName()).matches(); if (!allowedRealm) { log.warnf("### Ignoring custom-resource request for unsupported realm name: %s", realm.getName()); // only expose custom endpoints for allowed realms return null; } return new CustomResource(session, accessToken); } AdminPermissionEvaluator getAuth(KeycloakSession session) { AdminAuth adminAuth = AuthUtils.getAdminAuth(session); return AdminPermissions.evaluator(session, session.getContext().getRealm(), adminAuth); } @Override public void close() { // NOOP } @JBossLog @AutoService(RealmResourceProviderFactory.class) public static class Factory implements RealmResourceProviderFactory { private static final CustomResourceProvider INSTANCE = new CustomResourceProvider(); @Override public String getId() { return CustomResourceProvider.ID; } @Override public RealmResourceProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { log.info("Initialize"); } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/account/AcmeAccountResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.account; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.http.HttpRequest; import org.keycloak.models.AccountRoles; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.cors.Cors; import java.util.HashMap; import java.util.Set; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; public class AcmeAccountResource { private final KeycloakSession session; private final AccessToken token; public AcmeAccountResource(KeycloakSession session, AccessToken token) { this.session = session; this.token = token; } @OPTIONS public Response getCorsOptions() { return withCors(session.getContext().getHttpRequest()).add(Response.ok()); } @DELETE @Produces(MediaType.APPLICATION_JSON) public Response handleAccountDeletionRequest() { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); UserModel user = session.users().getUserById(realm, token.getSubject()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } var resourceAccess = token.getResourceAccess(); AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE)); if (!canAccessAccount) { return Response.status(FORBIDDEN).build(); } var uriInfo = session.getContext().getHttpRequest().getUri(); AccountActivity.onAccountDeletionRequested(session, realm, user, uriInfo); var responseBody = new HashMap(); var request = context.getHttpRequest(); return withCors(request).add(Response.ok(responseBody)); } private Cors withCors(HttpRequest request) { return CorsUtils.addCorsHeaders(session, request, Set.of("GET", "OPTIONS", "DELETE"), null); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/admin/AdminSettingsResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.admin; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.forms.login.freemarker.model.RealmBean; import org.keycloak.forms.login.freemarker.model.UrlBean; import org.keycloak.http.HttpRequest; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.theme.FolderTheme; import org.keycloak.theme.Theme; import org.keycloak.theme.freemarker.FreeMarkerProvider; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Predicate; /** * Renders a simple form to manage custom realm attributes. */ @JBossLog public class AdminSettingsResource { private static final File KEYCLOAK_CUSTOM_ADMIN_THEME_FOLDER; static { try { File themesFolder = new File(System.getProperty("kc.home.dir"), "themes").getCanonicalFile(); KEYCLOAK_CUSTOM_ADMIN_THEME_FOLDER = new File(themesFolder, "admin-custom/admin").getCanonicalFile(); } catch (IOException e) { throw new RuntimeException(e); } } private final KeycloakSession session; private final AuthenticationManager.AuthResult authResult; public AdminSettingsResource(KeycloakSession session, AuthenticationManager.AuthResult authResult) { this.session = session; this.authResult = authResult; } @GET public Response adminUi() throws Exception { var freeMarker = session.getProvider(FreeMarkerProvider.class); var theme = new FolderTheme(KEYCLOAK_CUSTOM_ADMIN_THEME_FOLDER, "admin-custom", Theme.Type.ADMIN); var context = session.getContext(); var realm = context.getRealm(); var attributes = new HashMap(); attributes.put("realm", new RealmBean(realm)); attributes.put("realmSettings", new RealmSettingsBean(realm)); attributes.put("properties", theme.getProperties()); var baseUri = context.getUri().getBaseUriBuilder().build(); attributes.put("url", new UrlBean(realm, theme, baseUri, null)); var htmlString = freeMarker.processTemplate(attributes, "admin-settings.ftl", theme); return Response.ok().type(MediaType.TEXT_HTML_TYPE).entity(htmlString).build(); } @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response updateAdminSettings() throws Exception { HttpRequest httpRequest = session.getContext().getHttpRequest(); var formData = httpRequest.getDecodedFormParameters(); var action = formData.getFirst("action"); if (!"save".equals(action)) { return adminUi(); } var context = session.getContext(); var realm = context.getRealm(); var realmSettings = new RealmSettingsBean(realm); var updateCount = 0; for (var setting : realmSettings.getSettings()) { var newValue = formData.getFirst(setting.getName()); var oldValue = setting.getValue(); if (!Objects.equals(newValue, oldValue)) { realm.setAttribute(setting.getName(), newValue); updateCount++; } } if (updateCount > 0) { log.infof("Realm Settings updated. realm=%s user=%s", realm.getName(), authResult.getUser().getUsername()); } return adminUi(); } @RequiredArgsConstructor public static class RealmSettingsBean { private final RealmModel realm; public Map getAttributes() { return realm.getAttributes(); } public List getSettings() { return getRawConfigSettings(setting -> { return setting.getName().startsWith("acme"); }); } private List getRawConfigSettings(Predicate filter) { var settings = new ArrayList(); for (var entry : getAttributes().entrySet()) { settings.add(new ConfigSetting(entry.getKey(), entry.getValue(), "text")); } settings.removeIf(Predicate.not(filter)); return settings; } } @Data @NoArgsConstructor @AllArgsConstructor public static class ConfigSetting { String name; String value; String type; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/admin/CustomAdminResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.admin; import com.github.thomasdarimont.keycloak.custom.endpoints.admin.UserProvisioningResource.UserProvisioningConfig; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator; import java.time.Instant; import java.util.Map; /** * Collection of custom Admin Resource Endpoints */ public class CustomAdminResource { private final KeycloakSession session; private final RealmModel realm; private final AdminPermissionEvaluator auth; private final AdminEventBuilder adminEvent; private final UserProvisioningConfig privisioningConfig; public CustomAdminResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent, UserProvisioningConfig privisioningConfig) { this.session = session; this.realm = realm; this.auth = auth; this.adminEvent = adminEvent; this.privisioningConfig = privisioningConfig; } /** * http://localhost:8080/auth/realms/acme-workshop/custom-admin-resources/example * * @return */ @Path("/example") @GET public Response getData() { if (auth.users().canView()) { return Response.status(Response.Status.FORBIDDEN).build(); } return Response.ok(Map.of("time", Instant.now())).build(); } /** * http://localhost:8080/auth/realms/acme-workshop/custom-admin-resources/users * * @return */ @Path("/users") public UserProvisioningResource provisioningResource() { return new UserProvisioningResource(session, realm, auth, adminEvent, privisioningConfig); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/admin/UserProvisioningResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.admin; import com.fasterxml.jackson.annotation.JsonInclude; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.Response; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.cache.UserCache; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.resources.admin.AdminEventBuilder; import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator; import java.time.Instant; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Custom Admin Resource for User Provisioning */ @JBossLog public class UserProvisioningResource { private final KeycloakSession session; private final RealmModel realm; private final AdminPermissionEvaluator auth; private final AdminEventBuilder adminEvent; private final UserProvisioningConfig privisioningConfig; public UserProvisioningResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent, UserProvisioningConfig privisioningConfig) { this.session = session; this.realm = realm; this.auth = auth; this.adminEvent = adminEvent; this.privisioningConfig = privisioningConfig; } /** * Supported Operations * - Manage User Attributes * * https://id.acme.test:8443/auth/admin/realms/acme-workshop/custom-admin-resources/users/provisioning * * @return */ @Path("/provisioning") @POST public Response provisionUsers(UserProvisioningRequest provisioningRequest) { if (!isAuthorized()) { return Response.status(Response.Status.FORBIDDEN).build(); } if (provisioningRequest.getUsers() == null) { return Response.status(Response.Status.BAD_REQUEST).build(); } Instant startedAt = Instant.now(); Map adminOperationRep = new LinkedHashMap<>(); adminOperationRep.put("startedAt", startedAt.toString()); try { RealmModel realm = session.getContext().getRealm(); UserProvider userProvider = session.getProvider(UserProvider.class); var provisioningContext = new UserProvisioningContext(realm, userProvider, provisioningRequest); for (UserRepresentation userRep : provisioningRequest.getUsers()) { var result = new UserProvisioningResult(); try { result.setUsername(userRep.getUsername()); provisionUser(result, userRep, provisioningContext); if (result.getError() != null) { log.debugf("Error during user provisioning. realm=%s username=%s error=%s", realm.getName(), result.getUsername(), result.getError()); } } catch (Exception ex) { result.setStatus(UserProvisioningStatus.ERROR); result.setError(UserProvisioningError.UNKNOWN); result.setErrorDetails(ex.getMessage()); } provisioningContext.addResult(result); } var response = createProvisioningResponse(provisioningContext); var errors = response.getErrors(); var updated = response.getUpdated(); adminOperationRep.put("errors", errors == null ? 0 : errors.size()); adminOperationRep.put("updates", updated == null ? 0 : updated.size()); return Response.ok().entity(response).build(); } finally { adminEvent.operation(OperationType.UPDATE) .resourcePath(session.getContext().getUri()) .representation(adminOperationRep) .success(); } } private boolean isAuthorized() { String requiredRealmRole = privisioningConfig.getRequiredRealmRole(); // ensure access token originated from master realm if (!"master".equals(auth.adminAuth().getRealm().getName())) { return false; } // ensure user has required realm role in the master realm return auth.adminAuth().hasRealmRole(requiredRealmRole); } private UserProvisioningResponse createProvisioningResponse(UserProvisioningContext provisioningContext) { // TreeSet for stable error order in map Map> errors = new TreeMap<>(); Set updated = new LinkedHashSet<>(); List results = provisioningContext.getResults(); for (var result : results) { if (result.getError() != null) { errors.computeIfAbsent(result.getError(), error -> new LinkedHashSet<>()).add(result.getUsername()); } else { updated.add(result.getUsername()); } } return new UserProvisioningResponse(updated, errors); } private void provisionUser(UserProvisioningResult result, UserRepresentation userRep, UserProvisioningContext provisioningContext) { if (userRep.getUsername() == null) { result.setUserId(userRep.getId()); result.setEmail(userRep.getEmail()); result.setStatus(UserProvisioningStatus.ERROR); result.setError(UserProvisioningError.INVALID_INPUT); result.setErrorDetails("Missing username in request!"); return; } UserProvider userProvider = provisioningContext.getUserProvider(); RealmModel realm = provisioningContext.getRealm(); UserModel user = userProvider.getUserByUsername(realm, userRep.getUsername()); if (user == null) { result.setUserId(userRep.getId()); result.setEmail(userRep.getEmail()); result.setUsername(userRep.getUsername()); result.setStatus(UserProvisioningStatus.ERROR); result.setError(UserProvisioningError.USER_NOT_FOUND); return; } UserProvisioningStatus attributeUpdateResult = updateUserAttributes(userRep, user); if (UserProvisioningStatus.UPDATED.equals(attributeUpdateResult)) { // ensure user is removed from cache to make update visible UserCache userCache = session.getProvider(UserCache.class); userCache.evict(realm, user); UserRepresentation updatedUserRep = new UserRepresentation(); updatedUserRep.setId(user.getId()); updatedUserRep.setUsername(user.getUsername()); updatedUserRep.setAttributes(userRep.getAttributes()); // generate admin event for the provisioning adminEvent.operation(OperationType.UPDATE) .resourcePath(session.getContext().getUri(), updatedUserRep.getId()) .representation(updatedUserRep).success(); } result.setUserId(user.getId()); result.setUsername(user.getUsername()); result.setEmail(user.getEmail()); result.setStatus(attributeUpdateResult); } private UserProvisioningStatus updateUserAttributes(UserRepresentation userRep, UserModel user) { Map> userRepAttributes = userRep.getAttributes(); if (userRepAttributes == null) { // no attribute updates given, skip attribute update return UserProvisioningStatus.SKIPPED; } // Handle managed attributes only Pattern managedAttributePattern = privisioningConfig.getManagedAttributePattern(); var managedAttributes = userRepAttributes.entrySet().stream() .filter(attribute -> managedAttributePattern.matcher(attribute.getKey()).matches()) .collect(Collectors.toList()); if (managedAttributes.isEmpty()) { // empty attributes given -> remove all managed attributes for (var attributeName : user.getAttributes().keySet()) { if (managedAttributePattern.matcher(attributeName).matches()) { user.removeAttribute(attributeName); } } return UserProvisioningStatus.UPDATED; } // update requested attributes // attributes with null values will be removed! for (var entry : userRepAttributes.entrySet()) { if (entry.getValue() != null) { // update value user.setAttribute(entry.getKey(), entry.getValue()); } else { // remove value user.removeAttribute(entry.getKey()); } } return UserProvisioningStatus.UPDATED; } @Data public static class UserProvisioningRequest { List users; } @Data public static class UserProvisioningContext { private final RealmModel realm; private final UserProvider userProvider; private final UserProvisioningRequest importRequest; private List results; private boolean errorFound; public UserProvisioningContext(RealmModel realm, UserProvider userProvider, UserProvisioningRequest importRequest) { this.realm = realm; this.userProvider = userProvider; this.importRequest = importRequest; this.results = new ArrayList<>(); } public void addResult(UserProvisioningResult result) { if (result.getError() != null) { errorFound = true; } this.results.add(result); } } @Data @JsonInclude(JsonInclude.Include.NON_EMPTY) public static class UserProvisioningResponse { final Set updated; final Map> errors; } public static enum UserProvisioningStatus { UPDATED, SKIPPED, ERROR } public static enum UserProvisioningError { INVALID_INPUT, USER_NOT_FOUND, UNKNOWN } @Data public static class UserProvisioningResult { String userId; String username; String email; UserProvisioningStatus status; UserProvisioningError error; String errorDetails; } @Data public static class UserProvisioningConfig { private final String requiredRealmRole; private final Pattern managedAttributePattern; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/applications/ApplicationsInfoResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.applications; import com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils; import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.models.AccountRoles; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.account.ClientRepresentation; import org.keycloak.representations.account.ConsentRepresentation; import org.keycloak.representations.account.ConsentScopeRepresentation; import org.keycloak.services.cors.Cors; import org.keycloak.services.util.ResolveRelative; import org.keycloak.theme.Theme; import jakarta.ws.rs.GET; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.io.IOException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; public class ApplicationsInfoResource { private final KeycloakSession session; private final AccessToken token; public ApplicationsInfoResource(KeycloakSession session, AccessToken token) { this.session = session; this.token = token; } @OPTIONS public Response getCorsOptions() { return withCors().add(Response.ok()); } @GET @Produces(MediaType.APPLICATION_JSON) public Response readApplicationInfo(@QueryParam("name") String clientName) { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); UserModel user = session.users().getUserById(realm, token.getSubject()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } var resourceAccess = token.getResourceAccess(); AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE)); if (!canAccessAccount) { return Response.status(FORBIDDEN).build(); } var credentialInfos = getApplicationsForUser(realm, user, clientName); var responseBody = new HashMap(); responseBody.put("clients", credentialInfos); return withCors().add(Response.ok(responseBody)); } public List getApplicationsForUser(RealmModel realm, UserModel user, String clientName) { List inUseClients = new LinkedList<>(); Set clients = session.sessions().getUserSessionsStream(realm, user) .flatMap(s -> s.getAuthenticatedClientSessions().values().stream()) .map(AuthenticatedClientSessionModel::getClient) .peek(client -> inUseClients.add(client.getClientId())).collect(Collectors.toSet()); List offlineClients = new LinkedList<>(); clients.addAll(session.sessions().getOfflineUserSessionsStream(realm, user) .flatMap(s -> s.getAuthenticatedClientSessions().values().stream()) .map(AuthenticatedClientSessionModel::getClient) .peek(client -> offlineClients.add(client.getClientId())) .collect(Collectors.toSet())); Map consentModels = new HashMap<>(); clients.addAll(session.users().getConsentsStream(realm, user.getId()) .peek(consent -> consentModels.put(consent.getClient().getClientId(), consent)) .map(UserConsentModel::getClient) .collect(Collectors.toSet())); realm.getAlwaysDisplayInConsoleClientsStream().forEach(clients::add); ClientModel accountConsole = realm.getClientByClientId(Constants.ACCOUNT_CONSOLE_CLIENT_ID); clients.add(accountConsole); Locale locale = session.getContext().resolveLocale(user); Properties messages = getAccountMessages(locale); return clients.stream().filter(client -> !client.isBearerOnly() && client.getBaseUrl() != null && !client.getClientId().isEmpty()) .filter(client -> matches(client, clientName)) .map(client -> modelToRepresentation(client, inUseClients, offlineClients, consentModels, messages)) .collect(Collectors.toList()); } private boolean matches(ClientModel client, String name) { if (name == null) { return true; } if (client.getName() == null) { return false; } return client.getName().toLowerCase().contains(name.toLowerCase()); } private ClientRepresentation modelToRepresentation(ClientModel model, List inUseClients, List offlineClients, Map consents, Properties messages) { ClientRepresentation representation = new ClientRepresentation(); representation.setClientId(model.getClientId()); representation.setClientName(StringPropertyReplacer.replaceProperties(model.getName(), messages::getProperty)); representation.setDescription(model.getDescription()); representation.setUserConsentRequired(model.isConsentRequired()); representation.setInUse(inUseClients.contains(model.getClientId())); representation.setOfflineAccess(offlineClients.contains(model.getClientId())); representation.setRootUrl(model.getRootUrl()); representation.setBaseUrl(model.getBaseUrl()); representation.setEffectiveUrl(ResolveRelative.resolveRelativeUri(session, model.getRootUrl(), model.getBaseUrl())); UserConsentModel consentModel = consents.get(model.getClientId()); if (consentModel != null) { representation.setConsent(modelToRepresentation(consentModel, messages)); } return representation; } private Properties getAccountMessages(Locale locale) { try { return session.theme().getTheme(Theme.Type.ACCOUNT).getMessages(locale); } catch (IOException e) { return null; } } private ConsentRepresentation modelToRepresentation(UserConsentModel model, Properties messages) { List grantedScopes = model.getGrantedClientScopes().stream() .map(m -> new ConsentScopeRepresentation(m.getId(), m.getName(), StringPropertyReplacer.replaceProperties(m.getConsentScreenText(), messages::getProperty))) .collect(Collectors.toList()); return new ConsentRepresentation(grantedScopes, model.getCreatedDate(), model.getLastUpdatedDate()); } private Cors withCors() { var request = session.getContext().getHttpRequest(); return CorsUtils.addCorsHeaders(session, request, Set.of("GET", "OPTIONS"), null); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/branding/BrandingResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.branding; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import java.util.Optional; public class BrandingResource { private final KeycloakSession session; public BrandingResource(KeycloakSession session) { this.session = session; } @GET @Path("css") @Consumes(MediaType.WILDCARD) @Produces("text/css") public Response getBranding() { KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); String brackgroundColor = Optional.ofNullable(realm.getAttribute("custom.branding.backgroundColor")).orElse("grey"); String css = """ .login-pf body { background-color: %s; } """.formatted(brackgroundColor); return Response.ok(css).build(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/credentials/UserCredentialsInfoResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.credentials; import com.github.thomasdarimont.keycloak.custom.account.AccountActivity; import com.github.thomasdarimont.keycloak.custom.account.MfaChange; import com.github.thomasdarimont.keycloak.custom.auth.mfa.emailcode.EmailCodeCredentialModel; import com.github.thomasdarimont.keycloak.custom.auth.mfa.sms.credentials.SmsCredentialModel; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceCookie; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.TrustedDeviceToken; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.action.TrustedDeviceInfo; import com.github.thomasdarimont.keycloak.custom.auth.trusteddevice.credentials.TrustedDeviceCredentialModel; import com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.Data; import org.keycloak.credential.CredentialModel; import org.keycloak.http.HttpRequest; import org.keycloak.models.AccountRoles; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.credential.RecoveryAuthnCodesCredentialModel; import org.keycloak.models.credential.WebAuthnCredentialModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.cors.Cors; import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; public class UserCredentialsInfoResource { private static final Set RELEVANT_CREDENTIAL_TYPES = Set.of(PasswordCredentialModel.TYPE, SmsCredentialModel.TYPE, OTPCredentialModel.TYPE, TrustedDeviceCredentialModel.TYPE, RecoveryAuthnCodesCredentialModel.TYPE, EmailCodeCredentialModel.TYPE, WebAuthnCredentialModel.TYPE_PASSWORDLESS); private static final Set REMOVABLE_CREDENTIAL_TYPES = Set.of(SmsCredentialModel.TYPE, TrustedDeviceCredentialModel.TYPE, OTPCredentialModel.TYPE, RecoveryAuthnCodesCredentialModel.TYPE, EmailCodeCredentialModel.TYPE, WebAuthnCredentialModel.TYPE_PASSWORDLESS); private final KeycloakSession session; private final AccessToken token; public UserCredentialsInfoResource(KeycloakSession session, AccessToken token) { this.session = session; this.token = token; } @OPTIONS public Response getCorsOptions() { return withCors(session.getContext().getHttpRequest()).add(Response.ok()); } @GET @Produces(MediaType.APPLICATION_JSON) public Response readCredentialInfo() { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); UserModel user = session.users().getUserById(realm, token.getSubject()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } var resourceAccess = token.getResourceAccess(); AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE)); if (!canAccessAccount) { return Response.status(FORBIDDEN).build(); } var credentialInfos = loadCredentialInfosForUser(realm, user); var responseBody = new HashMap(); responseBody.put("credentialInfos", credentialInfos); var request = context.getHttpRequest(); return withCors(request).add(Response.ok(responseBody)); } @DELETE @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response removeCredentialByType(RemoveCredentialRequest removeCredentialRequest) { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); UserModel user = session.users().getUserById(realm, token.getSubject()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } var resourceAccess = token.getResourceAccess(); AccessToken.Access accountAccess = resourceAccess == null ? null : resourceAccess.get(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); var canAccessAccount = accountAccess != null && (accountAccess.isUserInRole(AccountRoles.MANAGE_ACCOUNT) || accountAccess.isUserInRole(AccountRoles.VIEW_PROFILE)); if (!canAccessAccount) { return Response.status(FORBIDDEN).build(); } String credentialType = removeCredentialRequest.getCredentialType(); if (!REMOVABLE_CREDENTIAL_TYPES.contains(credentialType)) { return Response.status(BAD_REQUEST).build(); } String credentialId = removeCredentialRequest.getCredentialId(); // TODO check token.getAuth_time() var credentialManager = user.credentialManager(); var credentials = credentialManager.getStoredCredentialsByTypeStream(credentialType).toList(); if (credentials.isEmpty()) { var request = context.getHttpRequest(); return withCors(request).add(Response.status(Response.Status.NOT_FOUND)); } int removedCredentialCount = 0; for (var credential : credentials) { if (credentialId != null && !credential.getId().equals(credentialId)) { continue; } if (removeCredentialForUser(realm, user, credential)) { removedCredentialCount++; if (WebAuthnCredentialModel.TYPE_PASSWORDLESS.equals(credential.getType())) { AccountActivity.onUserPasskeyChanged(session, realm, user, credential, MfaChange.REMOVE); } else { AccountActivity.onUserMfaChanged(session, realm, user, credential, MfaChange.REMOVE); } } } var responseBody = new HashMap(); responseBody.put("removedCredentialCount", removedCredentialCount); var request = context.getHttpRequest(); return withCors(request).add(Response.ok(responseBody)); } private boolean removeCredentialForUser(RealmModel realm, UserModel user, CredentialModel credentialModel) { boolean removed = user.credentialManager().removeStoredCredentialById(credentialModel.getId()); if (removed && TrustedDeviceCredentialModel.TYPE.equals(credentialModel.getType()) && isCurrentRequestFromGivenTrustedDevice(credentialModel)) { // remove dangling trusted device cookie TrustedDeviceCookie.removeDeviceCookie(session, realm); AccountActivity.onTrustedDeviceChange(session, realm, user, new TrustedDeviceInfo(credentialModel.getUserLabel()), MfaChange.REMOVE); } return removed; } private Map> loadCredentialInfosForUser(RealmModel realm, UserModel user) { var credentialManager = user.credentialManager(); var credentials = credentialManager.getStoredCredentialsStream().toList(); var credentialData = new HashMap>(); for (var credential : credentials) { String type = credential.getType(); if (!RELEVANT_CREDENTIAL_TYPES.contains(type)) { continue; } credentialData.computeIfAbsent(type, s -> new ArrayList<>()).add(newCredentialInfo(credential, type)); } var credentialInfoData = new HashMap>(); for (var credentialType : credentialData.keySet()) { var creds = credentialData.get(credentialType); if (creds.size() > 1) { if (shouldAggregate(credentialType)) { CredentialInfo firstCredential = creds.get(0); CredentialInfo aggregatedCred = new CredentialInfo(null, credentialType, credentialType + " [" + creds.size() + "]", firstCredential.getCreatedAt()); aggregatedCred.setCollection(true); aggregatedCred.setCount(creds.size()); credentialInfoData.put(credentialType, List.of(aggregatedCred)); } else { credentialInfoData.put(credentialType, creds); } } else { credentialInfoData.put(credentialType, creds); } } return credentialInfoData; } private CredentialInfo newCredentialInfo(CredentialModel credential, String type) { String userLabel = credential.getUserLabel(); if (userLabel == null) { userLabel = type; } CredentialInfo credentialInfo = new CredentialInfo(credential.getId(), type, userLabel, credential.getCreatedDate()); if (TrustedDeviceCredentialModel.TYPE.equals(credential.getType())) { if (isCurrentRequestFromGivenTrustedDevice(credential)) { credentialInfo.getMetadata().put("current", "true"); } } if (RecoveryAuthnCodesCredentialModel.TYPE.equals(credential.getType())) { try { Map credentialData = JsonSerialization.readValue(credential.getCredentialData(), Map.class); credentialInfo.getMetadata().put("remainingCodes", credentialData.get("remainingCodes")); } catch (IOException e) { throw new RuntimeException(e); } } return credentialInfo; } private boolean isCurrentRequestFromGivenTrustedDevice(CredentialModel credential) { var request = session.getContext().getHttpRequest(); TrustedDeviceToken trustedDeviceToken = TrustedDeviceCookie.parseDeviceTokenFromCookie(request, session); if (trustedDeviceToken == null) { return false; } return credential.getSecretData().equals(trustedDeviceToken.getDeviceId()); } private boolean shouldAggregate(String credentialType) { return false; } private Cors withCors(HttpRequest request) { return CorsUtils.addCorsHeaders(session, request, Set.of("GET", "DELETE", "OPTIONS"), null); } @Data public static class CredentialInfo { private final String credentialId; private final String credentialType; private final String credentialLabel; private final Long createdAt; private boolean collection; private int count; private Map metadata = new HashMap<>(); } @Data public static class RemoveCredentialRequest { String credentialType; String credentialId; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/demo/DemosResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.demo; import com.github.thomasdarimont.keycloak.custom.migration.acmecred.AcmeCredentialModel; import com.github.thomasdarimont.keycloak.custom.oauth.client.OauthClientCredentialsTokenManager; import jakarta.persistence.Query; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.util.Time; import org.keycloak.component.ComponentModel; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.credential.CredentialModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.storage.UserStorageProvider; import java.util.Map; import java.util.UUID; @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class DemosResource { private final KeycloakSession session; public DemosResource(KeycloakSession session) { this.session = session; } /** * http://localhost:8080/auth/realms/acme-internal/custom-resources/demos/cached-serviceaccount-token * * @return * @throws Exception */ @Path("cached-serviceaccount-token") @GET public Response demoCachedServiceAccountToken() throws Exception { var clientTokenManager = new OauthClientCredentialsTokenManager(); clientTokenManager.setTokenUrl("https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/token"); clientTokenManager.setScope("openid profile"); clientTokenManager.setUseCache(true); clientTokenManager.setClientId("app-demo-service"); clientTokenManager.setClientSecret("secret"); SimpleHttp request = SimpleHttp.doGet("https://id.acme.test:8443/auth/realms/acme-internal/protocol/openid-connect/userinfo", session); request.auth(clientTokenManager.getToken(session)); var data = request.asJson(Map.class); return Response.ok(data).build(); } /** * http://localhost:8080/auth/realms/acme-internal/custom-resources/demos/slow-query * * @return * @throws Exception */ @Path("slow-query") @GET public Response demoSlowQuery() throws Exception { var provider = session.getProvider(JpaConnectionProvider.class); Query nativeQuery = provider.getEntityManager().createNativeQuery("SELECT pg_sleep(5)"); nativeQuery.getResultList(); return Response.ok(Map.of("executed", true)).build(); } /** * http://localhost:8080/auth/realms/acme-internal/custom-resources/demos/acme-legacy-user * * @return * @throws Exception */ @Path("acme-legacy-user") @GET public Response demoAcmeUser() throws Exception { KeycloakContext context = session.getContext(); String username = "acme-legacy"; String userId = UUID.nameUUIDFromBytes(username.getBytes()).toString(); UserModel acmeUser = session.users().addUser(context.getRealm(), userId, username, true, true); acmeUser.setEnabled(true); acmeUser.setFirstName("Arne"); acmeUser.setLastName("Legacy"); acmeUser.setEmail(username + "@acme.test"); var credModel = new CredentialModel(); credModel.setType("acme-password"); credModel.setCreatedDate(Time.currentTimeMillis()); credModel.setCredentialData(""" {"algorithm":"acme-sha1", "additionalParameters":{}} """); // passw0rd credModel.setSecretData(""" {"value":"0a66d1c3549605506df64337ece6e1953ddd09b7:mysalt", "salt":null, "additionalParameters":{}} """); var acmeModel = AcmeCredentialModel.createFromCredentialModel(credModel); CredentialModel storedCredential = acmeUser.credentialManager().createStoredCredential(acmeModel); return Response.ok(Map.of("username", username, "userId", userId)).build(); } @Path("component-provider-lookup") @GET public Response componentProviderLookupExample() throws Exception { KeycloakContext context = session.getContext(); ComponentModel componentModel = context.getRealm().getComponentsStream(context.getRealm().getId(), UserStorageProvider.class.getName()).findFirst().orElse(null); String componentId = componentModel.getId(); // String componentId = "8c309ce1-08cd-4fce-8b29-884d65603cbb"; // UserStorageProvider storageProvider = session.getComponentProvider(UserStorageProvider.class, componentId); session.getProvider(UserStorageProvider.class, componentModel); return Response.ok(Map.of("componentId", componentId)).build(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/idp/IdpApplications.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.idp; import com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils; import com.github.thomasdarimont.keycloak.custom.support.LocaleUtils; import jakarta.ws.rs.GET; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.Response; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.OAuth2Constants; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.freemarker.model.LoginBean; import org.keycloak.forms.login.freemarker.model.RealmBean; import org.keycloak.forms.login.freemarker.model.UrlBean; import org.keycloak.locale.LocaleSelectorProvider; import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.Urls; import org.keycloak.services.cors.Cors; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.RealmsResource; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.CommonClientSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.theme.Theme; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @JBossLog public class IdpApplications { private final KeycloakSession session; public IdpApplications(KeycloakSession session) { this.session = session; } @OPTIONS public Response getCorsOptions() { return withCors().add(Response.ok()); } @GET public Response applications(@QueryParam("alias") String idpProviderAlias, @QueryParam("login_hint") String loginHint) throws IOException { KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, true); if (authResult == null) { // user is not authenticated yet redirect to login return redirect(context, idpProviderAlias, loginHint); } Set defaultIgnoredClientIds = Set.of("account", "broker", "realm-management", "admin-cli", "security-admin-console", "idp-initiated"); Predicate clientFilter = client -> { if (defaultIgnoredClientIds.contains(client.getClientId())) { return false; } if (client.getBaseUrl() == null) { return false; } return true; }; Locale locale = LocaleUtils.extractLocaleWithFallbackToRealmLocale(context.getHttpRequest(), realm); UserModel user = authResult.getUser(); session.setAttribute(LocaleSelectorProvider.USER_REQUEST_LOCALE, locale.getLanguage()); Theme loginTheme = session.theme().getTheme(realm.getLoginTheme(), Theme.Type.LOGIN); RealmBean realmBean = new RealmBean(realm); LoginBean loginBean = new LoginBean(new MultivaluedHashMap<>(Map.of("username", user.getUsername()))); ApplicationsBean applicationsBean = new ApplicationsBean(realm, session, clientFilter); UrlBean urlBean = new UrlBean(realm, loginTheme, context.getUri().getBaseUri(), null); return session.getProvider(LoginFormsProvider.class) // .setAttribute("realm", realmBean) // .setAttribute("user", loginBean) // .setAttribute("application", applicationsBean) // .setAttribute("url", urlBean) // .createForm("login-applications.ftl"); } /** * Initiate a login through the Identity provider with the given providerId and loginHint. * * @param context * @param providerId * @param loginHint */ private Response redirect(KeycloakContext context, String providerId, String loginHint) { // adapted from org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator.redirect RealmModel realm = context.getRealm(); Optional idp = session.identityProviders().getAllStream() // .filter(IdentityProviderModel::isEnabled) // .filter(identityProvider -> Objects.equals(providerId, identityProvider.getAlias())) // .findFirst(); if (idp.isEmpty()) { log.warnf("Identity Provider not found or not enabled for realm. realm=%s provider=%s", realm.getName(), providerId); return Response.status(Response.Status.BAD_REQUEST).entity("invalid IdP Alias").build(); } String clientId = "idp-initiated"; ClientModel idpInitiatedClient = realm.getClientByClientId(clientId); String redirectUri = Urls.realmBase(session.getContext().getUri().getBaseUri()).path("{realm}/custom-resources/idp/applications").build(realm.getName()).toString(); AuthenticationSessionModel authSession = context.getAuthenticationSession(); if (authSession == null) { RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true); authSession = rootAuthSession.createAuthenticationSession(idpInitiatedClient); authSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name()); authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())); authSession.setRedirectUri(redirectUri); authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); } String accessCode = new ClientSessionCode<>(session, realm, authSession).getOrGenerateCode(); String tabId = authSession.getTabId(); URI location = Urls.identityProviderAuthnRequest(context.getUri().getBaseUri(), providerId, realm.getName(), accessCode, clientId, tabId, null, loginHint); Response response = Response.seeOther(location).build(); log.debugf("Redirecting to %s", providerId); return response; } private Cors withCors() { var request = session.getContext().getHttpRequest(); return CorsUtils.addCorsHeaders(session, request, Set.of("GET", "OPTIONS"), null); } @Data @RequiredArgsConstructor public static class ApplicationsBean { private final RealmModel realm; private final KeycloakSession session; private final Predicate clientFilter; public List getApplications() { return session.clients().getClientsStream(realm) // .filter(clientFilter == null ? c -> true : clientFilter) // .map(client -> { String clientId = client.getClientId(); String name = client.getName(); String description = client.getDescription(); if (description == null) { description = ""; } String icon = client.getAttribute("icon"); String clientRedirectUri = Urls.realmBase(session.getContext().getUri().getBaseUri()).path(RealmsResource.class, "getRedirect").build(realm.getName(), clientId).toString(); return new ApplicationInfo(clientId, name, description, icon, clientRedirectUri); }).toList(); } @Data @RequiredArgsConstructor public static class ApplicationInfo { private final String clientId; private final String name; private final String description; private final String icon; private final String url; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/migration/TokenMigrationResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.migration; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.ws.rs.POST; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; import lombok.Data; import lombok.RequiredArgsConstructor; import org.keycloak.OAuth2Constants; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.Urls; import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.util.TokenUtil; import java.util.Set; /** * Example for migrating an existing offline session form client-1 to a client-2 */ @RequiredArgsConstructor public class TokenMigrationResource { private static final Set ALLOWED_MIGRATION_CLIENT_ID_PAIRS = Set.of("client-1:client-2", "client-1:client-3"); private final KeycloakSession session; private final AccessToken token; @POST public Response migrateToken(Request request, TokenMigrationInput input) { // validate token (X) // validate source-client // validate target-client if (!isAllowedMigration(input)) { return Response.status(Response.Status.BAD_REQUEST).build(); } // lookup current client / user session referenced by token var sid = token.getSessionId(); RealmModel realm = session.getContext().getRealm(); String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()); UserSessionModel userSession = session.sessions().getUserSession(realm, sid); ClientModel sourceClient = session.clients().getClientByClientId(realm, token.issuedFor); ClientModel targetClient = session.clients().getClientByClientId(realm, input.getTargetClientId()); AuthenticatedClientSessionModel sourceClientAuthClientSession = userSession.getAuthenticatedClientSessionByClient(sourceClient.getId()); // propagate new target-client in session session.getContext().setClient(targetClient); // create new dedicated user session UserSessionModel newUserSession = session.sessions().createUserSession(null, realm, userSession.getUser(), userSession.getLoginUsername(), session.getContext().getConnection().getRemoteAddr(), "impersonate", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); // convert user session to offline user session newUserSession = session.sessions().createOfflineUserSession(newUserSession); for(var entry : userSession.getNotes().entrySet()) { // TODO filter notes if necessary newUserSession.setNote(entry.getKey(), entry.getValue()); } // generate new client session AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, targetClient, newUserSession); AuthenticatedClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession, newUserSession); offlineClientSession.setNote(OAuth2Constants.SCOPE, sourceClientAuthClientSession.getNote(OAuth2Constants.SCOPE)); offlineClientSession.setNote(OAuth2Constants.ISSUER, sourceClientAuthClientSession.getNote(OAuth2Constants.ISSUER)); // generate new access token response (AT+RT) with azp=target-client Set clientScope = Set.of(); ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndClientScopes(offlineClientSession, clientScope, null, session); var event = new EventBuilder(realm, session); event.detail("migration", "true"); TokenManager tokenManager = new TokenManager(); TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, this.session, newUserSession, clientSessionCtx); responseBuilder.generateAccessToken(); responseBuilder.getAccessToken().issuer(issuer); responseBuilder.getAccessToken().setScope(token.getScope()); responseBuilder.getAccessToken().issuedFor(targetClient.getClientId()); responseBuilder.generateRefreshToken(); responseBuilder.getRefreshToken().issuer(issuer); responseBuilder.getRefreshToken().setScope(token.getScope()); responseBuilder.getRefreshToken().type(TokenUtil.TOKEN_TYPE_OFFLINE); responseBuilder.getRefreshToken().issuedFor(targetClient.getClientId()); // skip generation of access token responseBuilder.accessToken(null); AccessTokenResponse accessTokenResponse = responseBuilder.build(); return Response.ok(accessTokenResponse).build(); } private boolean isAllowedMigration(TokenMigrationInput input) { return token != null && input != null && ALLOWED_MIGRATION_CLIENT_ID_PAIRS.contains(token.issuedFor + ":" + input.targetClientId); } @Data public static class TokenMigrationInput { @JsonProperty("target_client_id") String targetClientId; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/migration/UserImportMigrationResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.migration; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.AcmeUserStorageProvider; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.component.ComponentModel; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.credential.CredentialModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.representations.AccessToken; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.adapter.InMemoryUserAdapter; import org.keycloak.storage.federated.UserFederatedStorageProvider; import java.util.List; import java.util.Map; @JBossLog @RequiredArgsConstructor public class UserImportMigrationResource { private final KeycloakSession session; private final AccessToken token; /** * curl -k -v -H "Content-type: application/json" -d '{"batchSize":10000}' https://id.acme.test:8443/auth/realms/acme-user-migration/custom-resources/migration/users * * @param request * @return */ @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response migrateUsers(Request request, MigrationRequest migrationRequest) { log.infof("Migrate users..."); // UserStorageProvider storageProvider = session.getProvider(AcmeUserStorageProvider.class, AcmeUserStorageProvider.ID); KeycloakContext context = session.getContext(); UserProvider userProvider = session.users(); RealmModel realm = context.getRealm(); int batchSize = migrationRequest.batchSize(); ComponentModel customStorageProviderComponent = realm.getComponentsStream().filter(c -> AcmeUserStorageProvider.ID.equals(c.getProviderId())).toList().get(0); AcmeUserStorageProvider acmeStorageProvider = (AcmeUserStorageProvider) session.getComponentProvider(UserStorageProvider.class, customStorageProviderComponent.getProviderId()); // List federatedUsers = userProvider.searchForUserStream(realm, "*") // // .filter(u -> !StorageId.isLocalStorage(u.getId()) && "bugs".equals(u.getUsername())).toList(); // generate batch partitions // for batch partition (startIndex, pageSize) // check current tx / create new tx List federatedUsers = acmeStorageProvider.searchForUserStream(realm, Map.of(UserModel.SEARCH, "*"), 0, Integer.MAX_VALUE) // .filter(u -> u.getFirstAttribute("migrated") == null && "bugs".equals(u.getUsername())) // .toList(); EntityManager em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); Query migrateOfflineSesions = em.createNativeQuery(""" update offline_user_session ous set user_id = :localUserId where ous.realm_id = :realmId and ous.user_id = :federatedId """); for (var federatedUser : federatedUsers) { String fedUserId = federatedUser.getId(); String externalUserId = StorageId.externalId(fedUserId); UserModel localUser = userProvider.addUser(realm, externalUserId, federatedUser.getUsername(), true, true); localUser.setEnabled(federatedUser.isEnabled()); localUser.setFirstName(federatedUser.getFirstName()); localUser.setLastName(federatedUser.getLastName()); localUser.setEmail(federatedUser.getEmail()); localUser.setEmailVerified(federatedUser.isEmailVerified()); localUser.setCreatedTimestamp(federatedUser.getCreatedTimestamp()); for (var attr : federatedUser.getAttributes().entrySet()) { localUser.setAttribute(attr.getKey(), attr.getValue()); } localUser.setSingleAttribute("acmeLegacyId", fedUserId); UserFederatedStorageProvider jpaUserFederatedStorageProvider = session.getProvider(UserFederatedStorageProvider.class, "jpa"); List requiredActions = jpaUserFederatedStorageProvider.getRequiredActionsStream(realm, fedUserId).toList(); requiredActions.forEach(localUser::addRequiredAction); List federatedCreds = jpaUserFederatedStorageProvider.getStoredCredentialsStream(realm, fedUserId).toList(); //federatedUser.credentialManager().getStoredCredentialsStream() federatedCreds.forEach(cred -> { cred.setId(null); localUser.credentialManager().createStoredCredential(cred); }); List federatedIdentities = jpaUserFederatedStorageProvider.getFederatedIdentitiesStream(fedUserId, realm).toList(); // List federatedIdentities = userProvider.getFederatedIdentitiesStream(realm, federatedUser).toList(); for (FederatedIdentityModel fedId : federatedIdentities) { userProvider.addFederatedIdentity(realm, localUser, fedId); } // TODO verify migrate offline user session from federated user to new local user int updateCount = -1; // NOTE : This only moves the offline session on database level-> the old offline session is still in memory // Server / Cluster restart is needed in order to propagate new offline session information migrateOfflineSesions.setParameter("localUserId", localUser.getId()); migrateOfflineSesions.setParameter("realmId", realm.getId()); migrateOfflineSesions.setParameter("federatedId", fedUserId); updateCount = migrateOfflineSesions.executeUpdate(); log.infof("Imported local user. Old id=%s, username=%s. New id=%s, username=%s. Migrated offline sessions: %d", fedUserId, federatedUser.getUsername(), localUser.getId(), localUser.getUsername(), updateCount); // federatedUser.setSingleAttribute("migrated", "true"); // Note: dangling federated indentity links for federated user are cleared after restart jpaUserFederatedStorageProvider.preRemove(realm, new InMemoryUserAdapter(session, realm, fedUserId)); } // commit tx // end return Response.ok(Map.of("foo", "bar")).build(); } /** * curl -k -v -H "Content-type: application/json" -X DELETE -d '{}' https://id.acme.test:8443/auth/realms/acme-user-migration/custom-resources/migration/users/cache * * @param request * @return */ @Path("/cache") @DELETE @Consumes(MediaType.APPLICATION_JSON) public Response clearCache(Request request) { log.infof("Clearing offline session cache"); session.getProvider(InfinispanConnectionProvider.class) // .getCache(InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME) // .clear(); return Response.noContent().build(); } public record MigrationRequest(int batchSize) { } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/offline/OfflineSessionPropagationResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.offline; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.Time; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.representations.AccessToken; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.ResolveRelative; import java.net.URI; import java.util.Map; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; @JBossLog public class OfflineSessionPropagationResource { public static final int DEFAULT_TOKEN_VALIDITY_IN_SECONDS = 30; private final KeycloakSession session; private final AccessToken token; public OfflineSessionPropagationResource(KeycloakSession session, AccessToken token) { this.session = session; this.token = token; } /** * Generates an ActionToken to propagate the current offline session to an online session for the given target client_id. * *

     *   KC_OFFLINE_ACCESS_TOKEN="ey...."
     *   # For transient user session (session cookie)
     *   curl -k -v -H "Authorization: Bearer $KC_OFFLINE_ACCESS_TOKEN" -d "client_id=app-minispa" https://id.acme.test:8443/auth/realms/acme-internal/custom-resources/mobile/session-propagation | jq -C .
     *
     *   # For persistent user session (persistent cookie)
     *   curl -k -v -H "Authorization: Bearer $KC_OFFLINE_ACCESS_TOKEN" -d "client_id=app-minispa" -d "rememberMe=true" https://id.acme.test:8443/auth/realms/acme-internal/custom-resources/mobile/session-propagation | jq -C .
     * 
*/ @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) public Response propagateSession(@FormParam("client_id") String targetClientId, @FormParam("rememberMe") Boolean rememberMe) { // validate token if (token == null) { return Response.status(UNAUTHORIZED).build(); } var context = session.getContext(); var realm = context.getRealm(); var offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionId()); if (offlineUserSession == null) { return Response.status(UNAUTHORIZED).build(); } var sourceClientId = token.getIssuedFor(); // TODO validate sourceClientId (is source allowed to propagate session tokens?) if (targetClientId == null) { return Response.status(BAD_REQUEST).build(); } var targetClient = session.clients().getClientByClientId(realm, targetClientId); if (targetClient == null) { return Response.status(BAD_REQUEST).build(); } // TODO validate target client (is target allowed for source?) var user = offlineUserSession.getUser(); var targetUri = resolveBaseUri(targetClient); var userId = user.getId(); int absoluteExpirationInSecs = Time.currentTime() + DEFAULT_TOKEN_VALIDITY_IN_SECONDS; var actionToken = new SessionPropagationActionToken(userId, absoluteExpirationInSecs, targetClientId, targetUri.toString(), sourceClientId, rememberMe); var actionTokenString = actionToken.serialize(session, realm, context.getUri()); var uriBuilder = LoginActionsService.actionTokenProcessor(session.getContext().getUri()).queryParam(Constants.KEY, actionTokenString); var actionTokenLink = uriBuilder.build(realm.getName()).toString(); log.infof("User requested Offline-Session to User-Session propagation. realm=%s userId=%s sourceClientId=%s targetClientId=%s", realm.getName(), user.getId(), sourceClientId, targetClientId); return Response.ok(Map.of("actionLink", actionTokenLink)).build(); } private URI resolveBaseUri(ClientModel targetClient) { URI targetUri; if (targetClient.getRootUrl() != null && (targetClient.getBaseUrl() == null || targetClient.getBaseUrl().isEmpty())) { targetUri = KeycloakUriBuilder.fromUri(targetClient.getRootUrl()).build(); } else { targetUri = KeycloakUriBuilder.fromUri(ResolveRelative.resolveRelativeUri(session, targetClient.getRootUrl(), targetClient.getBaseUrl())).build(); } return targetUri; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/offline/SessionPropagationActionToken.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.offline; import com.fasterxml.jackson.annotation.JsonProperty; import org.keycloak.authentication.actiontoken.DefaultActionToken; public class SessionPropagationActionToken extends DefaultActionToken { private static final String CLAIM_PREFIX = "acme:"; public static final String TOKEN_TYPE = "acme-session-propagation"; private static final String REDIRECT_URI = CLAIM_PREFIX + "redirect-uri"; private static final String SOURCE_CLIENT_ID = CLAIM_PREFIX + "sourceClientId"; private static final String REMEMBER_ME = CLAIM_PREFIX + "rememberMe"; public SessionPropagationActionToken(String userId, int absoluteExpirationInSecs, String clientId, String redirectUri, String sourceClientId, Boolean rememberMe) { super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null); this.issuedFor = clientId; setRedirectUri(redirectUri); setSourceClientId(sourceClientId); setRememberMe(rememberMe); } /** * Required for deserialization. */ @SuppressWarnings("unused") private SessionPropagationActionToken() { } @JsonProperty(REDIRECT_URI) public String getRedirectUri() { return (String) getOtherClaims().get(REDIRECT_URI); } @JsonProperty(REDIRECT_URI) public void setRedirectUri(String redirectUri) { if (redirectUri != null) { setOtherClaims(REDIRECT_URI, redirectUri); return; } getOtherClaims().remove(REDIRECT_URI); } @JsonProperty(SOURCE_CLIENT_ID) public String getSourceClientId() { return (String) getOtherClaims().get(SOURCE_CLIENT_ID); } @JsonProperty(SOURCE_CLIENT_ID) public void setSourceClientId(String clientId) { getOtherClaims().put(SOURCE_CLIENT_ID, clientId); } @JsonProperty(REMEMBER_ME) public boolean getRememberMe() { return Boolean.parseBoolean(String.valueOf(getOtherClaims().get(REMEMBER_ME))); } @JsonProperty(REMEMBER_ME) public void setRememberMe(Boolean rememberMe) { getOtherClaims().put(REMEMBER_ME, rememberMe); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/offline/SessionPropagationActionTokenHandler.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.offline; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.TokenVerifier; import org.keycloak.authentication.actiontoken.AbstractActionTokenHandler; import org.keycloak.authentication.actiontoken.ActionTokenContext; import org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory; import org.keycloak.authentication.actiontoken.DefaultActionToken; import org.keycloak.authentication.actiontoken.TokenUtils; import org.keycloak.common.util.Time; import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.ErrorPageException; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.messages.Messages; import java.net.URI; @JBossLog @SuppressWarnings("rawtypes") @AutoService(ActionTokenHandlerFactory.class) public class SessionPropagationActionTokenHandler extends AbstractActionTokenHandler { private static final String ERROR_SESSION_PROPAGATION = "errorSessionPropagation"; public SessionPropagationActionTokenHandler() { super(SessionPropagationActionToken.TOKEN_TYPE, SessionPropagationActionToken.class, ERROR_SESSION_PROPAGATION, EventType.CLIENT_LOGIN, Errors.NOT_ALLOWED); } @Override public Response handleToken(SessionPropagationActionToken token, ActionTokenContext tokenContext) { var session = tokenContext.getSession(); var realm = tokenContext.getRealm(); var clientConnection = tokenContext.getClientConnection(); // mark token as consumed var singleUseObjectProvider = session.getProvider(SingleUseObjectProvider.class); singleUseObjectProvider.put(token.serializeKey(), token.getExp() - Time.currentTime() + 1, null); // mark token as invalidated, +1 second to account for rounding to seconds var authSession = tokenContext.getAuthenticationSession(); var authenticatedUser = authSession.getAuthenticatedUser(); var redirectUri = token.getRedirectUri(); // check for existing user session var authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, true); if (authResult != null) { if (!authenticatedUser.getId().equals(authResult.getUser().getId())) { // detected existing user session for different user, abort propagation. log.warnf("Skipped Offline-Session to User-Session propagation detected existing session for different user. realm=%s userId=%s sourceClientId=%s targetClientId=%s", authSession.getRealm().getName(), authenticatedUser.getId(), token.getSourceClientId(), token.getIssuedFor()); throw new ErrorPageException(session, authSession, Response.Status.BAD_REQUEST, Messages.DIFFERENT_USER_AUTHENTICATED, authResult.getUser().getUsername()); } // detected existing session for current user, reuse the existing session instead of creating a new one. log.infof("Skipped Offline-Session to User-Session propagation due to existing session. realm=%s userId=%s sourceClientId=%s targetClientId=%s", authSession.getRealm().getName(), authenticatedUser.getId(), token.getSourceClientId(), token.getIssuedFor()); return redirectTo(redirectUri); } // no user session found so create a new one. authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); authSession.setClientNote(OIDCLoginProtocol.ISSUER, token.getIssuer()); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, "openid"); var rememberMe = token.getRememberMe(); var userSession = session.sessions().createUserSession(null, realm, authSession.getAuthenticatedUser(), authSession.getAuthenticatedUser().getUsername(), clientConnection.getRemoteAddr(), OIDCLoginProtocol.LOGIN_PROTOCOL, rememberMe, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT); AuthenticationManager.setClientScopesInSession(session, authSession); AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, tokenContext.getUriInfo(), clientConnection); log.infof("Propagated Offline-Session to User-Session. realm=%s userId=%s sourceClientId=%s targetClientId=%s", authSession.getRealm().getName(), authenticatedUser.getId(), token.getSourceClientId(), token.getIssuedFor()); return redirectTo(redirectUri); } private Response redirectTo(String redirectUri) { return Response.temporaryRedirect(URI.create(redirectUri)).build(); } @Override @SuppressWarnings("unchecked") public TokenVerifier.Predicate[] getVerifiers(ActionTokenContext tokenContext) { // TODO add additional checks if necessary return TokenUtils.predicates(DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS); } @Override public boolean canUseTokenRepeatedly(SessionPropagationActionToken token, ActionTokenContext tokenContext) { return false; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/profile/ProfileData.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.profile; import lombok.Data; @Data public class ProfileData { String firstName; String lastName; String email; } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/profile/UserProfileResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.profile; import com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.cors.Cors; import java.util.Set; import java.util.regex.Pattern; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; public class UserProfileResource { private static final Pattern NAME_PATTERN = Pattern.compile("[\\w\\d][\\w\\d\\s]{0,64}"); private final KeycloakSession session; private final AccessToken token; public UserProfileResource(KeycloakSession session, AccessToken token) { this.session = session; this.token = token; } @OPTIONS public Response getCorsOptions() { return withCors().add(Response.ok()); } @GET @Produces(MediaType.APPLICATION_JSON) public Response readProfile() { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } ProfileData profileData = new ProfileData(); profileData.setFirstName(user.getFirstName()); profileData.setLastName(user.getLastName()); profileData.setEmail(user.getEmail()); return withCors().add(Response.ok(profileData)); } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response updateProfile(ProfileData newProfileData) { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } ProfileData currentProfileData = new ProfileData(); currentProfileData.setFirstName(user.getFirstName()); currentProfileData.setLastName(user.getLastName()); // TODO compute change between current and new profiledata String firstName = newProfileData.getFirstName(); if (firstName == null || !NAME_PATTERN.matcher(firstName).matches()) { return Response.status(Response.Status.BAD_REQUEST).build(); } user.setFirstName(firstName); String lastName = newProfileData.getLastName(); if (lastName == null || !NAME_PATTERN.matcher(lastName).matches()) { return Response.status(Response.Status.BAD_REQUEST).build(); } user.setLastName(lastName); // email update must be performed via application initiated required action return withCors().add(Response.ok(newProfileData)); } private Cors withCors() { var request = session.getContext().getHttpRequest(); return CorsUtils.addCorsHeaders(session, request, Set.of("GET", "PUT", "OPTIONS"), null); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/endpoints/settings/UserSettingsResource.java ================================================ package com.github.thomasdarimont.keycloak.custom.endpoints.settings; import com.github.thomasdarimont.keycloak.custom.endpoints.CorsUtils; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.OPTIONS; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessToken; import org.keycloak.services.cors.Cors; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED; public class UserSettingsResource { private final static String SETTINGS_KEY1 = "setting1"; private final static String SETTINGS_KEY2 = "setting2"; private final KeycloakSession session; private final AccessToken token; public UserSettingsResource(KeycloakSession session, AccessToken token) { this.session = session; this.token = token; } @OPTIONS public Response getCorsOptions() { return withCors().add(Response.ok()); } @GET @Produces(MediaType.APPLICATION_JSON) public Response readSettings() { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } Map responseBody = new HashMap<>(); String value1 = user.getFirstAttribute(SETTINGS_KEY1); responseBody.put(SETTINGS_KEY1, value1 == null ? "" : value1); String value2 = user.getFirstAttribute(SETTINGS_KEY2); responseBody.put(SETTINGS_KEY2, "true".equals(value2) ? "on" : ""); return withCors().add(Response.ok(responseBody)); } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response writeSettings(Map settings) { if (token == null) { return Response.status(UNAUTHORIZED).build(); } KeycloakContext context = session.getContext(); UserModel user = session.users().getUserByUsername(context.getRealm(), token.getPreferredUsername()); if (user == null) { return Response.status(UNAUTHORIZED).build(); } String value1 = settings.containsKey(SETTINGS_KEY1) ? String.valueOf(settings.get(SETTINGS_KEY1)) : null; if (value1 != null && !Pattern.matches("[\\w\\d\\s]{0,32}", value1)) { return Response.status(Response.Status.BAD_REQUEST).build(); } user.setSingleAttribute(SETTINGS_KEY1, Objects.requireNonNullElse(value1, "")); String value2 = settings.containsKey(SETTINGS_KEY2) ? String.valueOf(settings.get(SETTINGS_KEY2)) : null; if (value2 != null) { user.setSingleAttribute(SETTINGS_KEY2, "" + Boolean.parseBoolean(value2)); } Map responseBody = new HashMap<>(); return withCors().add(Response.ok(responseBody)); } private Cors withCors() { var request = session.getContext().getHttpRequest(); return CorsUtils.addCorsHeaders(session, request, Set.of("GET", "PUT", "OPTIONS"), null); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/AcmeEventPublisherEventListener.java ================================================ package com.github.thomasdarimont.keycloak.custom.eventpublishing; import com.google.auto.service.AutoService; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProviderFactory; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ServerInfoAwareProviderFactory; import java.util.Map; @JBossLog @RequiredArgsConstructor public class AcmeEventPublisherEventListener implements EventListenerProvider { public static final String ID = "acme-event-publisher"; private final KeycloakSession session; private final EventPublisher publisher; @Override public void onEvent(Event event) { publisher.publish("acme.iam.keycloak.user", enrichUserEvent(event)); } private Object enrichUserEvent(Event event) { return event; } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { publisher.publish("acme.iam.keycloak.admin", enrichAdminEvent(event, includeRepresentation)); } private Object enrichAdminEvent(AdminEvent event, boolean includeRepresentation) { return event; } @Override public void close() { // NOOP } @AutoService(EventListenerProviderFactory.class) public static class Factory implements EventListenerProviderFactory, ServerInfoAwareProviderFactory { private EventPublisher publisher; @Override public String getId() { return ID; } @Override // return singleton instance, create new AcmeAuditListener(session) or use lazy initialization public EventListenerProvider create(KeycloakSession session) { return new AcmeEventPublisherEventListener(session, publisher); } @Override public void init(Config.Scope config) { /* configure factory */ try { publisher = createNatsPublisher(config); } catch (Exception ex) { log.warnf("Could not create nats publisher: %s", ex.getMessage()); publisher = new NoopPublisher(); } } private NatsEventPublisher createNatsPublisher(Config.Scope config) { String url = config.get("nats-url", "nats://acme-nats:4222"); String username = config.get("nats-username", "keycloak"); String password = config.get("nats-password", "keycloak"); var nats = new NatsEventPublisher(url, username, password); nats.init(); log.info("Created new NatsPublisher"); return nats; } @Override // we could init our provider with information from other providers public void postInit(KeycloakSessionFactory factory) { /* post-process factory */ } @Override // close resources if necessary public void close() { if (publisher != null) { publisher.close(); } } @Override public Map getOperationalInfo() { return publisher.getOperationalInfo(); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/EventPublisher.java ================================================ package com.github.thomasdarimont.keycloak.custom.eventpublishing; import java.util.Map; public interface EventPublisher { void publish(String topic, Object event); Map getOperationalInfo(); void init(); void close(); } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NatsEventPublisher.java ================================================ package com.github.thomasdarimont.keycloak.custom.eventpublishing; import io.nats.client.Connection; import io.nats.client.Nats; import io.nats.client.Options; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.Map; @JBossLog @RequiredArgsConstructor public class NatsEventPublisher implements EventPublisher { private final String url; private final String username; private final String password; private Connection connection; public void publish(String subject, Object event) { byte[] messageBytes = null; try { messageBytes = JsonSerialization.writeValueAsBytes(event); } catch (IOException e) { log.warn("Could not serialize event", e); } if (messageBytes == null) { return; } try { connection.publish(subject, messageBytes); } catch (Exception e) { log.warn("Could not publish event", e); } } public Map getOperationalInfo() { return Map.of("url", url, "nats-username", username, "status", getStatus()); } public String getStatus() { if (connection == null) { return null; } return connection.getStatus().name(); } public void init() { Options options = Options.builder() // .connectionName("keycloak") // .userInfo(username, password) // .server(url) // .build(); try { connection = Nats.connect(options); } catch (Exception e) { throw new RuntimeException("Could not connect to nats server", e); } } public void close() { if (connection == null) { return; } try { connection.close(); } catch (Exception e) { log.warn("Could not close connection", e); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/eventpublishing/NoopPublisher.java ================================================ package com.github.thomasdarimont.keycloak.custom.eventpublishing; import lombok.extern.jbosslog.JBossLog; import java.util.Map; @JBossLog public class NoopPublisher implements EventPublisher{ @Override public void publish(String topic, Object event) { // NOOP } @Override public Map getOperationalInfo() { return Map.of(); } @Override public void init() { // NOOP } @Override public void close() { // NOOP } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/health/CustomHealthChecks.java ================================================ package com.github.thomasdarimont.keycloak.custom.health; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.spi.CDI; import lombok.extern.jbosslog.JBossLog; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.HealthCheckResponseBuilder; import org.eclipse.microprofile.health.Liveness; import org.eclipse.microprofile.health.Readiness; import org.keycloak.common.Version; import javax.annotation.Resource; import javax.sql.DataSource; import java.lang.management.ManagementFactory; import java.sql.Connection; import java.time.Instant; /** * Example for custom health checks * *

Keycloak.X (with custom http-relative-path=/auth

* Example Keycloak.X health checks */ @JBossLog @ApplicationScoped public class CustomHealthChecks { private static final HealthCheckResponseBuilder KEYCLOAK_SERVER_HEALTH_CHECK = // HealthCheckResponse.named("keycloak:server") // .withData("version", Version.VERSION) // .withData("startupTime", Instant.ofEpochMilli(ManagementFactory.getRuntimeMXBean().getStartTime()).toString()); /** * Example Keycloak.X liveness check * * @return */ @Produces @Liveness HealthCheck serverCheck() { return () -> { log.debug("Liveness check"); return KEYCLOAK_SERVER_HEALTH_CHECK.up().build(); }; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/health/CustomReadinessCheck.java ================================================ package com.github.thomasdarimont.keycloak.custom.health; import jakarta.enterprise.context.ApplicationScoped; import lombok.extern.jbosslog.JBossLog; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.Readiness; import java.util.concurrent.atomic.AtomicBoolean; @Readiness @JBossLog @ApplicationScoped public class CustomReadinessCheck implements HealthCheck { private static final AtomicBoolean STOPPED = new AtomicBoolean(false); @Override public HealthCheckResponse call() { return STOPPED.get() ? HealthCheckResponse.down("CONTAINER_STATUS") : HealthCheckResponse.up("CONTAINER_STATUS"); } static { log.info("Adding CustomReadinessCheck for SIG TERM"); // SignalHandler signalHandler = sig -> { // log.infof("Detected SIG %s, marking this instance as unavailable", sig.getName()); // STOPPED.set(true); // }; // try { // Signal.handle(new Signal("TERM"), signalHandler); // } catch (Exception e) { // log.warnf("Failed to register signal handler: ", e.getMessage()); // } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/azure/CustomAzureADGroupMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.idp.azure; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.auto.service.AutoService; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.jbosslog.JBossLog; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.oidc.mappers.AbstractClaimMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.http.simple.SimpleHttp; import org.keycloak.http.simple.SimpleHttpRequest; import org.keycloak.models.GroupModel; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.JsonWebToken; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; /** * EntraID Group Mapper */ @JBossLog @AutoService(IdentityProviderMapper.class) public class CustomAzureADGroupMapper extends AbstractClaimMapper { public static final String[] COMPATIBLE_PROVIDERS = {OIDCIdentityProviderFactory.PROVIDER_ID}; private static final List configProperties; static { var properties = new ArrayList(); // var claimsProperty = new ProviderConfigProperty(); // claimsProperty.setName(CLAIM_PROPERTY_NAME); // claimsProperty.setLabel("Claims"); // claimsProperty.setHelpText("Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)"); // claimsProperty.setType(ProviderConfigProperty.MAP_TYPE); // configProperties.add(claimsProperty); configProperties = Collections.unmodifiableList(properties); } public static final String PROVIDER_ID = "oidc-aad-groups-idp-mapper"; @Override public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { return true; } @Override public List getConfigProperties() { return configProperties; } @Override public String getId() { return PROVIDER_ID; } @Override public String[] getCompatibleProviders() { return COMPATIBLE_PROVIDERS; } @Override public String getDisplayCategory() { return "Group Importer"; } @Override public String getDisplayType() { return "Acme: AAD Groups claim to Group"; } @Override public String getHelpText() { return "Assign the user to the specified group."; } @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { updateGroupsIfNecessary(session, realm, user, mapperModel, context); } @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { updateGroupsIfNecessary(session, realm, user, mapperModel, context); } private void updateGroupsIfNecessary(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String aadAccessToken = ((AccessTokenResponse) context.getContextData().get(OIDCIdentityProvider.FEDERATED_ACCESS_TOKEN_RESPONSE)).getToken(); JsonWebToken aadIdToken = (JsonWebToken) context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN); if (aadIdToken == null) { log.errorf("Could not find validated AAD IDToken"); return; } if (aadIdToken.getOtherClaims() == null) { log.errorf("Could not find additional claims in AAD IDToken"); return; } // extract group ids from AAD ID-Token @SuppressWarnings("unchecked") List assignedGroupIds = (List) aadIdToken.getOtherClaims().get("groups"); if (assignedGroupIds == null || assignedGroupIds.isEmpty()) { log.debugf("Could not find groups claim in AAD IDToken"); return; } // TODO check if current user already has all assigned membership, in this case spare the graph api call // fetch available AAD groups via MS Graph API (and cache) // TODO add support for caching MS Graph API Responses AADGroupList aadGroupList = fetchGroupListFromMsGraphApi(session, aadAccessToken); if (aadGroupList == null) { return; } List aadAssignedGroups = aadGroupList.getEntries().stream().filter(g -> { String groupId = g.getId(); return assignedGroupIds.contains(groupId); }).toList(); for (AADGroupInfo aadGroup : aadAssignedGroups) { var aadGroupId = aadGroup.getId(); var groupName = aadGroup.getDisplayName(); var description = aadGroup.getDescription(); Optional maybeLocalGroup = realm.getGroupsStream() // .filter(g -> g.getName().equals(groupName)) // .findAny(); GroupModel localGroup = maybeLocalGroup.map(existingGroup -> { existingGroup.setSingleAttribute("description", description); existingGroup.setSingleAttribute("aadGroupId", aadGroupId); return existingGroup; }).orElseGet(() -> { GroupModel newGroup = session.groups().createGroup(realm, groupName); newGroup.setSingleAttribute("description", description); newGroup.setSingleAttribute("aadGroupId", aadGroupId); return newGroup; }); // let user join assigned groups if necessary if (!user.isMemberOf(localGroup)) { user.joinGroup(localGroup); } // TODO add ability to remove user from groups not listed in AAD Groups } } private AADGroupList fetchGroupListFromMsGraphApi(KeycloakSession session, String aadAccessToken) { AADGroupList aadGroupList = null; SimpleHttpRequest groupsListingRequest = queryMsGraphApi(session, aadAccessToken, "/groups"); try (var response = groupsListingRequest.asResponse()) { if (response.getStatus() == 200) { aadGroupList = response.asJson(AADGroupList.class); } else { log.warnf("Failed to fetch groups via MS Graph API. Response: %s", response.getStatus()); } } catch (Exception ex) { log.warnf(ex, "Failed to fetch groups via MS Graph API"); } return aadGroupList; } private SimpleHttpRequest queryMsGraphApi(KeycloakSession session, String aadAccessToken, String requestPath) { var url = "https://graph.microsoft.com/v1.0" + requestPath; var request = SimpleHttp.create(session).doGet(url); request.auth(aadAccessToken); return request; } @Data public static class AADData { Map data = new HashMap<>(); @JsonAnySetter public void setData(String key, Object value) { data.put(key, value); } } @Data @EqualsAndHashCode(callSuper = true) public static class AADGroupInfo extends AADData { String id; String displayName; String description; } @Data @EqualsAndHashCode(callSuper = true) public static class AADGroupList extends AADData { @JsonProperty("value") List entries = new ArrayList<>(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/azure/CustomEntraIdProfileMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.idp.azure; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.google.auto.service.AutoService; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.oidc.mappers.AbstractClaimMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.http.simple.SimpleHttp; import org.keycloak.http.simple.SimpleHttpRequest; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.JsonWebToken; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * EntraID Profile Mapper */ @JBossLog @AutoService(IdentityProviderMapper.class) public class CustomEntraIdProfileMapper extends AbstractClaimMapper { public static final String[] COMPATIBLE_PROVIDERS = {OIDCIdentityProviderFactory.PROVIDER_ID}; private static final List configProperties; static { var properties = new ArrayList(); // var claimsProperty = new ProviderConfigProperty(); // claimsProperty.setName(CLAIM_PROPERTY_NAME); // claimsProperty.setLabel("Claims"); // claimsProperty.setHelpText("Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)"); // claimsProperty.setType(ProviderConfigProperty.MAP_TYPE); // configProperties.add(claimsProperty); configProperties = Collections.unmodifiableList(properties); } public static final String PROVIDER_ID = "oidc-aad-profile-idp-mapper"; @Override public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { return true; } @Override public List getConfigProperties() { return configProperties; } @Override public String getId() { return PROVIDER_ID; } @Override public String[] getCompatibleProviders() { return COMPATIBLE_PROVIDERS; } @Override public String getDisplayCategory() { return "Profile"; } @Override public String getDisplayType() { return "Acme: EntraID Profile Mapper"; } @Override public String getHelpText() { return "Fetch additional profile from EntraID."; } @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { updateProfileIfNecessary(session, realm, user, mapperModel, context); } @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { updateProfileIfNecessary(session, realm, user, mapperModel, context); } private void updateProfileIfNecessary(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { JsonWebToken aadIdToken = (JsonWebToken) context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN); if (aadIdToken == null) { log.errorf("Could not find validated AAD IDToken"); return; } if (aadIdToken.getOtherClaims() == null) { log.errorf("Could not find additional claims in AAD IDToken"); return; } // log.info("Fetching profile..."); // String aadAccessToken = ((AccessTokenResponse) context.getContextData().get(OIDCIdentityProvider.FEDERATED_ACCESS_TOKEN_RESPONSE)).getToken(); // GraphApiData graphApiData = fetchProfileFromMsGraphApi(session, aadAccessToken); // log.infof("Fetched profile successfully."); // // updatePhoneInformation(user, graphApiData); updateLocaleInformation(realm, user, context); } protected void updateLocaleInformation(RealmModel realm, UserModel user, BrokeredIdentityContext context) { var idToken = (JsonWebToken) context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN); String userPreferedLang = (String)idToken.getOtherClaims().get("xms_pl"); if (userPreferedLang != null) { user.setSingleAttribute("locale", userPreferedLang); return; } String tenantPreferedLang = (String)idToken.getOtherClaims().get("xms_tpl"); if (tenantPreferedLang != null) { user.setSingleAttribute("locale", tenantPreferedLang); return; } // fallback to default user.setSingleAttribute("locale", realm.getDefaultLocale()); } protected void updatePhoneInformation(UserModel user, GraphApiData graphApiData) { for (var phone : graphApiData.getPhones()) { switch (phone.getType()) { case "mobile": user.setSingleAttribute("phone_number", phone.getNumber()); user.setSingleAttribute("phone_number_verified", "true"); break; case "business": user.setSingleAttribute("business_phone_number", phone.getNumber()); user.setSingleAttribute("business_phone_number_verified", "true"); break; } } } private GraphApiData fetchProfileFromMsGraphApi(KeycloakSession session, String aadAccessToken) { GraphApiData graphApiData = null; var groupsListingRequest = queryMsGraphApi(session, aadAccessToken, "/beta/me/profile/"); try (var response = groupsListingRequest.asResponse()) { if (response.getStatus() == 200) { graphApiData = response.asJson(GraphApiData.class); } else { log.warnf("Failed to fetch MS Graph API. Response: %s", response.getStatus()); } } catch (Exception ex) { log.warnf(ex, "Failed to fetch MS Graph API"); } return graphApiData; } private SimpleHttpRequest queryMsGraphApi(KeycloakSession session, String aadAccessToken, String requestPath) { var url = "https://graph.microsoft.com" + requestPath; var request = SimpleHttp.create(session).doGet(url); request.auth(aadAccessToken); return request; } @Data public static class GraphApiData { Map data = new HashMap<>(); List phones = new ArrayList<>(); @JsonAnySetter public void setData(String key, Object value) { data.put(key, value); } } @Data public static class EntraIdPhone { String type; String number; Map data = new HashMap<>(); @JsonAnySetter public void setData(String key, Object value) { data.put(key, value); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/brokering/RestrictBrokeredUserMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.idp.brokering; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.provider.AbstractIdentityProviderMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.services.messages.Messages; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.Collections; import java.util.List; @JBossLog @AutoService(IdentityProviderMapper.class) public class RestrictBrokeredUserMapper extends AbstractIdentityProviderMapper { public static final String PROVIDER_ID = "oidc-restrict-brokered-user"; public static final String[] COMPATIBLE_PROVIDERS = {OIDCIdentityProviderFactory.PROVIDER_ID, KeycloakOIDCIdentityProviderFactory.PROVIDER_ID}; private static final List configProperties; static { var properties = new ArrayList(); // var claimsProperty = new ProviderConfigProperty(); // claimsProperty.setName(CLAIM_PROPERTY_NAME); // claimsProperty.setLabel("Claims"); // claimsProperty.setHelpText("Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)"); // claimsProperty.setType(ProviderConfigProperty.MAP_TYPE); // configProperties.add(claimsProperty); configProperties = Collections.unmodifiableList(properties); } @Override public String getId() { return PROVIDER_ID; } @Override public String[] getCompatibleProviders() { return COMPATIBLE_PROVIDERS.clone(); } @Override public String getDisplayCategory() { return "Preprocessor"; } @Override public String getDisplayType() { return "Acme: Restrict Brokered User"; } @Override public String getHelpText() { return "Only allow LDAP federated user to login via IdP Brokering."; } @Override public List getConfigProperties() { return List.copyOf(configProperties); } @Override public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { return true; } public IdentityProviderMapper create(KeycloakSession session) { return this; } @Override public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String brokerUsername = context.getUsername(); // check if user can be found via existing user federation UserModel user = session.users().getUserByUsername(realm, brokerUsername); if (user == null) { // Aborted identity brokering because user was not found in this realm log.infof("User is not allowed to access this realm. realm=%s username=%s", realm.getName(), brokerUsername); // user could not be found via user federation, so we reject the user throw new WebApplicationException(createErrorPageResponse(session, brokerUsername)); } } private static Response createErrorPageResponse(KeycloakSession session, String attemptedUsername) { var form = session.getProvider(LoginFormsProvider.class); form.setError(Messages.ACCESS_DENIED); form.setInfo("userNotAllowedToAccess", attemptedUsername); return form.createErrorPage(Response.Status.FORBIDDEN); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/linking/AcmeIdpLinkAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.idp.linking; import java.util.Set; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.broker.provider.IdpLinkAction; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.sessions.AuthenticationSessionModel; /** * Custom IdpLinkAction that allows app initiated IdP linking for selected clients. */ // @AutoService(RequiredActionFactory.class) public class AcmeIdpLinkAction extends IdpLinkAction { @Override public String getDisplayText() { return "Acme: Linking Identity Provider"; } @Override public void requiredActionChallenge(RequiredActionContext context) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); UserModel user = context.getUser(); ClientModel client = authSession.getClient(); EventBuilder event = context.getEvent().clone(); event.event(EventType.FEDERATED_IDENTITY_LINK); String identityProviderAlias = authSession.getClientNote(Constants.KC_ACTION_PARAMETER); if (identityProviderAlias == null) { event.error(Errors.UNKNOWN_IDENTITY_PROVIDER); context.ignore(); return; } event.detail(Details.IDENTITY_PROVIDER, identityProviderAlias); IdentityProviderModel identityProviderModel = session.identityProviders().getByAlias(identityProviderAlias); if (identityProviderModel == null) { event.error(Errors.UNKNOWN_IDENTITY_PROVIDER); context.ignore(); return; } boolean forceAllowAccountLinking = isAllowAccountLinkingForcedFor(realm, client, user, identityProviderModel); if (!forceAllowAccountLinking) { // Check role ClientModel accountService = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); RoleModel manageAccountRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT); if (!user.hasRole(manageAccountRole) || !client.hasScope(manageAccountRole)) { RoleModel linkRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT_LINKS); if (!user.hasRole(linkRole) || !client.hasScope(linkRole)) { event.error(Errors.NOT_ALLOWED); context.ignore(); return; } } } String idpDisplayName = KeycloakModelUtils.getIdentityProviderDisplayName(session, identityProviderModel); Response challenge = context.form() .setAttribute("idpDisplayName", idpDisplayName) .createForm("link-idp-action.ftl"); context.challenge(challenge); } protected boolean isAllowAccountLinkingForcedFor(RealmModel realm, ClientModel client, UserModel user, IdentityProviderModel targetIdp) { // your custom logic here return "company-apps".equals(realm.getName()) && Set.of("special-client").contains(client.getClientId()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/oidc/AcmeOidcIdentityProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.idp.oidc; import com.google.auto.service.AutoService; import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.JsonWebToken; import java.io.IOException; /** * PoC for a custom {@link OidcIdentityProvider} that uses an OID claim to link user accounts. */ public class AcmeOidcIdentityProvider extends OIDCIdentityProvider { public AcmeOidcIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) { super(session, config); } @Override protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { String sub = idToken.getId(); String oid = (String)idToken.getOtherClaims().get("oid"); idToken.setSubject(oid); return super.extractIdentity(tokenResponse, accessToken, idToken); } // @AutoService(IdentityProviderFactory.class) public static class Factory extends OIDCIdentityProviderFactory { @Override public OIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) { return new AcmeOidcIdentityProvider(session, new OIDCIdentityProviderConfig(model)); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/idp/social/linkedin/LinkedInUserProfileImportIdpMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.idp.social.linkedin; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.broker.provider.AbstractIdentityProviderMapper; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.social.linkedin.LinkedInOIDCIdentityProviderFactory; import org.keycloak.utils.KeycloakSessionUtil; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; /** * Imports additional linkedin user profile data such as profile picture etc. */ @JBossLog @AutoService(IdentityProviderMapper.class) public class LinkedInUserProfileImportIdpMapper extends AbstractIdentityProviderMapper { private static final String[] COMPATIBLE_PROVIDERS = {LinkedInOIDCIdentityProviderFactory.PROVIDER_ID}; @Override public String getId() { return "acme-idp-mapper-linkedin-user-importer"; } @Override public String[] getCompatibleProviders() { return COMPATIBLE_PROVIDERS.clone(); } @Override public String getDisplayCategory() { return "Attribute Importer"; } @Override public String getDisplayType() { return "Acme: LinkedIn User Importer"; } @Override public String getHelpText() { return "Imports linkedin user profile data"; } @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { log.debugf("Create User based on linkedin profile data. realm=%s userId=%s", realm.getName(), user.getId()); updateUser(realm, user, context, Action.CREATE); } @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { log.debugf("Update User based on linkedin profile data. realm=%s userId=%s", realm.getName(), user.getId()); updateUser(realm, user, context, Action.UPDATE); } enum Action { CREATE, UPDATE } private void updateUser(RealmModel realm, UserModel user, BrokeredIdentityContext context, Action action) { Map contextData = context.getContextData(); if (contextData == null) { return; } ObjectNode userInfo = (ObjectNode) contextData.get("UserInfo"); if (userInfo == null) { return; } try { JsonNode pictureEl = userInfo.get("picture"); if (pictureEl != null) { String profilePictureUrl = pictureEl.asText(); user.setSingleAttribute("picture", profilePictureUrl); } } catch (Exception ex) { log.warnf("Could not extract user profile picture from linkedin profile data. realm=%s userId=%s error=%s", realm.getName(), user.getId(), ex.getMessage()); } } @Override public List getConfigProperties() { return Collections.emptyList(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/infinispan/CustomInfinispanUserSessionProviderFactory.java ================================================ package com.github.thomasdarimont.keycloak.custom.infinispan; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.common.util.MultiSiteUtils; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.concurrent.ArrayBlockingQueue; @JBossLog //@AutoService(UserSessionProviderFactory.class) public class CustomInfinispanUserSessionProviderFactory extends InfinispanUserSessionProviderFactory { @Override public UserSessionProvider create(KeycloakSession session) { UserSessionProvider target = super.create(session); return UserSessionProvider.class.cast(Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{UserSessionProvider.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("getUserSession") && args.length == 2 && /* find the proper parameter types */ true) { // ddd } return method.invoke(target, args); } })); } // @Override // public void init(Config.Scope config) { // super.init(config); // log.infof("### Using patched InfinispanUserSessionProviderFactory"); // boolean useBatches = config.getBoolean(CONFIG_USE_BATCHES, /*DEFAULT_USE_BATCHES*/ true) && MultiSiteUtils.isPersistentSessionsEnabled(); // if (useBatches) { // // -Dkeycloak.infinispan.asyncQueuePersistentUpdateSize=5000 // int queueSize = Integer.getInteger("keycloak.infinispan.asyncQueuePersistentUpdateSize", 1000 /* default */); // var q = new ArrayBlockingQueue<>(queueSize); // // try { // // VarHandle asyncQueuePersistentUpdateHandle = MethodHandles // .privateLookupIn(InfinispanUserSessionProviderFactory.class, MethodHandles.lookup()) // .findVarHandle(InfinispanUserSessionProviderFactory.class, "asyncQueuePersistentUpdate", ArrayBlockingQueue.class); // asyncQueuePersistentUpdateHandle.set(this, q); // // log.infof("### Using patched InfinispanUserSessionProviderFactory with asyncQueuePersistentUpdateSize=%s", queueSize); // } catch (Exception e) { // log.warn("### Could not patch InfinispanUserSessionProviderFactory", e); // } // } // } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/jpa/CustomQuarkusJpaConnectionProviderFactory.java ================================================ package com.github.thomasdarimont.keycloak.custom.jpa; import com.google.auto.service.AutoService; import jakarta.persistence.EntityManagerFactory; import lombok.extern.jbosslog.JBossLog; import org.keycloak.connections.jpa.JpaConnectionProviderFactory; import org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaConnectionProviderFactory; import java.util.HashMap; import java.util.Map; @JBossLog @AutoService(JpaConnectionProviderFactory.class) public class CustomQuarkusJpaConnectionProviderFactory extends QuarkusJpaConnectionProviderFactory { private static final Map TUNING_PROPERTIES; static { log.info("### Using custom Quarkus JPA Connection factory"); Map props = new HashMap<>(); // props.put("hibernate.generate_statistics", "true"); if (!props.isEmpty()) { log.infof("### Apply additional hibernate tuning properties: %s", props); } TUNING_PROPERTIES = props; } @Override protected EntityManagerFactory getEntityManagerFactory() { EntityManagerFactory emf = super.getEntityManagerFactory(); if (TUNING_PROPERTIES.isEmpty()) { emf.getProperties().putAll(TUNING_PROPERTIES); } return emf; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetric.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor public class KeycloakMetric { private final String name; private final String description; private final KeycloakMetrics.Level level; } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetricAccessor.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics; public interface KeycloakMetricAccessor { Double getMetricValue(String metricKey); } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetricStore.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics; import com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MetricUpdateValue; import com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MultiMetricUpdateValues; import com.google.common.base.Stopwatch; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import lombok.extern.jbosslog.JBossLog; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.utils.KeycloakModelUtils; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; /** * Store for dynamically computed custom metrics. * The metrics collection only happens after a configured refresh interval to minimize overhead. */ @JBossLog public class KeycloakMetricStore implements KeycloakMetricAccessor { // TODO read value from configuration private static final int CUSTOM_METRICS_REFRESH_INTERVAL_MILLIS = Integer.getInteger("keycloak.metrics.refresh_interval_millis", 5000); private final KeycloakSessionFactory sessionFactory; private final MeterRegistry meterRegistry; private final RealmMetricsUpdater realmMetricsUpdater; private volatile long lastUpdateTimestamp; private Map metricData; public KeycloakMetricStore(KeycloakSessionFactory sessionFactory, MeterRegistry meterRegistry, RealmMetricsUpdater realmMetricsUpdater) { this.sessionFactory = sessionFactory; this.meterRegistry = meterRegistry; this.realmMetricsUpdater = realmMetricsUpdater; } public Double getMetricValue(String metricKey) { refreshMetricsIfNecessary(); Map metricData = this.metricData; if (metricData == null) { return -1.0; } Double count = metricData.get(metricKey); if (count != null) { return count; } // metric no longer present // MetricID metricID = toMetricId(meterId); // boolean removed = meterRegistry.remove(metricID); return -1.0; } private boolean isRefreshNecessary() { if (metricData == null) { return true; } long millisSinceLastUpdate = System.currentTimeMillis() - lastUpdateTimestamp; return millisSinceLastUpdate > CUSTOM_METRICS_REFRESH_INTERVAL_MILLIS; } private void refreshMetricsIfNecessary() { if (!isRefreshNecessary()) { return; } synchronized (this) { if (!isRefreshNecessary()) { return; } this.metricData = refreshMetrics(); this.lastUpdateTimestamp = System.currentTimeMillis(); } } private Map refreshMetrics() { log.trace("Begin collecting custom metrics"); Stopwatch stopwatch = Stopwatch.createStarted(); Map metricBuffer = new HashMap<>(); KeycloakModelUtils.runJobInTransaction(sessionFactory, session -> { // depending on the number of realms this might be expensive! collectCustomRealmMetricsIntoBuffer(session, metricBuffer); }); long lastUpdateDurationMillis = stopwatch.elapsed().toMillis(); log.debugf("metrics refresh took %sms", lastUpdateDurationMillis); metricBuffer.put(KeycloakMetrics.INSTANCE_METRICS_REFRESH.getName(), (double) lastUpdateDurationMillis); log.trace("Finished collecting custom metrics."); return metricBuffer; } private void collectCustomRealmMetricsIntoBuffer(KeycloakSession session, Map metricsBuffer) { RealmMetricUpdater metricUpdater = (metric, value, realm) -> { if (value == null) { // skip recording empty values return; } if (value instanceof MultiMetricUpdateValues) { Map tagsToMetrics = ((MultiMetricUpdateValues) value).getValue(); Tags realmTags = realm == null ? Tags.empty() : Tags.of("realm", realm.getName()); for (var entry : tagsToMetrics.entrySet()) { Tags tags = entry.getKey(); Number val = entry.getValue(); var metricTags = Tags.concat(realmTags, tags); String metricKey = registerCustomMetricIfMissing(metric, metricTags); Double metricValue = val.doubleValue(); metricsBuffer.put(metricKey, metricValue); } } else if (value instanceof MetricUpdateValue) { Tags tags = realm == null ? Tags.empty() : Tags.of("realm", realm.getName()); String metricKey = registerCustomMetricIfMissing(metric, tags); @SuppressWarnings("unchecked") Double metricValue = ((MetricUpdateValue) value).getValue().doubleValue(); metricsBuffer.put(metricKey, metricValue); } }; realmMetricsUpdater.updateGlobalMetrics(session, metricUpdater, lastUpdateTimestamp); session.realms().getRealmsStream().forEach(realm -> { realmMetricsUpdater.updateRealmMetrics(session, metricUpdater, realm, lastUpdateTimestamp); }); } private String registerCustomMetricIfMissing(KeycloakMetric metric, Tags tags) { // using a string like metric_name{tag1=value1,tag2=value2} is smaller than MetricID String metricKey = toMetricKey(metric.getName(), tags); // avoid duplicate metric registration Gauge gauge = meterRegistry.find(metric.getName()).tags(tags).gauge(); boolean metricPresent = gauge != null; if (metricPresent) { return metricKey; } Gauge.builder(metric.getName(), () -> getMetricValue(metricKey)) // .description(metric.getDescription()) // .tags(tags) // .register(meterRegistry); return metricKey; } private static String toMetricKey(String metricName, Tags tags) { // TreeMap for stable tag order -> stable metricKey strings Map tagMap = new TreeMap<>(); for (Tag tag : tags) { tagMap.put(tag.getKey(), tag.getValue()); } return metricName + tagMap; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/KeycloakMetrics.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics; import com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MetricUpdateValue; import com.github.thomasdarimont.keycloak.custom.metrics.RealmMetricUpdater.MultiMetricUpdateValues; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.hibernate.jpa.AvailableHints; import org.hibernate.jpa.QueryHints; import org.keycloak.common.Version; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @JBossLog public class KeycloakMetrics { private static final ConcurrentMap keycloakMetrics = new ConcurrentHashMap<>(); public static final KeycloakMetric INSTANCE_METADATA = newKeycloakMetric("keycloak_instance_metadata", "Keycloak Instance Metadata", Level.INSTANCE); public static final KeycloakMetric INSTANCE_METRICS_REFRESH = newKeycloakMetric("keycloak_instance_metrics_refresh_total_milliseconds", "Duration of Keycloak Metrics refresh in milliseconds.", Level.INSTANCE); public static final KeycloakMetric INVENTORY_REALMS_TOTAL = newKeycloakMetric("keycloak_inventory_realms_total", "Total realms per instance", Level.INSTANCE); public static final KeycloakMetric INVENTORY_SESSIONS_TOTAL = newKeycloakMetric("keycloak_inventory_sessions_total", "Total sessions per realm", Level.REALM); public static final KeycloakMetric INVENTORY_USERS_TOTAL = newKeycloakMetric("keycloak_inventory_users_total", "Total users per realm", Level.REALM); public static final KeycloakMetric INVENTORY_CLIENTS_TOTAL = newKeycloakMetric("keycloak_inventory_clients_total", "Total clients per realm", Level.REALM); public static final KeycloakMetric INVENTORY_GROUPS_TOTAL = newKeycloakMetric("keycloak_inventory_groups_total", "Total groups per realm", Level.REALM); public static final KeycloakMetric AUTH_CLIENT_LOGIN_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_auth_client_login_attempt_total", "Total attempted client logins", Level.REALM); public static final KeycloakMetric AUTH_CLIENT_LOGIN_SUCCESS_TOTAL = newKeycloakMetric("keycloak_auth_client_login_success_total", "Total successful client logins", Level.REALM); public static final KeycloakMetric AUTH_CLIENT_LOGIN_ERROR_TOTAL = newKeycloakMetric("keycloak_auth_client_login_error_total", "Total errors during client logins", Level.REALM); public static final KeycloakMetric AUTH_USER_LOGIN_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_auth_user_login_attempt_total", "Total attempted user logins", Level.REALM); public static final KeycloakMetric AUTH_USER_LOGIN_SUCCESS_TOTAL = newKeycloakMetric("keycloak_auth_user_login_success_total", "Total successful user logins", Level.REALM); public static final KeycloakMetric AUTH_USER_LOGIN_ERROR_TOTAL = newKeycloakMetric("keycloak_auth_user_login_error_total", "Total errors during user logins", Level.REALM); public static final KeycloakMetric AUTH_USER_LOGOUT_SUCCESS_TOTAL = newKeycloakMetric("keycloak_auth_user_logout_success_total", "Total successful user logouts", Level.REALM); public static final KeycloakMetric AUTH_USER_LOGOUT_ERROR_TOTAL = newKeycloakMetric("keycloak_auth_user_logout_error_total", "Total errors during user logouts", Level.REALM); public static final KeycloakMetric AUTH_USER_REGISTER_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_auth_user_register_attempt_total", "Total attempted user registrations", Level.REALM); public static final KeycloakMetric AUTH_USER_REGISTER_SUCCESS_TOTAL = newKeycloakMetric("keycloak_auth_user_register_success_total", "Total user registrations", Level.REALM); public static final KeycloakMetric AUTH_USER_REGISTER_ERROR_TOTAL = newKeycloakMetric("keycloak_auth_user_register_error_total", "Total errors during user registrations", Level.REALM); public static final KeycloakMetric OAUTH_TOKEN_REFRESH_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_oauth_token_refresh_attempt_total", "Total attempted token refreshes", Level.REALM); public static final KeycloakMetric OAUTH_TOKEN_REFRESH_SUCCESS_TOTAL = newKeycloakMetric("keycloak_oauth_token_refresh_success_total", "Total token refreshes", Level.REALM); public static final KeycloakMetric OAUTH_TOKEN_REFRESH_ERROR_TOTAL = newKeycloakMetric("keycloak_oauth_token_refresh_error_total", "Total errors during token refreshes", Level.REALM); public static final KeycloakMetric OAUTH_CODE_TO_TOKEN_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_oauth_code_to_token_attempts_total", "Total attempts for code to token exchanges", Level.REALM); public static final KeycloakMetric OAUTH_CODE_TO_TOKEN_SUCCESS_TOTAL = newKeycloakMetric("keycloak_oauth_code_to_token_success_total", "Total code to token exchanges", Level.REALM); public static final KeycloakMetric OAUTH_CODE_TO_TOKEN_ERROR_TOTAL = newKeycloakMetric("keycloak_oauth_code_to_token_error_total", "Total errors during code to token exchanges", Level.REALM); public static final KeycloakMetric OAUTH_USERINFO_REQUEST_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_oauth_userinfo_request_attempt_total", "Total attempted user info requests", Level.REALM); public static final KeycloakMetric OAUTH_USERINFO_REQUEST_SUCCESS_TOTAL = newKeycloakMetric("keycloak_oauth_userinfo_request_success_total", "Total user info requests", Level.REALM); public static final KeycloakMetric OAUTH_USERINFO_REQUEST_ERROR_TOTAL = newKeycloakMetric("keycloak_oauth_userinfo_request_error_total", "Total errors during user info requests", Level.REALM); public static final KeycloakMetric OAUTH_TOKEN_EXCHANGE_ATTEMPT_TOTAL = newKeycloakMetric("keycloak_oauth_token_exchange_attempt_total", "Total attempted token refreshes", Level.REALM); public static final KeycloakMetric OAUTH_TOKEN_EXCHANGE_SUCCESS_TOTAL = newKeycloakMetric("keycloak_oauth_token_exchange_success_total", "Total token refreshes", Level.REALM); public static final KeycloakMetric OAUTH_TOKEN_EXCHANGE_ERROR_TOTAL = newKeycloakMetric("keycloak_oauth_token_exchange_error_total", "Total errors during token refreshes", Level.REALM); private static KeycloakMetric newKeycloakMetric(String name, String description, Level level) { var metric = new KeycloakMetric(name, description, level); keycloakMetrics.put(name, metric); return metric; } private final MeterRegistry meterRegistry; private final KeycloakMetricStore store; public KeycloakMetrics(MeterRegistry meterRegistry, KeycloakSessionFactory sessionFactory) { this.meterRegistry = meterRegistry; this.store = new KeycloakMetricStore(sessionFactory, meterRegistry, new RealmMetricsUpdater() { @Override public void updateGlobalMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, long lastUpdateTimestamp) { // Performs the dynamic metrics collection on global level: this is called when metrics need to be refreshed log.debugf("Updating realm count"); var em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); Number realmCount = (Number) em.createQuery("select count(r) from RealmEntity r").setHint(AvailableHints.HINT_READ_ONLY, true).getSingleResult(); metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_REALMS_TOTAL, new MetricUpdateValue<>(realmCount), null); log.debugf("Updated realm count"); } @Override public void updateRealmMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, RealmModel realm, long lastUpdateTimestamp) { // Performs the dynamic metrics collection on realm level: this is called when metrics need to be refreshed metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_USERS_TOTAL, new MetricUpdateValue<>(session.users().getUsersCount(realm)), realm); metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_CLIENTS_TOTAL, new MetricUpdateValue<>(session.clients().getClientsCount(realm)), realm); metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_GROUPS_TOTAL, new MetricUpdateValue<>(session.groups().getGroupsCount(realm, false)), realm); var realmSessionStats = collectRealmSessionStats(session, realm); var metricUpdateValue = new MultiMetricUpdateValues(Map.of(Tags.of("type", "online"), realmSessionStats.getOnlineSessions(), Tags.of("type", "offline"), realmSessionStats.getOfflineSessions())); metricUpdater.updateMetricValue(KeycloakMetrics.INVENTORY_SESSIONS_TOTAL, metricUpdateValue, realm); } }); } private RealmSessionStats collectRealmSessionStats(KeycloakSession session, RealmModel realm) { var userSessionsCount = session.sessions().getActiveClientSessionStats(realm, false).values().stream().reduce(0L, Long::sum); var offlineSessionsCount = session.sessions().getActiveClientSessionStats(realm, true).values().stream().reduce(0L, Long::sum); return new RealmSessionStats(userSessionsCount, offlineSessionsCount); } @Data static class RealmSessionStats { private final long onlineSessions; private final long offlineSessions; } public void registerInstanceMetrics() { Gauge.builder(INSTANCE_METADATA.getName(), () -> 0) // .description(INSTANCE_METADATA.getDescription()) // .tags(Tags.of("version", Version.VERSION, "buildtime", Version.BUILD_TIME)) // .register(meterRegistry); Gauge.builder(INSTANCE_METRICS_REFRESH.getName(), () -> 0) // .description(INSTANCE_METRICS_REFRESH.getDescription()) // .register(meterRegistry); } public MeterRegistry getMeterRegistry() { return meterRegistry; } public void removeRealmMetrics(RealmModel realm) { log.infof("Remove metrics for deleted realm %s", realm.getName()); var realmTag = Tags.of("realm", realm.getName()); var snapshot = new ArrayList<>(keycloakMetrics.values()); for (var keycloakMetric : snapshot) { Gauge gauge = meterRegistry.find(keycloakMetric.getName()).tags(realmTag).gauge(); if (gauge != null) { meterRegistry.remove(gauge); continue; } Counter counter = meterRegistry.find(keycloakMetric.getName()).tags(realmTag).counter(); if (counter != null) { meterRegistry.remove(counter); continue; } Meter meter = meterRegistry.find(keycloakMetric.getName()).tags(realmTag).meter(); if (meter != null) { meterRegistry.remove(meter); } } } public void initialize() { store.getMetricValue(null); } public enum Level { INSTANCE, REALM } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/RealmMetricUpdater.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics; import io.micrometer.core.instrument.Tags; import lombok.Data; import org.keycloak.models.RealmModel; import java.util.Map; public interface RealmMetricUpdater { /** * Updates a single metric with the given value in the context of a realm. *

* If the realm is null the metric is consider to be global. * * @param keycloakMetric * @param value * @param realm */ void updateMetricValue(KeycloakMetric keycloakMetric, MetricUpdateValue value, RealmModel realm); @Data class MetricUpdateValue { private final V value; } class MultiMetricUpdateValues extends MetricUpdateValue> { public MultiMetricUpdateValues(Map value) { super(value); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/RealmMetricsUpdater.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; public interface RealmMetricsUpdater { void updateGlobalMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, long lastUpdateTimestamp); void updateRealmMetrics(KeycloakSession session, RealmMetricUpdater metricUpdater, RealmModel realm, long lastUpdateTimestamp); } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/events/MetricEventListenerProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics.events; import com.github.thomasdarimont.keycloak.custom.metrics.KeycloakMetrics; import com.google.auto.service.AutoService; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProviderFactory; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.quarkus.runtime.configuration.Configuration; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; public class MetricEventListenerProvider implements EventListenerProvider { private final MetricEventRecorder recorder; public MetricEventListenerProvider(MetricEventRecorder recorder) { this.recorder = recorder; } @Override public void onEvent(Event event) { recorder.recordEvent(event); } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { recorder.recordEvent(event, includeRepresentation); } @Override public void close() { // NOOP } @JBossLog @AutoService(EventListenerProviderFactory.class) public static class Factory implements EventListenerProviderFactory { private EventListenerProvider instance; @Override public String getId() { return "acme-metrics"; } @Override public EventListenerProvider create(KeycloakSession session) { return instance; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory sessionFactory) { // var metricsEnabled = Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("metrics-enabled")).orElse(false); // if (!metricsEnabled) { // instance = new NoopEventListenerProvider(); // } // // var keycloakMetrics = new KeycloakMetrics(lookupMeterRegistry(), sessionFactory); // keycloakMetrics.registerInstanceMetrics(); // // sessionFactory.register(event -> { // // if (event instanceof PostMigrationEvent) { // keycloakMetrics.initialize(); // } else if (event instanceof RealmModel.RealmRemovedEvent) { // var realmRemoved = (RealmModel.RealmRemovedEvent) event; // keycloakMetrics.removeRealmMetrics(realmRemoved.getRealm()); // } // }); // // var metricRecorder = new MetricEventRecorder(keycloakMetrics); // // instance = new MetricEventListenerProvider(metricRecorder); } protected MeterRegistry lookupMeterRegistry() { return Metrics.globalRegistry; } @Override public void close() { // NOOP } } private static class NoopEventListenerProvider implements EventListenerProvider { @Override public void onEvent(Event event) { // NOOP assert true; } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { // NOOP assert true; } @Override public void close() { // NOOP assert true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/metrics/events/MetricEventRecorder.java ================================================ package com.github.thomasdarimont.keycloak.custom.metrics.events; import com.github.thomasdarimont.keycloak.custom.metrics.KeycloakMetrics; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.events.Details; import org.keycloak.events.Event; import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.utils.KeycloakSessionUtil; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.function.Consumer; import static org.keycloak.events.EventType.CLIENT_LOGIN; import static org.keycloak.events.EventType.CLIENT_LOGIN_ERROR; import static org.keycloak.events.EventType.CODE_TO_TOKEN; import static org.keycloak.events.EventType.CODE_TO_TOKEN_ERROR; import static org.keycloak.events.EventType.LOGIN; import static org.keycloak.events.EventType.LOGIN_ERROR; import static org.keycloak.events.EventType.LOGOUT; import static org.keycloak.events.EventType.LOGOUT_ERROR; import static org.keycloak.events.EventType.REFRESH_TOKEN; import static org.keycloak.events.EventType.REFRESH_TOKEN_ERROR; import static org.keycloak.events.EventType.REGISTER; import static org.keycloak.events.EventType.REGISTER_ERROR; import static org.keycloak.events.EventType.TOKEN_EXCHANGE; import static org.keycloak.events.EventType.TOKEN_EXCHANGE_ERROR; import static org.keycloak.events.EventType.USER_INFO_REQUEST; import static org.keycloak.events.EventType.USER_INFO_REQUEST_ERROR; @JBossLog @RequiredArgsConstructor public class MetricEventRecorder { private static final String USER_EVENT_METRIC_NAME = "keycloak_user_event"; private static final String ADMIN_EVENT_METRIC_NAME = "keycloak_admin_event"; private final MeterRegistry metricRegistry; private final Map> customUserEventHandlers; private final ConcurrentMap realmNameCache = new ConcurrentHashMap<>(); public MetricEventRecorder(KeycloakMetrics keycloakMetrics) { this.metricRegistry = keycloakMetrics.getMeterRegistry(); this.customUserEventHandlers = registerCustomUserEventHandlers(); } private Map> registerCustomUserEventHandlers() { Map> map = new HashMap<>(); map.put(LOGIN, this::recordUserLogin); map.put(LOGIN_ERROR, this::recordUserLoginError); map.put(LOGOUT, this::recordUserLogout); map.put(LOGOUT_ERROR, this::recordUserLogoutError); map.put(CLIENT_LOGIN, this::recordClientLogin); map.put(CLIENT_LOGIN_ERROR, this::recordClientLoginError); map.put(REGISTER, this::recordUserRegistration); map.put(REGISTER_ERROR, this::recordUserRegistrationError); map.put(REFRESH_TOKEN, this::recordOauthTokenRefresh); map.put(REFRESH_TOKEN_ERROR, this::recordOauthTokenRefreshError); map.put(CODE_TO_TOKEN, this::recordOauthCodeToToken); map.put(CODE_TO_TOKEN_ERROR, this::recordOauthCodeToTokenError); map.put(USER_INFO_REQUEST, this::recordOauthUserInfoRequest); map.put(USER_INFO_REQUEST_ERROR, this::recordOauthUserInfoRequestError); map.put(TOKEN_EXCHANGE, this::recordOauthTokenExchange); map.put(TOKEN_EXCHANGE_ERROR, this::recordOauthTokenExchangeError); return map; } public void recordEvent(Event event) { lookupUserEventHandler(event).accept(event); } public void recordEvent(AdminEvent event, boolean includeRepresentation) { // TODO add capability to ignore certain admin events recordGenericAdminEvent(event); } private void recordGenericAdminEvent(AdminEvent event) { var operationType = event.getOperationType(); var resourceType = event.getResourceType(); var realmName = resolveRealmName(event.getRealmId()); var resourceTypeName = resourceType.name(); var operationTypeName = operationType.name(); var tags = Tags.of("realm", realmName, "resource", resourceTypeName, "operation_type", operationTypeName); metricRegistry.counter(ADMIN_EVENT_METRIC_NAME, tags).increment(); } public Consumer lookupUserEventHandler(Event event) { return customUserEventHandlers.getOrDefault(event.getType(), this::recordGenericUserEvent); } protected void recordOauthUserInfoRequestError(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId), "error", error); metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordOauthUserInfoRequest(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId)); metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_USERINFO_REQUEST_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordOauthTokenExchange(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId)); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordOauthTokenExchangeError(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId), "error", event.getError()); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_EXCHANGE_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordUserLogout(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); // String clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "provider", provider); metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGOUT_SUCCESS_TOTAL.getName(), tags).increment(); } protected void recordUserLogoutError(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "provider", provider, "client_id", resolveClientId(clientId), "error", error); metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGOUT_ERROR_TOTAL.getName(), tags).increment(); } protected void recordOauthCodeToTokenError(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "provider", provider, "client_id", resolveClientId(clientId), "error", error); metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordOauthCodeToToken(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "provider", provider, "client_id", resolveClientId(clientId)); metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_CODE_TO_TOKEN_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordClientLogin(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId)); metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordClientLoginError(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId), "error", error); metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.AUTH_CLIENT_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordOauthTokenRefreshError(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId), "error", error, "provider", provider); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordOauthTokenRefresh(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId)); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.OAUTH_TOKEN_REFRESH_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordUserRegistrationError(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId), "error", error, "provider", provider); metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordUserRegistration(Event event) { var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId)); metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.AUTH_USER_REGISTER_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordUserLoginError(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = event.getClientId(); var error = event.getError(); var tags = Tags.of("realm", realmName, "client_id", resolveClientId(clientId), "error", error, "provider", provider); metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_ERROR_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment(); } protected void recordUserLogin(Event event) { var provider = getIdentityProvider(event); var realmName = resolveRealmName(event.getRealmId()); var clientId = resolveClientId(event.getClientId()); var tags = Tags.of("realm", realmName, "client_id", clientId, "provider", provider); metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_SUCCESS_TOTAL.getName(), tags).increment(); metricRegistry.counter(KeycloakMetrics.AUTH_USER_LOGIN_ATTEMPT_TOTAL.getName(), tags).increment(); } /** * Count generic user event * * @param event User event */ protected void recordGenericUserEvent(Event event) { var eventType = event.getType(); var realmName = resolveRealmName(event.getRealmId()); var eventTypeName = eventType.name(); var tags = Tags.of("realm", realmName, "event_type", eventTypeName); if (eventType == EventType.CUSTOM_REQUIRED_ACTION && event.getDetails().get(Details.CUSTOM_REQUIRED_ACTION) != null) { tags = Tags.concat(tags, Tags.of("custom_required_action", event.getDetails().get(Details.CUSTOM_REQUIRED_ACTION))); } metricRegistry.counter(USER_EVENT_METRIC_NAME, tags).increment(); } /** * Retrieve the identity provider name from event details or *

* default to {@value "keycloak"}. * * @param event User event * @return Identity provider name */ private String getIdentityProvider(Event event) { String identityProvider = null; if (event.getDetails() != null) { identityProvider = event.getDetails().get("identity_provider"); } if (identityProvider == null) { identityProvider = "@realm"; } return identityProvider; } /** * Creates a counter based on a event name */ private Counter createCounter(String name, boolean isAdmin) { var description = isAdmin ? "Generic KeyCloak Admin event" : "Generic KeyCloak User event"; return Counter.builder(name).description(description).register(metricRegistry); } private String resolveClientId(String clientId) { if (clientId == null) { return "missing"; } return clientId; } private String resolveRealmName(String realmId) { return realmNameCache.computeIfAbsent(realmId, key -> { var realm = KeycloakSessionUtil.getKeycloakSession().realms().getRealm(key); return realm.getName(); }); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/migration/acmecred/AcmeCredentialModel.java ================================================ package com.github.thomasdarimont.keycloak.custom.migration.acmecred; import org.keycloak.credential.CredentialModel; import org.keycloak.models.credential.dto.PasswordCredentialData; import org.keycloak.models.credential.dto.PasswordSecretData; import org.keycloak.util.JsonSerialization; import java.io.IOException; import static org.keycloak.utils.StringUtil.isBlank; public class AcmeCredentialModel extends CredentialModel { public static final String TYPE = "acme-password"; private final PasswordCredentialData acmeCredentialData; private final PasswordSecretData acmeSecretData; public AcmeCredentialModel(PasswordCredentialData acmeCredentialData, PasswordSecretData acmeSecretData) { this.acmeCredentialData = acmeCredentialData; this.acmeSecretData = acmeSecretData; } public static AcmeCredentialModel createFromCredentialModel(CredentialModel credentialModel) { try { PasswordCredentialData credentialData = isBlank(credentialModel.getCredentialData()) ? null : JsonSerialization.readValue(credentialModel.getCredentialData(), PasswordCredentialData.class); PasswordSecretData secretData = isBlank(credentialModel.getSecretData()) ? null : JsonSerialization.readValue(credentialModel.getSecretData(), PasswordSecretData.class); AcmeCredentialModel acmeCredentialModel = new AcmeCredentialModel(credentialData, secretData); acmeCredentialModel.setCreatedDate(credentialModel.getCreatedDate()); acmeCredentialModel.setCredentialData(credentialModel.getCredentialData()); acmeCredentialModel.setId(credentialModel.getId()); acmeCredentialModel.setSecretData(credentialModel.getSecretData()); acmeCredentialModel.setType(TYPE); acmeCredentialModel.setUserLabel(credentialModel.getUserLabel()); return acmeCredentialModel; } catch (IOException e) { throw new RuntimeException(e); } } public PasswordCredentialData getAcmeCredentialData() { return acmeCredentialData; } public PasswordSecretData getAcmeSecretData() { return acmeSecretData; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/migration/acmecred/AcmeCredentialProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.migration.acmecred; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.credential.CredentialTypeMetadata; import org.keycloak.credential.CredentialTypeMetadataContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.PasswordCredentialModel; @JBossLog public class AcmeCredentialProvider implements CredentialProvider, CredentialInputValidator { private final KeycloakSession session; public AcmeCredentialProvider(KeycloakSession session) { this.session = session; } @Override public String getType() { return AcmeCredentialModel.TYPE; } @Override public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel credentialModel) { // we don't support acme-credential creation return null; } @Override public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) { return user.credentialManager().removeStoredCredentialById(credentialId); } @Override public CredentialModel getCredentialFromModel(CredentialModel model) { // we support the acme-password and normal password credential model // this is required to support the correct credential type in admin-console if (model.getType().equals(AcmeCredentialModel.TYPE)) { return AcmeCredentialModel.createFromCredentialModel(model); } return PasswordCredentialModel.createFromCredentialModel(model); } @Override public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) { CredentialTypeMetadata.CredentialTypeMetadataBuilder metadataBuilder = CredentialTypeMetadata.builder() .type(getType()) .category(CredentialTypeMetadata.Category.BASIC_AUTHENTICATION) .displayName("Acme Password") .helpText("password-help-text") .iconCssClass("kcAuthenticatorPasswordClass"); // Check if we are creating or updating password UserModel user = metadataContext.getUser(); if (user != null && user.credentialManager().isConfiguredFor(getType())) { metadataBuilder.updateAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString()); } else { metadataBuilder.createAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString()); } return metadataBuilder .removeable(false) .build(session); } @Override public boolean supportsCredentialType(String credentialType) { // HACK: to support password input validation via UsernamePasswordForm authenticator // we need to pretend to accept credential type "password" return PasswordCredentialModel.TYPE.equals(credentialType); } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { boolean supportedCredentialType = supportsCredentialType(credentialType); boolean acmeCredentialConfigured = isAcmeCredentialConfigured(user); return supportedCredentialType && acmeCredentialConfigured; } private boolean isAcmeCredentialConfigured(UserModel user) { return user.credentialManager().getCredentials().anyMatch(cm -> AcmeCredentialModel.TYPE.equals(cm.getType())); } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { CredentialModel credentialModel = user.credentialManager().getStoredCredentialsByTypeStream(AcmeCredentialModel.TYPE).findFirst().orElse(null); if (credentialModel == null) { // abort the acme password validation early return false; } String password = credentialInput.getChallengeResponse(); AcmeCredentialModel acmeCredentialModel = AcmeCredentialModel.createFromCredentialModel(credentialModel); String algorithm = acmeCredentialModel.getAcmeCredentialData().getAlgorithm(); boolean valid = switch (algorithm) { case "acme-sha1" -> { yield AcmePasswordValidator.validateLegacyPassword(password, acmeCredentialModel); } // add additional legacy password validations... default -> false; }; if (valid) { migrateCredential(realm, user, password, acmeCredentialModel); } return valid; } protected void migrateCredential(RealmModel realm, UserModel user, String password, AcmeCredentialModel acmeCredentialModel) { // remove the old password user.credentialManager().removeStoredCredentialById(acmeCredentialModel.getId()); // store the current password with the default hashing mechanism user.credentialManager().updateCredential(UserCredentialModel.password(password, false)); // remove acme federation link // user.setFederationLink(null); log.infof("Migrated user password after successful acme-credential validation. realm=%s userId=%s username=%s", realm.getName(), user.getId(), user.getUsername()); } @AutoService(CredentialProviderFactory.class) public static class Factory implements CredentialProviderFactory { @Override public String getId() { return "acme"; } @Override public AcmeCredentialProvider create(KeycloakSession session) { return new AcmeCredentialProvider(session); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/migration/acmecred/AcmePasswordValidator.java ================================================ package com.github.thomasdarimont.keycloak.custom.migration.acmecred; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; public class AcmePasswordValidator { public static boolean validateLegacyPassword(String password, AcmeCredentialModel acmeCredentialModel) { String hashAndSalt = acmeCredentialModel.getAcmeSecretData().getValue(); String hash = hashAndSalt.substring(0, hashAndSalt.lastIndexOf(':')); String salt = hashAndSalt.substring(hashAndSalt.lastIndexOf(':') + 1); return verifyPasswordSha1(password, hash, salt); } private static boolean verifyPasswordSha1(String password, String expectedPasswordHash, String salt) { String passwordHash = encodePassword(password, salt); return expectedPasswordHash.equals(passwordHash); } public static String encodePassword(String password, String salt) { char[] passwordChars = password.toCharArray(); byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8); ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(passwordChars)); byteBuffer.rewind(); byte[] passwordBytes = byteBuffer.array(); byte[] hashedPasswordBytes = createHashedPassword(passwordBytes, saltBytes); return HexFormat.of().formatHex(hashedPasswordBytes, 0, hashedPasswordBytes.length); } private static byte[] createHashedPassword(byte[] passwordBytes, byte[] saltBytes) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update(passwordBytes, 0, passwordBytes.length); digest.update(saltBytes); return digest.digest(); } catch (NoSuchAlgorithmException noSuchAlgorithmException) { throw new RuntimeException(noSuchAlgorithmException); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/client/OauthClientCredentialsTokenManager.java ================================================ package com.github.thomasdarimont.keycloak.custom.oauth.client; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.jbosslog.JBossLog; import org.apache.http.HttpStatus; import org.keycloak.OAuth2Constants; import org.keycloak.http.simple.SimpleHttp; import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.SingleUseObjectProvider; import org.keycloak.representations.AccessTokenResponse; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.time.Duration; import java.time.Instant; import java.util.Map; @JBossLog @Getter @Setter @RequiredArgsConstructor public class OauthClientCredentialsTokenManager { private static final int EXPIRATION_SLACK_SECONDS = 0; private String clientId; private String tokenUrl; private String scope; private boolean useCache; private String clientSecret; private String clientAssertion; private String clientAssertionType; private String customHttpClientProviderId; public String getToken(KeycloakSession session) { SingleUseObjectProvider cache = null; String tokenKey = createTokenCacheKey(session); if (useCache) { cache = session.getProvider(SingleUseObjectProvider.class); Map cachedAccessToken = cache.get(tokenKey); if (cachedAccessToken != null) { log.debugf("Fetched tokens from cache. tokenKey=%s", tokenKey); String accessToken = cachedAccessToken.get(OAuth2Constants.ACCESS_TOKEN); return accessToken; } log.debugf("Could not fetch tokens from cache. tokenKey=%s", tokenKey); } AccessTokenResponse accessTokenResponse = fetchToken(session, tokenKey); String accessToken = accessTokenResponse.getToken(); if (useCache && cache != null) { // store token long expiresInSeconds = accessTokenResponse.getExpiresIn(); // let's timeout the cached token a bit earlier than it actually does to avoid stale tokens long lifespanSeconds = Math.max(expiresInSeconds - EXPIRATION_SLACK_SECONDS, 0); Map tokenData = Map.of( // OAuth2Constants.ACCESS_TOKEN, accessToken, // OAuth2Constants.EXPIRES_IN, Duration.ofSeconds(expiresInSeconds).toString(), // OAuth2Constants.SCOPE, accessTokenResponse.getScope(), "fetchedAtInstant", Instant.now().toString() // ); cache.put(tokenKey, lifespanSeconds, tokenData); log.debugf("Stored new tokens in cache. tokenKey=%s cacheLifespanSeconds=%s", tokenKey, lifespanSeconds); } return accessToken; } private String createTokenCacheKey(KeycloakSession session) { String realmName = session.getContext().getRealm().getName(); String cacheKey = "tokens:" + realmName + ":" + clientId + ":" + Integer.toString(tokenUrl.hashCode(), 32); return cacheKey; } protected AccessTokenResponse fetchToken(KeycloakSession session, String tokenKey) { KeycloakSession keycloakSession = session; if (customHttpClientProviderId != null) { // create proxy to intercept calls to keycloakSession.getProvider(HttpClientProvider.class) // this allows to easily serve custom http client providers that can use custom client certificates for MTLS auth etc. keycloakSession = createKeycloakSessionProxy(session); } var request = SimpleHttp.create(session).doPost(tokenUrl); request.param(OAuth2Constants.CLIENT_ID, clientId); request.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS); if (clientSecret != null) { request.param(OAuth2Constants.CLIENT_SECRET, clientSecret); } if (clientAssertion != null) { request.param(OAuth2Constants.CLIENT_ASSERTION, clientAssertion); } if (clientAssertionType != null) { request.param(OAuth2Constants.CLIENT_ASSERTION_TYPE, clientAssertionType); } request.param(OAuth2Constants.SCOPE, scope); // TODO wrap this around a retry with exponatial backoff in case of HTTP Status 429 / 503 / etc. { AccessTokenResponse accessTokenResponse = null; try (var response = request.asResponse()){ if (response.getStatus() != HttpStatus.SC_OK) { throw new RuntimeException("Token retrieval failed: Bad status. status=" + response.getStatus() + " tokenKey=" + tokenKey); } accessTokenResponse = response.asJson(AccessTokenResponse.class); log.debugf("Fetched new tokens. tokenKey=%s", tokenKey); } catch (IOException e) { throw new RuntimeException("Token retrieval failed: I/O Error. tokenKey=" + tokenKey, e); } return accessTokenResponse; } } private KeycloakSession createKeycloakSessionProxy(KeycloakSession target) { ClassLoader cl = getClass().getClassLoader(); Class[] ifaces = {KeycloakSession.class}; InvocationHandler handler = (Object proxy, Method method, Object[] args) -> { if ("getProvider".equals(method.getName()) && args.length == 1 && HttpClientProvider.class.equals(args[0])) { return target.getProvider(HttpClientProvider.class, customHttpClientProviderId); } return method.invoke(target, args); }; Object sessionProxy = Proxy.newProxyInstance(cl, ifaces, handler); return (KeycloakSession) sessionProxy; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/tokenexchange/ApiKeyTokenExchangeProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.oauth.tokenexchange; import com.github.thomasdarimont.keycloak.custom.support.TokenUtils; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenExchangeContext; import org.keycloak.protocol.oidc.TokenExchangeProvider; import org.keycloak.protocol.oidc.TokenExchangeProviderFactory; import org.keycloak.protocol.oidc.tokenexchange.V1TokenExchangeProvider; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import java.util.regex.Pattern; /** * PoC for a custom Token-Exchange which can translate an API key of a technical user into an access-token. * * See custom_token_exchange.http "Perform custom token exchange with API Key" */ @JBossLog @RequiredArgsConstructor public class ApiKeyTokenExchangeProvider extends V1TokenExchangeProvider { public static final String ID = "acme-apikey-token-exchange"; public static final String ALLOWED_CLIENT_ID = "acme-api-gateway"; public static final String API_KEY_PARAM = "api_key"; public static final String DEFAULT_SCOPE = "roles"; private static final Pattern COLON_SPLIT_PATTERN = Pattern.compile(":"); private final KeycloakSession session; @Override protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession, AccessToken token, boolean disallowOnHolderOfTokenMismatch) { var context = session.getContext(); var realm = context.getRealm(); var formParams = context.getHttpRequest().getDecodedFormParameters(); var apiKey = formParams.getFirst(API_KEY_PARAM); if (apiKey == null) { return unsupportedResponse(); } var clientId = ALLOWED_CLIENT_ID; var scope = DEFAULT_SCOPE; var usernameAndKey = COLON_SPLIT_PATTERN.split(apiKey); var apiUsername = usernameAndKey[0]; var user = session.users().getUserByUsername(realm, apiUsername); if (user == null) { return unsupportedResponse(); } var key = usernameAndKey[1]; var valid = user.credentialManager().isValid(UserCredentialModel.password(key)); if (!valid) { return unsupportedResponse(); } var accessToken = TokenUtils.generateAccessToken(session, realm, user, clientId, scope, null); var tokenResponse = new AccessTokenResponse(); tokenResponse.setToken(accessToken); tokenResponse.setIdToken(null); tokenResponse.setRefreshToken(null); tokenResponse.setRefreshExpiresIn(0); tokenResponse.getOtherClaims().clear(); return Response.ok(tokenResponse) // .type(MediaType.APPLICATION_JSON_TYPE) // .build(); } @Override protected Response exchangeExternalToken(String issuer, String subjectToken) { return unsupportedResponse(); } @Override protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) { return unsupportedResponse(); } private Response unsupportedResponse() { return Response.status(Response.Status.BAD_REQUEST).build(); } @Override public boolean supports(TokenExchangeContext context) { var clientIdMatches = context.getClient() != null && ALLOWED_CLIENT_ID.equals(context.getClient().getClientId()); if (!clientIdMatches) { return false; } var apiKey = context.getFormParams().getFirst("api_key"); if (apiKey == null) { return false; } return true; } @Override public void close() { // NOOP } @AutoService(TokenExchangeProviderFactory.class) public static class Factory implements TokenExchangeProviderFactory { @Override public String getId() { return ID; } @Override public TokenExchangeProvider create(KeycloakSession session) { return new ApiKeyTokenExchangeProvider(session); } @Override public int order() { // default order in DefaultTokenExchangeProviderFactory is 0. // A higher order ensures we're executed first. return 20; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/tokenexchange/CustomTokenExchangeProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.oauth.tokenexchange; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.OAuth2Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenExchangeContext; import org.keycloak.protocol.oidc.TokenExchangeProvider; import org.keycloak.protocol.oidc.TokenExchangeProviderFactory; import org.keycloak.protocol.oidc.tokenexchange.V1TokenExchangeProvider; import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import java.util.Set; @JBossLog public class CustomTokenExchangeProvider extends V1TokenExchangeProvider { public static final String ID = "acme-token-exchange"; public static final String ALLOWED_REQUESTED_ISSUER = "https://id.acme.test/offline"; public static final Set ALLOWED_CLIENT_IDS = Set.of("acme-client-cli-app"); @Override protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession, AccessToken token, boolean disallowOnHolderOfTokenMismatch) { return unsupportedResponse(); } @Override protected Response exchangeExternalToken(String issuer, String subjectToken) { return unsupportedResponse(); } @Override protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) { // propagate new offline session to mobile_bff // obtain new access token & refresh token from mobile_bff // TODO ensure keys from mobile_bff JWKS endpoint are combined with keycloak jwks keys var accessToken = "appAT"; var refreshToken = "appRT"; var tokenResponse = new AccessTokenResponse(); tokenResponse.setToken(accessToken); tokenResponse.setIdToken(null); tokenResponse.setRefreshToken(refreshToken); tokenResponse.setRefreshExpiresIn(0); tokenResponse.getOtherClaims().clear(); return Response.ok(tokenResponse) // .type(MediaType.APPLICATION_JSON_TYPE) // .build(); } private Response unsupportedResponse() { return Response.status(Response.Status.BAD_REQUEST).build(); } @Override public boolean supports(TokenExchangeContext context) { var issuerMatches = context.getFormParams() != null && ALLOWED_REQUESTED_ISSUER.equals(context.getFormParams().getFirst(OAuth2Constants.REQUESTED_ISSUER)); var clientIdMatches = context.getClient() != null && ALLOWED_CLIENT_IDS.contains(context.getClient().getClientId()); return issuerMatches && clientIdMatches; } @Override public void close() { // NOOP } @AutoService(TokenExchangeProviderFactory.class) public static class Factory implements TokenExchangeProviderFactory, EnvironmentDependentProviderFactory { public static final TokenExchangeProvider INSTANCE = new CustomTokenExchangeProvider(); @Override public String getId() { return ID; } @Override public TokenExchangeProvider create(KeycloakSession session) { return INSTANCE; } @Override public int order() { // default order in DefaultTokenExchangeProviderFactory is 0. // A higher order ensures we're executed first. return 20; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } @Override public boolean isSupported(Config.Scope config) { // Disable custom token exchange provider for now return false; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oauth/tokenexchange/CustomV2TokenExchangeProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.oauth.tokenexchange; import com.google.auto.service.AutoService; import jakarta.ws.rs.core.Response; import lombok.extern.jbosslog.JBossLog; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenExchangeProvider; import org.keycloak.protocol.oidc.TokenExchangeProviderFactory; import org.keycloak.protocol.oidc.tokenexchange.StandardTokenExchangeProvider; import org.keycloak.protocol.oidc.tokenexchange.StandardTokenExchangeProviderFactory; import org.keycloak.representations.AccessToken; import java.util.List; @JBossLog public class CustomV2TokenExchangeProvider extends StandardTokenExchangeProvider { @Override protected Response exchangeClientToOIDCClient(UserModel targetUser, UserSessionModel targetUserSession, String requestedTokenType, List targetAudienceClients, String scope, AccessToken subjectToken) { return super.exchangeClientToOIDCClient(targetUser, targetUserSession, requestedTokenType, targetAudienceClients, scope, subjectToken); } // @AutoService(TokenExchangeProviderFactory.class) public static class Factory extends StandardTokenExchangeProviderFactory { @Override public TokenExchangeProvider create(KeycloakSession session) { return new CustomV2TokenExchangeProvider(); } static { log.debug("Initializing CustomV2TokenExchangeProvider"); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/ageinfo/AgeInfoMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.ageinfo; import com.google.auto.service.AutoService; import com.google.common.annotations.VisibleForTesting; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper; import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.IDToken; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; @AutoService(ProtocolMapper.class) public class AgeInfoMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { public static final String PROVIDER_ID = "oidc-acme-ageinfo-mapper"; public static final String AGE_CLASS_CLAIM = "acme_age_class"; private static final List CONFIG_PROPERTIES; static { List configProperties = new ArrayList<>(); OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, AgeInfoMapper.class); CONFIG_PROPERTIES = configProperties; } @Override public String getId() { return PROVIDER_ID; } @Override public String getDisplayType() { return "Acme: AgeInfo"; } @Override public String getHelpText() { return "Exposes the age-class of the user as claim. The age is computed from a birthdate user attribute."; } @Override public String getDisplayCategory() { return TOKEN_MAPPER_CATEGORY; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { UserModel user = userSession.getUser(); String birthdate = user.getFirstAttribute("birthdate"); String ageClass = computeAgeClass(birthdate); token.getOtherClaims().put(AGE_CLASS_CLAIM, ageClass); } @VisibleForTesting String computeAgeClass(String maybeBirthdate) { if (maybeBirthdate == null) { return "missing"; } LocalDate birthdate = parseLocalDate(maybeBirthdate); if (birthdate == null) { return "invalid"; } long ageInYears = ChronoUnit.YEARS.between(birthdate, LocalDateTime.now()); String ageClass = "under16"; if (ageInYears >= 16) { ageClass = "over16"; } if (ageInYears >= 18) { ageClass = "over18"; } if (ageInYears >= 21) { ageClass = "over21"; } return ageClass; } private LocalDate parseLocalDate(String maybeLocalDate) { try { // 1983-01-01 return LocalDate.parse(maybeLocalDate); } catch (DateTimeParseException ex) { return null; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/authzenclaims/AuthzenClaimMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.authzenclaims; import com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen; import com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthzenClient; import com.github.thomasdarimont.keycloak.custom.auth.opa.OpaClient; import com.github.thomasdarimont.keycloak.custom.config.MapConfig; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.common.util.CollectionUtil; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper; import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.IDToken; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @JBossLog @AutoService(ProtocolMapper.class) public class AuthzenClaimMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { private static final List CONFIG_PROPERTIES; static { var list = ProviderConfigurationBuilder.create() // .property().name(AuthzenClient.AUTHZ_URL) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Authzen Policy URL") // .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) // .helpText("URL of Authzen Policy URL") // .add() // .property().name(AuthzenClient.AUTHZ_TYPE) // .type(ProviderConfigProperty.LIST_TYPE) // .label("Authzen call type") // .options(AuthzenClient.AUTHZ_TYPE_ACCESS, AuthzenClient.AUTHZ_TYPE_SEARCH) .defaultValue(AuthzenClient.AUTHZ_TYPE_ACCESS) // .helpText("Type of the Authzen API call") // .add() // .property().name(AuthzenClient.USER_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("User Attributes") // .defaultValue(null) // .helpText("Comma separated list of user attributes to send with authz requests.") // .add() // .property().name(AuthzenClient.ACTION) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Action") // .defaultValue(null) // .helpText("Name fo the action to check.") // .add() // .property().name(AuthzenClient.DESCRIPTION) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Description") // .defaultValue(null) // .helpText("Description.") // .add() // .property().name(AuthzenClient.RESOURCE_TYPE) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Resource Type") // .defaultValue(null) // .helpText("The resource type to access.") // .add() // .property().name(AuthzenClient.RESOURCE_CLAIM_NAME) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Resource Claim Name") // .defaultValue(null) // .helpText("Name of the claim to extract the resource claims.") // .add() // .property().name(AuthzenClient.REALM_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Realm Attributes") // .defaultValue(null) // .helpText("Comma separated list of realm attributes to send with authz requests.") // .add() // .property().name(AuthzenClient.CONTEXT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Context Attributes") // .defaultValue(null) // .helpText("Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress") // .add() // .property().name(AuthzenClient.REQUEST_HEADERS) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Request Headers") // .defaultValue(null) // .helpText("Comma separated list of request headers to send with authz requests.") // .add() // .property().name(AuthzenClient.CLIENT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Client Attributes") // .defaultValue(null) // .helpText("Comma separated list of client attributes to send with authz requests.") // .add() // .property().name(AuthzenClient.USE_REALM_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use realm roles") // .defaultValue("true") // .helpText("If enabled, realm roles will be sent with authz requests.") // .add() // .property().name(AuthzenClient.USE_CLIENT_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use client roles") // .defaultValue("true") // .helpText("If enabled, client roles will be sent with authz requests.") // .add() // .property().name(AuthzenClient.USE_GROUPS) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use groups") // .defaultValue("true") // .helpText("If enabled, group information will be sent with authz requests.") // .add() // .property().name(AuthzenClient.USE_USER_ATTRIBUTES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use user attributes") // .defaultValue("true") // .helpText("If enabled, user attribute information will be sent with authz requests based on the selection of user attributes.") // .add() // .build(); OIDCAttributeMapperHelper.addAttributeConfig(list, UserPropertyMapper.class); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } @Override public String getId() { return "acme-oidc-authzen-mapper"; } @Override public String getDisplayType() { return "Acme: Authzen Claim Mapper"; } @Override public String getHelpText() { return "Uses the Authzen Search API to obtain claims to add to the Token"; } @Override public String getDisplayCategory() { return "authzen"; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { var context = keycloakSession.getContext(); var realm = context.getRealm(); var authSession = clientSessionCtx.getClientSession(); var user = authSession.getUserSession().getUser(); var config = new MapConfig(mappingModel.getConfig()); String action = mappingModel.getConfig().get(AuthzenClient.ACTION); String resourceType = mappingModel.getConfig().get(AuthzenClient.RESOURCE_TYPE); var resource = new AuthZen.Resource(resourceType); var client = authSession.getClient(); var authZenClient = new AuthzenClient(); String authzenType = mappingModel.getConfig().get(AuthzenClient.AUTHZ_TYPE); switch (authzenType) { case AuthzenClient.AUTHZ_TYPE_ACCESS -> { var accessResponse = authZenClient.checkAccess(keycloakSession, config, realm, user, client, action, resource); if (accessResponse == null) { return; } copyAccessResultToClaim(token, config, accessResponse); } case AuthzenClient.AUTHZ_TYPE_SEARCH -> { var searchResponse = authZenClient.search(keycloakSession, config, realm, user, client, action, resource); if (searchResponse == null || CollectionUtil.isEmpty(searchResponse.results())) { return; } copySearchResultToClaim(token, config, searchResponse); } case null -> { // NOOP } default -> throw new IllegalStateException("Unexpected value: " + authzenType); } } protected void copyAccessResultToClaim(IDToken token, MapConfig config, AuthZen.AccessResponse accessResponse) { // TODO implement me } protected void copySearchResultToClaim(IDToken token, MapConfig config, AuthZen.SearchResponse searchResponse) { String targetClaimName = config.getString("claim.name"); String sourceClaimName = config.getString(AuthzenClient.RESOURCE_CLAIM_NAME); List results = searchResponse.results(); Set values = new LinkedHashSet<>(results.size()); for (AuthZen.Resource result : results) { if ("id".equals(sourceClaimName)) { values.add(result.id()); } else if ("type".equals(sourceClaimName)) { values.add(result.type()); } else { if (result.properties() != null) { Object value = result.properties().get(sourceClaimName); values.add(value); } } } token.setOtherClaims(targetClaimName, values); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/opaclaims/OpaClaimMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.opaclaims; import com.github.thomasdarimont.keycloak.custom.auth.authzen.AuthZen; import com.github.thomasdarimont.keycloak.custom.auth.opa.OpaClient; import com.github.thomasdarimont.keycloak.custom.config.MapConfig; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper; import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.IDToken; import java.util.Collections; import java.util.List; @JBossLog @AutoService(ProtocolMapper.class) public class OpaClaimMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { private static final List CONFIG_PROPERTIES; static { var list = ProviderConfigurationBuilder.create() // .property().name(OpaClient.OPA_AUTHZ_URL) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Authz Server Policy URL") // .defaultValue(OpaClient.DEFAULT_OPA_AUTHZ_URL) // .helpText("URL of OPA Authz Server Policy Resource") // .add() // .property().name(OpaClient.OPA_USER_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("User Attributes") // .defaultValue(null) // .helpText("Comma separated list of user attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_ACTION) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Action") // .defaultValue(null) // .helpText("Name fo the action to check.") // .add() // .property().name(OpaClient.OPA_DESCRIPTION) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Description") // .defaultValue(null) // .helpText("Description.") // .add() // .property().name(OpaClient.OPA_RESOURCE_TYPE) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Resource Type") // .defaultValue(null) // .helpText("The resource type to access.") // .add() // .property().name(OpaClient.OPA_RESOURCE_CLAIM_NAME) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Resource Claim Name") // .defaultValue(null) // .helpText("Name of the claim to extract the resource claims.") // .add() // .property().name(OpaClient.OPA_REALM_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Realm Attributes") // .defaultValue(null) // .helpText("Comma separated list of realm attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_CONTEXT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Context Attributes") // .defaultValue(null) // .helpText("Comma separated list of context attributes to send with authz requests. Supported attributes: remoteAddress") // .add() // .property().name(OpaClient.OPA_REQUEST_HEADERS) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Request Headers") // .defaultValue(null) // .helpText("Comma separated list of request headers to send with authz requests.") // .add() // .property().name(OpaClient.OPA_CLIENT_ATTRIBUTES) // .type(ProviderConfigProperty.STRING_TYPE) // .label("Client Attributes") // .defaultValue(null) // .helpText("Comma separated list of client attributes to send with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_REALM_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use realm roles") // .defaultValue("true") // .helpText("If enabled, realm roles will be sent with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_CLIENT_ROLES) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use client roles") // .defaultValue("true") // .helpText("If enabled, client roles will be sent with authz requests.") // .add() // .property().name(OpaClient.OPA_USE_GROUPS) // .type(ProviderConfigProperty.BOOLEAN_TYPE) // .label("Use groups") // .defaultValue("true") // .helpText("If enabled, group information will be sent with authz requests.") // .add() // .build(); OIDCAttributeMapperHelper.addAttributeConfig(list, UserPropertyMapper.class); CONFIG_PROPERTIES = Collections.unmodifiableList(list); } @Override public String getId() { return "acme-oidc-opa-mapper"; } @Override public String getDisplayType() { return "Acme: OPA Claim Mapper"; } @Override public String getHelpText() { return "Executes an OPA Policy to obtain claims to add to the Token"; } @Override public String getDisplayCategory() { return "opa"; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { var context = keycloakSession.getContext(); var realm = context.getRealm(); var authSession = clientSessionCtx.getClientSession(); var user = authSession.getUserSession().getUser(); var config = new MapConfig(mappingModel.getConfig()); String action = mappingModel.getConfig().get(OpaClient.OPA_ACTION); String resourceType = mappingModel.getConfig().get(OpaClient.OPA_RESOURCE_TYPE); var resource = new AuthZen.Resource(resourceType); var client = authSession.getClient(); OpaClient opaClient = new OpaClient(); var accessResponse = opaClient.checkAccess(keycloakSession, config, realm, user, client, action, resource); if (accessResponse == null) { return; } if (accessResponse.getResult() == null) { return; } String targetClaimName = config.getString("claim.name"); String sourceClaimName = config.getString(OpaClient.OPA_RESOURCE_CLAIM_NAME); if (accessResponse.getResult().decision()) { token.setOtherClaims(targetClaimName, accessResponse.getResult().context().get(sourceClaimName)); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/remoteclaims/RemoteOidcMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.remoteclaims; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.thomasdarimont.keycloak.custom.support.TokenUtils; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.apache.http.HttpHeaders; import org.jboss.logging.Logger; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper; import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.IDToken; import org.keycloak.services.Urls; import jakarta.ws.rs.core.UriBuilder; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; /** *
{@code
 *
 * KC_ISSUER=http://localhost:8081/auth/realms/remote-claims
 * KC_CLIENT_ID=demo-client-remote-claims
 * KC_USERNAME=tester
 * KC_PASSWORD=test
 *
 * KC_RESPONSE=$( \
 * curl \
 *   -d "client_id=$KC_CLIENT_ID" \
 *   -d "username=$KC_USERNAME" \
 *   -d "password=$KC_PASSWORD" \
 *   -d "grant_type=password" \
 *   "$KC_ISSUER/protocol/openid-connect/token" \
 * )
 * echo $KC_RESPONSE | jq -C .
 *
 * }
*/ @JBossLog @AutoService(ProtocolMapper.class) public class RemoteOidcMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper { private static final String PROVIDER_ID = "oidc-remote-protocol-mapper"; private static final Logger LOGGER = Logger.getLogger(RemoteOidcMapper.class); private static final List CONFIG_PROPERTIES; private static final String CONFIG_REMOTE_URL = "remoteUrl"; private static final String CONFIG_ADD_AUTH_HEADER = "addAuthHeader"; private static final String CONFIG_INTERNAL_CLIENT_ID = "internalClientId"; private static final String DEFAULT_INTERNAL_CLIENT_ID = "admin-cli"; public static final String DEFAULT_REMOTE_CLAIM_URL = "https://id.acme.test:4543/api/users/claims?userId={userId}&username={username}&clientId={clientId}&issuer={issuer}"; public static final String ROOT_OBJECT = "$ROOT$"; private static final ObjectMapper MAPPER = new ObjectMapper(); static { CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() .property() .name(CONFIG_REMOTE_URL) .type(ProviderConfigProperty.STRING_TYPE) .label("Remote URL") .helpText("URL to fetch custom claims for the given user") .defaultValue(DEFAULT_REMOTE_CLAIM_URL) .add() .property() .name(CONFIG_ADD_AUTH_HEADER) .type(ProviderConfigProperty.BOOLEAN_TYPE) .label("Add Authorization Header") .helpText("If set to true, an dynamically generated access-token will added to the Authorization header of the request") .defaultValue(true) .add() .property() .name(CONFIG_INTERNAL_CLIENT_ID) .type(ProviderConfigProperty.STRING_TYPE) .label("Internal Client ID") .helpText("The client_id to generate the internal access-token for the Authorization header. Defaults to admin-cli.") .defaultValue(DEFAULT_INTERNAL_CLIENT_ID) .add() .build(); OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserPropertyMapper.class); } @Override public String getDisplayCategory() { return TOKEN_MAPPER_CATEGORY; } @Override public String getDisplayType() { return "Acme: Remote Mapper"; } @Override public String getHelpText() { return "A protocol mapper that can fetch additional claims from an external service"; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public String getId() { return PROVIDER_ID; } @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) { // extract information from httpRequest as necessary // HttpRequest httpRequest = Resteasy.getContextData(HttpRequest.class); // httpRequest.getFormParameters().getFirst("formParam"); // httpRequest.getUri().getQueryParameters().getFirst("queryParam") KeycloakContext context = session.getContext(); boolean userInfoEndpointRequest = context.getUri().getPath().endsWith("/userinfo") || context.getUri().getPath().endsWith("/generate-example-userinfo"); String issuer = token.getIssuedFor(); String clientId = token.getIssuedFor(); if (userInfoEndpointRequest) { clientId = context.getClient().getClientId(); issuer = Urls.realmIssuer(context.getUri().getBaseUri(), context.getRealm().getName()); } var internalClientId = mappingModel.getConfig().getOrDefault(CONFIG_INTERNAL_CLIENT_ID, DEFAULT_INTERNAL_CLIENT_ID); if (internalClientId.equals(clientId)) { // workaround for infinite loop when generating remote claims into access-token. return; } Object claimValue = fetchRemoteClaims(mappingModel, userSession, session, issuer, clientId); LOGGER.infof("setClaim %s=%s", mappingModel.getName(), claimValue); String claimName = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); if (copyClaimsToRoot(claimValue, claimName)) { Map values = MAPPER.convertValue(claimValue, new TypeReference<>() { }); token.getOtherClaims().putAll(values); return; } if (claimValue == null) { log.warnf("Remote claims request returned null."); return; } OIDCAttributeMapperHelper.mapClaim(token, mappingModel, claimValue); } private boolean copyClaimsToRoot(Object claimValue, String claimName) { return ROOT_OBJECT.equals(claimName) && claimValue instanceof ObjectNode; } private Object fetchRemoteClaims(ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, String issuer, String clientId) { try { var url = createUri(mappingModel, userSession, issuer, clientId); var http = SimpleHttp.doGet(url, session); if (Boolean.parseBoolean(mappingModel.getConfig().getOrDefault(CONFIG_ADD_AUTH_HEADER, "false"))) { var accessToken = createInternalAccessToken(userSession, session); http.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); } try (var response = http.asResponse()) { if (response.getStatus() != 200) { log.warnf("Could not fetch remote claims for user. status=%s", response.getStatus()); return null; } return response.asJson(); } } catch (IOException e) { log.warnf("Could not fetch remote claims for user. error=%s", e.getMessage()); } return null; } private String createInternalAccessToken(UserSessionModel userSession, KeycloakSession session) { return TokenUtils.generateAccessToken(session, userSession, "admin-cli", "iam", token -> { // mark this token request as an internal iam request token.getOtherClaims().put("groups", List.of("iam")); }); } protected String createUri(ProtocolMapperModel mappingModel, UserSessionModel userSession, String issuer, String clientId) { String remoteUrlTemplate = mappingModel.getConfig().getOrDefault(CONFIG_REMOTE_URL, DEFAULT_REMOTE_CLAIM_URL); UserModel user = userSession.getUser(); UriBuilder uriBuilder = UriBuilder.fromUri(remoteUrlTemplate); Map params = new HashMap<>(); params.put("userId", user.getId()); params.put("username", user.getUsername()); params.put("clientId", clientId); params.put("issuer", issuer); URI uri = uriBuilder.buildFromMap(params); return uri.toString(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/scopes/OnlyGrantedScopesMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.scopes; import com.google.auto.service.AutoService; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.stream.Collectors; @AutoService(ProtocolMapper.class) public class OnlyGrantedScopesMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { private static final String PROVIDER_ID = "oidc-granted-scopes-protocol-mapper"; private static final Logger LOGGER = Logger.getLogger(OnlyGrantedScopesMapper.class); private static final List CONFIG_PROPERTIES; static { var list = new ArrayList(); OIDCAttributeMapperHelper.addIncludeInTokensConfig(list, OnlyGrantedScopesMapper.class); CONFIG_PROPERTIES = list; } @Override public String getDisplayCategory() { return TOKEN_MAPPER_CATEGORY; } @Override public String getDisplayType() { return "Acme: Ensure only granted scopes"; } @Override public String getHelpText() { return "A protocol mapper that ensures only granted scopes."; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public String getId() { return PROVIDER_ID; } @Override protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) { var context = session.getContext(); var user = userSession.getUser(); var client = clientSessionCtx.getClientSession().getClient(); var consentByClient = session.users().getConsentByClient(context.getRealm(), user.getId(), client.getId()); var at = (AccessToken) token; var requestedScopeValue = at.getScope(); var scopeItems = new ArrayList<>(List.of(requestedScopeValue.split(" "))); var grantedScopes = consentByClient.getGrantedClientScopes().stream() // .map(ClientScopeModel::getName) // .collect(Collectors.toList()); var result = new LinkedHashSet(); result.add(OAuth2Constants.SCOPE_OPENID); for (var requestedScopeItem : scopeItems) { if (grantedScopes.contains(requestedScopeItem)) { result.add(requestedScopeItem); } } var claimValue = String.join(" ", result); at.setScope(claimValue); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/userdata/AcmeUserInfoMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.userdata; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.representations.IDToken; import java.util.List; @JBossLog @AutoService(ProtocolMapper.class) public class AcmeUserInfoMapper extends AbstractOIDCProtocolMapper implements UserInfoTokenMapper { private static final String PROVIDER_ID = "oidc-acme-userdata-mapper"; private static final List CONFIG_PROPERTIES; static { CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() .build(); OIDCAttributeMapperHelper.addAttributeConfig(CONFIG_PROPERTIES, UserPropertyMapper.class); } @Override public String getDisplayCategory() { return TOKEN_MAPPER_CATEGORY; } @Override public String getDisplayType() { return "Acme Userdata Mapper"; } @Override public String getHelpText() { return "A protocol mapper that adds additional claims to userinfo"; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override public String getId() { return PROVIDER_ID; } protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession session, ClientSessionContext clientSessionCtx) { // extract information from httpRequest KeycloakContext context = session.getContext(); // Resteasy.getContextData(HttpRequest.class).getFormParameters().getFirst("acme_supplier_id"); boolean userInfoEndpointRequest = context.getUri().getPath().endsWith("/userinfo"); if (userInfoEndpointRequest) { var clientId = context.getClient().getClientId(); token.getOtherClaims().put("acme-userdata", // Stream.iterate(1, i -> i + 1).limit(100).map(i -> "User Data: " + i) List.of(1, 2, 3) ); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/oidc/wellknown/AcmeOidcWellKnownProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.oidc.wellknown; import com.google.auto.service.AutoService; import org.keycloak.OAuth2Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.protocol.oidc.OIDCWellKnownProvider; import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.wellknown.WellKnownProvider; import org.keycloak.wellknown.WellKnownProviderFactory; import java.util.ArrayList; /** * Custom OpenID {@link WellKnownProvider} which can remove unwanted OpenID configuration information. */ public class AcmeOidcWellKnownProvider implements WellKnownProvider { private final KeycloakSession session; private final OIDCWellKnownProvider delegate; public AcmeOidcWellKnownProvider(KeycloakSession session, OIDCWellKnownProvider delegate) { this.session = session; this.delegate = delegate; } @Override public Object getConfig() { OIDCConfigurationRepresentation config = (OIDCConfigurationRepresentation) delegate.getConfig(); var grantTypesSupported = new ArrayList<>(config.getGrantTypesSupported()); config.setGrantTypesSupported(grantTypesSupported); // // remove device-flow metadata // grantTypesSupported.remove(OAuth2Constants.DEVICE_CODE_GRANT_TYPE); // config.setDeviceAuthorizationEndpoint(null); // // remove ciba metadata // grantTypesSupported.remove(OAuth2Constants.CIBA_GRANT_TYPE); // config.setMtlsEndpointAliases(null); // config.setBackchannelAuthenticationEndpoint(null); // config.setBackchannelAuthenticationRequestSigningAlgValuesSupported(null); // config.setBackchannelTokenDeliveryModesSupported(null); // // remove dynamic client registration endpoint // config.setRegistrationEndpoint(null); // // Add custom claim // var claimsSupported = new ArrayList<>(config.getClaimsSupported()); // claimsSupported.add("customClaim"); // config.setClaimsSupported(claimsSupported); return config; } @Override public void close() { // NOOP } /** * Custom {@link WellKnownProviderFactory} which reuses the {@link OIDCWellKnownProviderFactory#PROVIDER_ID} to override * the default {@link OIDCWellKnownProviderFactory}. */ @AutoService(WellKnownProviderFactory.class) public static class Factory extends OIDCWellKnownProviderFactory { @Override public WellKnownProvider create(KeycloakSession session) { return new AcmeOidcWellKnownProvider(session, (OIDCWellKnownProvider) super.create(session)); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/profile/AcmeUserAttributes.java ================================================ package com.github.thomasdarimont.keycloak.custom.profile; public enum AcmeUserAttributes { ACCOUNT_DELETION_REQUESTED_AT("deletion-requested-at"); public static final String PREFIX = "acme:"; private final String attributeName; AcmeUserAttributes(String name) { this.attributeName = PREFIX + name; } public String getAttributeName() { return attributeName; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/profile/emailupdate/UpdateEmailRequiredAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.profile.emailupdate; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.SecretGenerator; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.email.freemarker.beans.ProfileBean; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.services.validation.Validation; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.storage.adapter.InMemoryUserAdapter; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @JBossLog public class UpdateEmailRequiredAction implements RequiredActionProvider { public static final String ID = "acme-update-email"; public static final String AUTH_NOTE_CODE = "emailCode"; public static final String EMAIL_FIELD = "email"; public static final int VERIFY_CODE_LENGTH = 6; private static final String UPDATE_EMAIL_AUTH_NOTE = ID; @Override public InitiatedActionSupport initiatedActionSupport() { // whether we can refer to that action via kc_actions URL parameter return InitiatedActionSupport.SUPPORTED; } @Override public void evaluateTriggers(RequiredActionContext context) { // check whether we need to show the update custom info form. AuthenticationSessionModel authSession = context.getAuthenticationSession(); if (!ID.equals(authSession.getClientNotes().get(Constants.KC_ACTION))) { // only show update form if we explicitly asked for the required action execution return; } if (context.getUser().getEmail() == null) { context.getUser().addRequiredAction(ID); } } @Override public void requiredActionChallenge(RequiredActionContext context) { // Show form context.challenge(createForm(context, null)); } protected Response createForm(RequiredActionContext context, Consumer formCustomizer) { LoginFormsProvider form = context.form(); form.setAttribute("username", context.getUser().getUsername()); if (context.getAuthenticationSession().getAuthNote(UPDATE_EMAIL_AUTH_NOTE) != null) { // we are already sent a code return form.createForm("verify-email-form.ftl"); } String email = context.getUser().getEmail(); form.setAttribute("currentEmail", email); if (formCustomizer != null) { formCustomizer.accept(form); } // use form from src/main/resources/theme-resources/templates/ return form.createForm("update-email-form.ftl"); } @Override public void processAction(RequiredActionContext context) { // TODO trigger email verification via email // user submitted the form MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); AuthenticationSessionModel authSession = context.getAuthenticationSession(); RealmModel realm = context.getRealm(); UserModel currentUser = context.getUser(); KeycloakSession session = context.getSession(); String oldEmail = currentUser.getEmail(); String newEmail = String.valueOf(formData.getFirst(EMAIL_FIELD)).trim(); EventBuilder errorEvent = context.getEvent().clone().event(EventType.UPDATE_EMAIL_ERROR) .client(authSession.getClient()) .user(authSession.getAuthenticatedUser()); if (formData.getFirst("update") != null) { final String emailError; if (Validation.isBlank(newEmail) || !Validation.isEmailValid(newEmail)) { emailError = "invalidEmailMessage"; errorEvent.detail("error", "invalid-email-format"); } else if (Objects.equals(newEmail, currentUser.getEmail())) { emailError = "invalidEmailSameAddressMessage"; errorEvent.detail("error", "invalid-email-same-email"); } else if (session.users().getUserByEmail(realm, newEmail) != null) { emailError = "invalidEmailMessage"; errorEvent.detail("error", "invalid-email-already-in-use"); } else { emailError = null; } if (emailError != null) { errorEvent.error(Errors.INVALID_INPUT); Response challenge = createForm(context, form -> { form.addError(new FormMessage(EMAIL_FIELD, emailError)); }); context.challenge(challenge); return; } String code = SecretGenerator.getInstance().randomString(VERIFY_CODE_LENGTH).toLowerCase(); authSession.setAuthNote(AUTH_NOTE_CODE, code); LoginFormsProvider form = context.form(); form.setAttribute("currentEmail", newEmail); try { EmailTemplateProvider emailTemplateProvider = session.getProvider(EmailTemplateProvider.class); emailTemplateProvider.setRealm(realm); // adapt current user to be able to override the email for the verification email UserModel userAdapter = new InMemoryUserAdapter(session, realm, currentUser.getId()); userAdapter.setEmail(newEmail); userAdapter.setUsername(currentUser.getUsername()); userAdapter.setFirstName(currentUser.getFirstName()); userAdapter.setLastName(currentUser.getLastName()); emailTemplateProvider.setUser(userAdapter); Map attributes = new HashMap<>(); attributes.put("code", code); attributes.put("user", new ProfileBean(currentUser, session) { @Override public String getEmail() { return newEmail; } }); String realmDisplayName = realm.getDisplayName(); if (realmDisplayName == null) { realmDisplayName = realm.getName(); } emailTemplateProvider.send("acmeEmailVerifySubject", List.of(realmDisplayName), "acme-email-verification-with-code.ftl", attributes); authSession.setAuthNote(UPDATE_EMAIL_AUTH_NOTE, newEmail); form.setInfo("emailSentInfo", newEmail); context.challenge(form.createForm("verify-email-form.ftl")); } catch (EmailException e) { log.errorf(e, "Could not send verify email."); context.failure(); } return; } if (formData.getFirst("verify") != null) { String emailFromAuthNote = authSession.getAuthNote(UPDATE_EMAIL_AUTH_NOTE); String expectedCode = authSession.getAuthNote(AUTH_NOTE_CODE); String actualCode = String.valueOf(formData.getFirst("code")).trim(); if (!expectedCode.equals(actualCode)) { LoginFormsProvider form = context.form(); form.setAttribute("currentEmail", emailFromAuthNote); form.setErrors(List.of(new FormMessage("code", "error-invalid-code"))); context.challenge(form.createForm("verify-email-form.ftl")); return; } // update username if necessary if (realm.isEditUsernameAllowed() && realm.isLoginWithEmailAllowed()) { currentUser.setUsername(emailFromAuthNote); } currentUser.setEmail(emailFromAuthNote); currentUser.setEmailVerified(true); currentUser.removeRequiredAction(ID); EventBuilder event = context.getEvent().clone().event(EventType.UPDATE_EMAIL); event.detail("email_old", oldEmail); event.detail(Details.EMAIL, newEmail); event.success(); context.success(); return; } context.failure(); } @Override public void close() { // NOOP } @AutoService(RequiredActionFactory.class) public static class Factory implements RequiredActionFactory { private static final UpdateEmailRequiredAction INSTANCE = new UpdateEmailRequiredAction(); @Override public RequiredActionProvider create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } @Override public String getId() { return UpdateEmailRequiredAction.ID; } @Override public String getDisplayText() { return "Acme: Update Email"; } @Override public boolean isOneTimeAction() { return true; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/profile/phonenumber/AcmePhoneValidator.java ================================================ package com.github.thomasdarimont.keycloak.custom.profile.phonenumber; import com.google.auto.service.AutoService; import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.validate.AbstractStringValidator; import org.keycloak.validate.ValidationContext; import org.keycloak.validate.ValidationError; import org.keycloak.validate.ValidatorConfig; import org.keycloak.validate.ValidatorFactory; import java.util.ArrayList; import java.util.List; @AutoService(ValidatorFactory.class) public class AcmePhoneValidator extends AbstractStringValidator implements ConfiguredProvider { public static final String ID = "acme-phone-validator"; public static final String DEFAULT_ERROR_MESSAGE_KEY = "error-invalid-phone-number"; public static final String ERROR_MESSAGE_PROPERTY = "error-message"; public static final String ALLOWED_NUMBERS_PATTERN_PROPERTY = "pattern"; private static final List CONFIG_PROPERTIES; static { var configProperties = new ArrayList(); ProviderConfigProperty property; property = new ProviderConfigProperty(); property.setName(ERROR_MESSAGE_PROPERTY); property.setType(ProviderConfigProperty.STRING_TYPE); property.setLabel("Error message key"); property.setHelpText("Key of the error message in i18n bundle"); property.setDefaultValue(DEFAULT_ERROR_MESSAGE_KEY); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(ALLOWED_NUMBERS_PATTERN_PROPERTY); property.setType(ProviderConfigProperty.STRING_TYPE); property.setLabel("Allowed Number Pattern"); property.setHelpText("Pattern for allowed phone numbers"); property.setDefaultValue("[+]?\\d+"); configProperties.add(property); CONFIG_PROPERTIES = configProperties; } @Override public String getId() { return ID; } @Override public String getHelpText() { return "Validates a Phone number"; } @Override public List getConfigProperties() { return CONFIG_PROPERTIES; } @Override protected void doValidate(String input, String inputHint, ValidationContext context, ValidatorConfig config) { // Simple example validations, use libphonenumber for more sophisticated validations, see: https://github.com/google/libphonenumber/tree/master if (config.get(ALLOWED_NUMBERS_PATTERN_PROPERTY) instanceof String patternString) { if (!input.matches(patternString)) { context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(ERROR_MESSAGE_PROPERTY, DEFAULT_ERROR_MESSAGE_KEY))); } } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/registration/actiontokens/AcmeExecuteActionsActionTokenHandler.java ================================================ package com.github.thomasdarimont.keycloak.custom.registration.actiontokens; import com.google.auto.service.AutoService; import org.keycloak.authentication.actiontoken.ActionTokenContext; import org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory; import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken; import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler; import org.keycloak.models.UserModel; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.sessions.AuthenticationSessionModel; /** * Example for changing Keycloaks standard behavior to remain logged in after action token handler execution. */ @AutoService(ActionTokenHandlerFactory.class) public class AcmeExecuteActionsActionTokenHandler extends ExecuteActionsActionTokenHandler { @Override public AuthenticationSessionModel startFreshAuthenticationSession(ExecuteActionsActionToken token, ActionTokenContext tokenContext) { AuthenticationSessionModel authSession = super.startFreshAuthenticationSession(token, tokenContext); boolean remainSingedIn = true; // set to true to remain signed in after auth boolean remainSignedInAfterExecuteActions = tokenContext.getRealm().getAttribute("acme.remainSignedInAfterExecuteActions", remainSingedIn); if (remainSignedInAfterExecuteActions) { authSession.removeAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS); } return authSession; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/registration/formaction/CustomRegistrationUserCreation.java ================================================ package com.github.thomasdarimont.keycloak.custom.registration.formaction; import com.github.thomasdarimont.keycloak.custom.support.ScopeUtils; import com.google.auto.service.AutoService; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormContext; import org.keycloak.authentication.ValidationContext; import org.keycloak.authentication.forms.RegistrationUserCreation; import org.keycloak.events.Errors; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.Constants; import org.keycloak.models.utils.FormMessage; import org.keycloak.userprofile.AttributeGroupMetadata; import org.keycloak.userprofile.AttributeMetadata; import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.UserProfileMetadata; import jakarta.ws.rs.core.MultivaluedMap; import java.util.ArrayList; import java.util.List; import java.util.Map; @AutoService(FormActionFactory.class) public class CustomRegistrationUserCreation extends RegistrationUserCreation { private static final String ID = "custom-registration-user-creation"; private static final String TERMS_FIELD = "terms"; private static final String TERMS_ACCEPTED_USER_ATTRIBUTE = "terms_accepted"; private static final String ACCEPT_TERMS_REQUIRED_FORM_ATTRIBUTE = "acceptTermsRequired"; private static final String TERMS_REQUIRED_MESSAGE = "termsRequired"; @Override public String getId() { return ID; } @Override public String getDisplayType() { return "Custom Registration: User Creation with Terms"; } @Override public void validate(ValidationContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); if (!formData.containsKey(TERMS_FIELD)) { context.error(Errors.INVALID_REGISTRATION); formData.remove(TERMS_FIELD); List errors = List.of(new FormMessage(TERMS_FIELD, TERMS_REQUIRED_MESSAGE)); context.validationError(formData, errors); return; } // TODO validate dynamic custom profile fields super.validate(context); } @Override public void success(FormContext context) { super.success(context); context.getUser().setSingleAttribute(TERMS_ACCEPTED_USER_ATTRIBUTE, String.valueOf(System.currentTimeMillis())); } @Override public void buildPage(FormContext context, LoginFormsProvider form) { form.setAttribute(ACCEPT_TERMS_REQUIRED_FORM_ATTRIBUTE, true); // addCustomDynamicProfileFields(context, form); } private void addCustomDynamicProfileFields(FormContext context, LoginFormsProvider form) { String scope = context.getAuthenticationSession().getClientNotes().get("scope"); var profileMetadata = new UserProfileMetadata(UserProfileContext.REGISTRATION); var groupAnnotations = Map.of(); var additionalAttributeGroupMetadata = new AttributeGroupMetadata("additionalData", "Additional Data", "Additional Profile Data", groupAnnotations); int guiOrder = 10; if (ScopeUtils.hasScope(ScopeUtils.SCOPE_ACME_AGE_INFO, scope)) { var birthdateAttribute = profileMetadata.addAttribute(Constants.USER_ATTRIBUTES_PREFIX + "birthdate", guiOrder++) // .setAttributeDisplayName("birthdate") // .addAnnotations(Map.of("inputType", "html5-date", "required", Boolean.TRUE)); birthdateAttribute.setAttributeGroupMetadata(additionalAttributeGroupMetadata); } if (ScopeUtils.hasScope(OAuth2Constants.SCOPE_PHONE, scope)) { var phoneNumberAttribute = profileMetadata.addAttribute(Constants.USER_ATTRIBUTES_PREFIX + "phone_number", guiOrder++) // .setAttributeDisplayName("phoneNumber") // .addAnnotations(Map.of("inputType", "html5-tel", "required", Boolean.FALSE)); phoneNumberAttribute.setAttributeGroupMetadata(additionalAttributeGroupMetadata); } // TODO add more robust mechanism to support custom profile fields, see AcmeFreemarkerLoginFormsProvider form.setAttribute("customProfile", new CustomProfile(profileMetadata)); } public static class CustomProfile { private final List attributes; public CustomProfile(UserProfileMetadata profileMetadata) { this.attributes = createAttributes(profileMetadata); } private List createAttributes(UserProfileMetadata profileMetadata) { if (profileMetadata.getAttributes() == null) { return List.of(); } var attributes = new ArrayList(); for (var attr : profileMetadata.getAttributes()) { attributes.add(new CustomAttribute(attr, null)); } return attributes; } public List getAttributes() { return attributes; } } public static class CustomAttribute { private final AttributeMetadata attributeMetadata; private final AttributeGroupMetadata groupMetadata; private final String value; public CustomAttribute(AttributeMetadata attributeMetadata, String value) { this.attributeMetadata = attributeMetadata; this.groupMetadata = attributeMetadata.getAttributeGroupMetadata(); this.value = value; } public String getName() { return this.attributeMetadata.getName(); } public String getDisplayName() { return this.attributeMetadata.getAttributeDisplayName(); } public boolean isRequired() { return Boolean.TRUE.equals(this.attributeMetadata.getAnnotations().get("required")); } public boolean isReadOnly() { return false; } public String getAutocomplete() { return null; } public String getValue() { return value; } public Map getAnnotations() { return attributeMetadata.getAnnotations(); } public String getGroup() { return groupMetadata.getName(); } public String getGroupDisplayHeader() { return groupMetadata.getDisplayHeader(); } public String getGroupDisplayDescription() { return groupMetadata.getDisplayDescription(); } public Map getGroupAnnotations() { return groupMetadata.getAnnotations(); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/registration/formaction/WelcomeEmailFormAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.registration.formaction; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormContext; import org.keycloak.authentication.ValidationContext; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.freemarker.model.RealmBean; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This welcome action can be placed as the last step of a custom registration flow to send an welcome-email to the new user. */ @JBossLog public class WelcomeEmailFormAction implements FormAction { @Override public void buildPage(FormContext context, LoginFormsProvider form) { // NOOP } @Override public void validate(ValidationContext context) { context.success(); } @Override public void success(FormContext context) { var session = context.getSession(); var realm = context.getRealm(); var user = context.getUser(); var username = user.getUsername(); var userDisplayName = getUserDisplayName(user); // NOOP Map mailBodyAttributes = new HashMap<>(); mailBodyAttributes.put("realm", new RealmBean(realm)); mailBodyAttributes.put("username", username); mailBodyAttributes.put("userDisplayName", userDisplayName); var realmDisplayName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName(); List subjectParams = List.of(realmDisplayName, userDisplayName); try { var emailProvider = session.getProvider(EmailTemplateProvider.class); emailProvider.setRealm(realm); emailProvider.setUser(user); // Don't forget to add the acme-welcome.ftl (html and text) template to your theme. emailProvider.send("acmeWelcomeSubject", subjectParams, "acme-welcome.ftl", mailBodyAttributes); } catch (EmailException eex) { log.errorf(eex, "Failed to send welcome email. realm=%s user=%s", realm.getName(), username); } } private String getUserDisplayName(UserModel user) { var firstname = user.getFirstName(); var lastname = user.getLastName(); if (firstname != null && lastname != null) { return firstname + " " + lastname; } return user.getUsername(); } @Override public boolean requiresUser() { // must return false here during registration. return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @AutoService(FormActionFactory.class) public static class Factory implements FormActionFactory { private static final WelcomeEmailFormAction INSTANCE = new WelcomeEmailFormAction(); public static final String ID = "acme-welcome-form-action"; @Override public String getId() { return ID; } @Override public String getDisplayType() { return "Acme: Welcome mail"; } @Override public String getHelpText() { return "Sends a welcome mail to a newly registered user."; } @Override public String getReferenceCategory() { return "Post Processing"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return Collections.emptyList(); } @Override public FormAction create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/saml/AcmeSamlAuthenticationPreprocessor.java ================================================ package com.github.thomasdarimont.keycloak.custom.saml; import com.google.auto.service.AutoService; import org.keycloak.Config; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.saml.preprocessor.SamlAuthenticationPreprocessor; import org.keycloak.sessions.AuthenticationSessionModel; /** * Example for customizing SAML Requests / Responses */ @AutoService(SamlAuthenticationPreprocessor.class) public class AcmeSamlAuthenticationPreprocessor implements SamlAuthenticationPreprocessor { @Override public String getId() { return "acme-saml-auth-preprocessor"; } @Override public AuthnRequestType beforeProcessingLoginRequest(AuthnRequestType authnRequest, AuthenticationSessionModel authSession) { return SamlAuthenticationPreprocessor.super.beforeProcessingLoginRequest(authnRequest, authSession); } @Override public LogoutRequestType beforeProcessingLogoutRequest(LogoutRequestType logoutRequest, UserSessionModel authSession, AuthenticatedClientSessionModel clientSession) { return SamlAuthenticationPreprocessor.super.beforeProcessingLogoutRequest(logoutRequest, authSession, clientSession); } @Override public AuthnRequestType beforeSendingLoginRequest(AuthnRequestType authnRequest, AuthenticationSessionModel clientSession) { return SamlAuthenticationPreprocessor.super.beforeSendingLoginRequest(authnRequest, clientSession); } @Override public LogoutRequestType beforeSendingLogoutRequest(LogoutRequestType logoutRequest, UserSessionModel authSession, AuthenticatedClientSessionModel clientSession) { return SamlAuthenticationPreprocessor.super.beforeSendingLogoutRequest(logoutRequest, authSession, clientSession); } @Override public StatusResponseType beforeProcessingLoginResponse(StatusResponseType statusResponse, AuthenticationSessionModel authSession) { return SamlAuthenticationPreprocessor.super.beforeProcessingLoginResponse(statusResponse, authSession); } @Override public StatusResponseType beforeSendingResponse(StatusResponseType statusResponse, AuthenticatedClientSessionModel clientSession) { return SamlAuthenticationPreprocessor.super.beforeSendingResponse(statusResponse, clientSession); } @Override public SamlAuthenticationPreprocessor create(KeycloakSession session) { return this; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/saml/brokering/AcmeSamlRoleImporter.java ================================================ package com.github.thomasdarimont.keycloak.custom.saml.brokering; import com.google.auto.service.AutoService; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityProviderMapper; import org.keycloak.broker.saml.SAMLEndpoint; import org.keycloak.broker.saml.SAMLIdentityProviderFactory; import org.keycloak.broker.saml.mappers.AbstractAttributeToRoleMapper; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @AutoService(IdentityProviderMapper.class) public class AcmeSamlRoleImporter extends AbstractAttributeToRoleMapper { public static final String PROVIDER_ID = "acme-saml-dynamic-role-idp-mapper"; public static final String ROLE_ATTRIBUTE = "roleAttribute"; public static final String FILTER_PATTERN = "roleNameFilterPattern"; public static final String EXTRACTION_PATTERN = "roleNameExtractionPattern"; private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID}; private static final List configProperties = new ArrayList<>(); static { ProviderConfigProperty property; property = new ProviderConfigProperty(); property.setName(ROLE_ATTRIBUTE); property.setLabel("Role Attribute"); property.setDefaultValue("Roles"); property.setHelpText("Name of the attributes to search for in SAML assertion."); property.setType(ProviderConfigProperty.STRING_TYPE); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(FILTER_PATTERN); property.setLabel("Role filter pattern"); property.setType(ProviderConfigProperty.STRING_TYPE); property.setDefaultValue(""); property.setHelpText("If set, only roles that match the filter pattern are included."); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(EXTRACTION_PATTERN); property.setLabel("Role extraction pattern"); property.setType(ProviderConfigProperty.STRING_TYPE); property.setDefaultValue(""); property.setHelpText("If set, the result of the first group match will be used as rolename"); configProperties.add(property); } @Override public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); } @Override public List getConfigProperties() { return configProperties; } @Override public String getId() { return PROVIDER_ID; } @Override public String[] getCompatibleProviders() { return COMPATIBLE_PROVIDERS; } @Override public String getDisplayCategory() { return "Role Importer"; } @Override public String getDisplayType() { return "Acme SAML Role Importer."; } @Override public String getHelpText() { return "Maps incoming roles based on a filter pattern to existing roles."; } @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { if (!this.applies(mapperModel, context)) { return; } List roles = getRolesForUser(context, mapperModel); roles.forEach(user::grantRole); } private List getRolesForUser(BrokeredIdentityContext context, IdentityProviderMapperModel mapperModel) { AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION); Set attributeAssertions = assertion.getAttributeStatements(); if (attributeAssertions == null) { return List.of(); } String filterPatternString = mapperModel.getConfig().getOrDefault(FILTER_PATTERN, ".*"); Pattern filterPattern = Pattern.compile(filterPatternString); String extractionPatternString = mapperModel.getConfig().getOrDefault(EXTRACTION_PATTERN, ".*"); Pattern extractionPattern = Pattern.compile(extractionPatternString); String roleAttribute = mapperModel.getConfig().getOrDefault(ROLE_ATTRIBUTE, "Role"); RealmModel realm = context.getAuthenticationSession().getRealm(); List roles = new ArrayList<>(); for (var attributeStatement : attributeAssertions) { for (var attr : attributeStatement.getAttributes()) { var attribute = attr.getAttribute(); if (!roleAttribute.equals(attribute.getName())) { continue; } for (var value : attribute.getAttributeValue()) { if (value == null) { continue; } String roleName = value.toString(); if (roleName.isBlank()) { continue; } if (!filterPattern.matcher(roleName).matches()) { continue; } Matcher matcher = extractionPattern.matcher(roleName); if (!matcher.matches()) { continue; } var extractedRoleName = matcher.group(1); RoleModel role = KeycloakModelUtils.getRoleFromString(realm, extractedRoleName); if (role == null) { continue; } roles.add(role); } } } return roles; } @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { if (!this.applies(mapperModel, context)) { return; } List roles = getRolesForUser(context, mapperModel); roles.forEach(user::grantRole); // RoleModel role = getRole(realm, mapperModel); // String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); // // KEYCLOAK-8730 if a previous mapper has already granted the same role, skip the checks so we don't accidentally remove a valid role. // if (!context.hasMapperGrantedRole(roleName)) { // if (this.applies(mapperModel, context)) { // context.addMapperGrantedRole(roleName); // user.grantRole(role); // } else { // user.deleteRoleMapping(role); // } // } } protected boolean applies(final IdentityProviderMapperModel mapperModel, final BrokeredIdentityContext context) { AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION); Set attributeAssertions = assertion.getAttributeStatements(); return attributeAssertions != null; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/saml/rolelist/AcmeSamlRoleListMapper.java ================================================ package com.github.thomasdarimont.keycloak.custom.saml.rolelist; import com.google.auto.service.AutoService; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.RoleUtils; import org.keycloak.protocol.ProtocolMapper; import org.keycloak.protocol.saml.SamlProtocol; import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper; import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; @AutoService(ProtocolMapper.class) public class AcmeSamlRoleListMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { public static final String PROVIDER_ID = "acme-saml-role-list-mapper"; public static final String SINGLE_ROLE_ATTRIBUTE = "single"; public static final String PREFIX_CLIENT_ROLES = "prefixClientRoles"; public static final String FILTER_PATTERN = "roleNameFilterPattern"; private static final List configProperties = new ArrayList<>(); static { ProviderConfigProperty property; property = new ProviderConfigProperty(); property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAME); property.setLabel("Role attribute name"); property.setDefaultValue("Role"); property.setHelpText("Name of the SAML attribute you want to put your roles into. i.e. 'Role', 'memberOf'."); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(AttributeStatementHelper.FRIENDLY_NAME); property.setLabel(AttributeStatementHelper.FRIENDLY_NAME_LABEL); property.setHelpText(AttributeStatementHelper.FRIENDLY_NAME_HELP_TEXT); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT); property.setLabel("SAML Attribute NameFormat"); property.setHelpText("SAML Attribute NameFormat. Can be basic, URI reference, or unspecified."); property.setType(ProviderConfigProperty.LIST_TYPE); property.setOptions(List.of(AttributeStatementHelper.BASIC, // AttributeStatementHelper.URI_REFERENCE, // AttributeStatementHelper.UNSPECIFIED)); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(SINGLE_ROLE_ATTRIBUTE); property.setLabel("Single Role Attribute"); property.setType(ProviderConfigProperty.BOOLEAN_TYPE); property.setDefaultValue("true"); property.setHelpText("If true, all roles will be stored under one attribute with multiple attribute values."); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(FILTER_PATTERN); property.setLabel("Role filter prefix"); property.setType(ProviderConfigProperty.STRING_TYPE); property.setDefaultValue(""); property.setHelpText("If set, only roles that match the filter pattern are included."); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(PREFIX_CLIENT_ROLES); property.setLabel("Prefix client roles with clientId"); property.setType(ProviderConfigProperty.BOOLEAN_TYPE); property.setDefaultValue("true"); property.setHelpText("If true, all client roles will be prefixed with the clientId followed by ':'"); configProperties.add(property); } @Override public String getDisplayCategory() { return "Role Mapper"; } @Override public String getDisplayType() { return "Acme SAML Role list"; } @Override public String getHelpText() { return "Role names are stored in an attribute value. There is either one attribute with multiple attribute values, or an attribute per role name depending on how you configure it. You can also specify the attribute name i.e. 'Role' or 'memberOf' being examples."; } @Override public List getConfigProperties() { return configProperties; } @Override public String getId() { return PROVIDER_ID; } @Override public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { var singleAttribute = Boolean.parseBoolean(mappingModel.getConfig().get(SINGLE_ROLE_ATTRIBUTE)); var prefixClientRoles = Boolean.parseBoolean(mappingModel.getConfig().get(PREFIX_CLIENT_ROLES)); AttributeType singleAttributeType = null; var allRoles = RoleUtils.expandCompositeRoles(userSession.getUser().getRoleMappingsStream().collect(Collectors.toSet())); String filterPatternString = mappingModel.getConfig().get(FILTER_PATTERN); Pattern filterPattern = null; if (filterPatternString != null && !filterPatternString.isBlank()) { filterPattern = Pattern.compile(filterPatternString); } for (RoleModel role : allRoles) { AttributeType attributeType; if (singleAttribute) { if (singleAttributeType == null) { singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel); attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(singleAttributeType)); } attributeType = singleAttributeType; } else { attributeType = AttributeStatementHelper.createAttributeType(mappingModel); attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType)); } var roleName = role.getName(); if (filterPattern != null) { if (!filterPattern.matcher(roleName).matches()) { continue; } } if (prefixClientRoles && role.isClientRole()) { roleName = ((ClientModel) role.getContainer()).getClientId() + ":" + roleName; } attributeType.addAttributeValue(roleName); } } public static ProtocolMapperModel create(String name, String samlAttributeName, String nameFormat, String friendlyName, boolean singleAttribute, boolean prefixClientRoles) { ProtocolMapperModel mapper = new ProtocolMapperModel(); mapper.setName(name); mapper.setProtocolMapper(PROVIDER_ID); mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL); Map config = new HashMap<>(); config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName); if (friendlyName != null) { config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName); } if (nameFormat != null) { config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, nameFormat); } config.put(SINGLE_ROLE_ATTRIBUTE, Boolean.toString(singleAttribute)); config.put(PREFIX_CLIENT_ROLES, Boolean.toString(prefixClientRoles)); mapper.setConfig(config); return mapper; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/ScheduledTaskProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.scheduling; import org.keycloak.provider.Provider; import org.keycloak.timer.ScheduledTask; public interface ScheduledTaskProvider extends Provider { ScheduledTask getScheduledTask(); long getInterval(); String getTaskName(); } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/ScheduledTaskProviderFactory.java ================================================ package com.github.thomasdarimont.keycloak.custom.scheduling; import lombok.extern.jbosslog.JBossLog; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.ProviderFactory; import org.keycloak.timer.TimerProvider; @JBossLog public abstract class ScheduledTaskProviderFactory implements ProviderFactory { private KeycloakSessionFactory keycloakSessionFactory; @Override public final void postInit(KeycloakSessionFactory keycloakSessionFactory) { this.keycloakSessionFactory = keycloakSessionFactory; keycloakSessionFactory.register((event) -> { if (event instanceof PostMigrationEvent) { var session = keycloakSessionFactory.create(); var timerProvider = session.getProvider(TimerProvider.class); var scheduledTaskProvider = create(session); timerProvider.scheduleTask(scheduledTaskProvider.getScheduledTask(), scheduledTaskProvider.getInterval(), scheduledTaskProvider.getTaskName()); log.infof("Scheduled Task %s", scheduledTaskProvider.getTaskName()); } }); } @Override public final void close() { var session = keycloakSessionFactory.create(); var timerProvider = session.getProvider(TimerProvider.class); var scheduledTaskProvider = this.create(session); timerProvider.cancelTask(scheduledTaskProvider.getTaskName()); log.infof("Cancelled Task %s", scheduledTaskProvider.getTaskName()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/ScheduledTaskSpi.java ================================================ package com.github.thomasdarimont.keycloak.custom.scheduling; import com.google.auto.service.AutoService; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; @AutoService(Spi.class) public final class ScheduledTaskSpi implements Spi { @Override public boolean isInternal() { return true; } @Override public String getName() { return "acme-scheduled-task"; } @Override public Class getProviderClass() { return ScheduledTaskProvider.class; } @Override public Class> getProviderFactoryClass() { return ScheduledTaskProviderFactory.class; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/scheduling/tasks/AcmeScheduledTaskProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.scheduling.tasks; import com.github.thomasdarimont.keycloak.custom.scheduling.ScheduledTaskProvider; import com.github.thomasdarimont.keycloak.custom.scheduling.ScheduledTaskProviderFactory; import com.google.auto.service.AutoService; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.ServerInfoAwareProviderFactory; import org.keycloak.timer.ScheduledTask; import java.time.Duration; import java.util.Map; @JBossLog @RequiredArgsConstructor public class AcmeScheduledTaskProvider implements ScheduledTaskProvider { private final String taskName; private final Duration interval; @Override public ScheduledTask getScheduledTask() { return (session -> { // do something with the cluster // ClusterProvider cluster = session.getProvider(ClusterProvider.class); // int taskTimeoutSeconds = 1000; // String taskKey = getTaskName() + "::scheduled"; // cluster.executeIfNotExecuted(taskKey, taskTimeoutSeconds, () -> { // // do something here // return null; // }); // do something here on every instance log.infof("Running %s", getTaskName()); }); } @Override public long getInterval() { return interval.toMillis(); } @Override public String getTaskName() { return taskName; } @Override public void close() { // NOOP } @AutoService(ScheduledTaskProviderFactory.class) public static class Factory extends ScheduledTaskProviderFactory implements ServerInfoAwareProviderFactory, EnvironmentDependentProviderFactory { private Duration interval; private String taskName; @Override public String getId() { return "acme-custom-task"; } @Override public ScheduledTaskProvider create(KeycloakSession session) { return new AcmeScheduledTaskProvider(taskName, interval); } @Override public void init(Config.Scope config) { interval = Duration.ofMillis(config.getLong("interval", 60000L)); taskName = config.get("task-name", "acme-custom-task"); } @Override public Map getOperationalInfo() { String version = getClass().getPackage().getImplementationVersion(); if (version == null) { version = "dev-snapshot"; } return Map.of("taskName", taskName, "interval", interval.toString(), "version", version); } @Override public boolean isSupported(Config.Scope config) { return Boolean.getBoolean("acme.scheduling.enabled"); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/filter/IpAccessFilter.java ================================================ package com.github.thomasdarimont.keycloak.custom.security.filter; import io.netty.handler.ipfilter.IpFilterRuleType; import io.netty.handler.ipfilter.IpSubnetFilterRule; import io.vertx.core.http.HttpServerRequest; import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.ext.Provider; import lombok.Data; import lombok.extern.jbosslog.JBossLog; import org.eclipse.microprofile.config.Config; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.utils.StringUtil; import java.net.InetSocketAddress; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; /** * Filter to restrict access to Keycloak Endpoints via CIDR IP ranges. */ @JBossLog @Provider public class IpAccessFilter implements ContainerRequestFilter { public static final String DEFAULT_IP_FILTER_RULES = "127.0.0.1/24,192.168.80.1/16,172.0.0.1/8"; public static final String ADMIN_IP_FILTER_RULES_ALLOW = "acme.keycloak.admin.ip-filter-rules.allow"; public static final ForbiddenException FORBIDDEN_EXCEPTION = new ForbiddenException(); public static final Pattern SLASH_SPLIT_PATTERN = Pattern.compile("/"); public static final Pattern COMMA_SPLIT_PATTERN = Pattern.compile(","); private final PathIpFilterRules adminPathIpFilterRules; @Context private HttpServerRequest httpServerRequest; public IpAccessFilter() { this.adminPathIpFilterRules = createAdminIpFilterRules(Configuration.getConfig()); } private PathIpFilterRules createAdminIpFilterRules(Config config) { var contextPath = config.getValue("quarkus.http.root-path", String.class); var adminPath = makeContextPath(contextPath, "admin"); var filterRules = config // .getOptionalValue(ADMIN_IP_FILTER_RULES_ALLOW, String.class) // .orElse(DEFAULT_IP_FILTER_RULES); if (StringUtil.isBlank(filterRules)) { return null; } var rules = new LinkedHashSet(); var ruleType = IpFilterRuleType.ACCEPT; var ruleDefinitions = List.of(COMMA_SPLIT_PATTERN.split(filterRules)); for (var rule : ruleDefinitions) { var ipAndCidrPrefix = SLASH_SPLIT_PATTERN.split(rule); var ip = ipAndCidrPrefix[0]; var cidrPrefix = Integer.parseInt(ipAndCidrPrefix[1]); rules.add(new IpSubnetFilterRule(ip, cidrPrefix, ruleType)); } var ruleDescription = adminPath + " " + ruleType + " from " + String.join(",", ruleDefinitions); var pathIpFilterRules = new PathIpFilterRules(ruleDescription, adminPath, Set.copyOf(rules)); log.infof("Created Security Filter rules for %s", pathIpFilterRules); return pathIpFilterRules; } private String makeContextPath(String contextPath, String subPath) { if (contextPath.endsWith("/")) { return contextPath + subPath; } return contextPath + "/" + subPath; } @Override public void filter(ContainerRequestContext requestContext) { if (adminPathIpFilterRules == null) { return; } var requestUri = requestContext.getUriInfo().getRequestUri(); log.tracef("Processing request: %s", requestUri); var requestPath = requestUri.getPath(); if (requestPath.startsWith(adminPathIpFilterRules.getPathPrefix())) { if (!isAdminRequestAllowed()) { throw FORBIDDEN_EXCEPTION; } } } private boolean isAdminRequestAllowed() { var remoteIp = httpServerRequest.connection().remoteAddress(); var address = new InetSocketAddress(remoteIp.host(), remoteIp.port()); for (var filterRule : adminPathIpFilterRules.getIpFilterRules()) { if (filterRule.matches(address)) { return true; } } return false; } @Data static class PathIpFilterRules { private final String ruleDescription; private final String pathPrefix; private final Set ipFilterRules; public String toString() { return ruleDescription; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptcha.java ================================================ package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha; import jakarta.ws.rs.core.MultivaluedMap; import lombok.Data; import lombok.Getter; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; import java.util.Locale; /** * FriendlyCaptcha Facade */ @Getter public class FriendlyCaptcha { public static final String FRIENDLY_CAPTCHA_SOLUTION_MISSING_MESSAGE = "friendly-captcha-solution-missing"; public static final String FRIENDLY_CAPTCHA_SOLUTION_INVALID_MESSAGE = "friendly-captcha-solution-invalid"; private final FriendlyCaptchaConfig config; private final FriendlyCaptchaClient client; public FriendlyCaptcha(KeycloakSession session, FriendlyCaptchaConfig config) { this.config = config; this.client = new FriendlyCaptchaClient(session, config); } public FriendlyCaptcha(KeycloakSession session) { this(session, new FriendlyCaptchaConfig(session.getContext().getRealm())); } public void configureForm(LoginFormsProvider form, Locale locale) { form.setAttribute("friendlyCaptchaEnabled", config.isEnabled()); form.setAttribute("friendlyCaptchaSiteKey", config.getSiteKey()); form.setAttribute("friendlyCaptchaStart", config.getStart()); form.setAttribute("friendlyCaptchaLang", locale.getLanguage()); form.setAttribute("friendlyCaptchaSourceModule", config.getSourceModule()); form.setAttribute("friendlyCaptchaSourceNoModule", config.getSourceNoModule()); form.setAttribute("friendlyCaptchaSolutionFieldName", config.getSolutionFieldName()); } public boolean isEnabled() { return config.isEnabled(); } public boolean verifySolution(String solutionValue) { return client.verifySolution(solutionValue); } public VerificationResult verifySolution(MultivaluedMap formData) { var solutionFieldName = config.getSolutionFieldName(); var solutionValue = formData.getFirst(solutionFieldName); if (solutionValue == null) { return new VerificationResult(false, FRIENDLY_CAPTCHA_SOLUTION_MISSING_MESSAGE); } var valid = verifySolution(solutionValue); if (!valid) { return new VerificationResult(false, FRIENDLY_CAPTCHA_SOLUTION_INVALID_MESSAGE); } return VerificationResult.OK; } @Data public static class VerificationResult { public static final VerificationResult OK = new VerificationResult(true, null); private final boolean successful; private final String errorMessage; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptchaClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha; import lombok.extern.jbosslog.JBossLog; import org.keycloak.http.simple.SimpleHttp; import org.keycloak.models.KeycloakSession; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * FriendlyCaptcha client to verify a captcha solution. */ @JBossLog public class FriendlyCaptchaClient { private final KeycloakSession session; private final FriendlyCaptchaConfig config; public FriendlyCaptchaClient(KeycloakSession session, FriendlyCaptchaConfig config) { this.session = session; this.config = config; } public boolean verifySolution(String solutionValue) { // see: https://docs.friendlycaptcha.com/#/verification_api var requestBody = new HashMap(); requestBody.put("solution", solutionValue); requestBody.put("sitekey", config.getSiteKey()); requestBody.put("secret", config.getSecret()); var post = SimpleHttp.create(session).doPost(config.getUrl()); post.json(requestBody); try (var response = post.asResponse()) { var responseBody = response.asJson(Map.class); if (Boolean.parseBoolean(String.valueOf(responseBody.get("success")))) { log.debugf("Captcha verification service returned success. status=%s", response.getStatus()); return true; } else { log.warnf("Captcha verification returned error. status=%s, errors=%s", response.getStatus(), responseBody.get("errors")); } } catch (IOException e) { log.error("Could not access captcha verification service.", e); } return false; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptchaConfig.java ================================================ package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import org.keycloak.models.RealmModel; public class FriendlyCaptchaConfig extends RealmConfig { public static final String DEFAULT_VERIFICATION_URL = "https://api.friendlycaptcha.com/api/v1/siteverify"; public FriendlyCaptchaConfig(RealmModel realm) { super(realm); } public String getSiteKey() { return getString("friendlyCaptchaSiteKey"); } public String getSolutionFieldName() { return getString("friendlyCaptchaSolutionFieldName"); } public String getSecret() { return getString("friendlyCaptchaSecret"); } public String getStart() { return getString("friendlyCaptchaStart"); } public boolean isEnabled() { return getBoolean("friendlyCaptchaEnabled", false); } public String getSourceModule() { return getString("friendlyCaptchaSourceModule"); } public String getSourceNoModule() { return getString("friendlyCaptchaSourceNoModule"); } public String getUrl() { return getString("friendlyCaptchaVerificationUrl", DEFAULT_VERIFICATION_URL); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/security/friendlycaptcha/FriendlyCaptchaFormAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.security.friendlycaptcha; import com.github.thomasdarimont.keycloak.custom.support.LocaleUtils; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.authentication.FormAction; import org.keycloak.authentication.FormActionFactory; import org.keycloak.authentication.FormContext; import org.keycloak.authentication.ValidationContext; import org.keycloak.events.Errors; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.ProviderConfigProperty; import java.util.Collections; import java.util.List; @JBossLog public class FriendlyCaptchaFormAction implements FormAction { @Override public void buildPage(FormContext context, LoginFormsProvider form) { var locale = LocaleUtils.extractLocaleWithFallbackToRealmLocale(context.getHttpRequest(), context.getRealm()); var captcha = new FriendlyCaptcha(context.getSession()); captcha.configureForm(form, locale); } @Override public void validate(ValidationContext context) { var captcha = new FriendlyCaptcha(context.getSession()); var formData = context.getHttpRequest().getDecodedFormParameters(); var solutionFieldName = captcha.getConfig().getSolutionFieldName(); var verificationResult = captcha.verifySolution(formData); if (!verificationResult.isSuccessful()) { String errorMessage = verificationResult.getErrorMessage(); context.error(Errors.INVALID_REGISTRATION); formData.remove(solutionFieldName); context.validationError(formData, List.of(new FormMessage(solutionFieldName, errorMessage))); return; } context.success(); } @Override public void success(FormContext context) { log.debug("Friendly captcha verification successful"); } @Override public boolean requiresUser() { return false; } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return false; } @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { // NOOP } @Override public void close() { // NOOP } @AutoService(FormActionFactory.class) public static class Factory implements FormActionFactory { private static final FriendlyCaptchaFormAction INSTANCE = new FriendlyCaptchaFormAction(); public static final String ID = "acme-friendly-captcha-form-action"; @Override public String getId() { return ID; } @Override public String getDisplayType() { return "Acme: Friendly Captcha"; } @Override public String getHelpText() { return "Generates a friendly captcha."; } @Override public String getReferenceCategory() { return "Post Processing"; } @Override public boolean isConfigurable() { return false; } @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override public boolean isUserSetupAllowed() { return false; } @Override public List getConfigProperties() { return Collections.emptyList(); } @Override public FormAction create(KeycloakSession session) { return INSTANCE; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/AuthUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import jakarta.ws.rs.core.Response; import org.keycloak.models.KeycloakSession; import org.keycloak.services.ErrorResponse; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.resources.admin.AdminAuth; import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.fgap.AdminPermissions; public class AuthUtils { public static AdminPermissionEvaluator getAdminPermissionEvaluator(KeycloakSession session) { return AdminPermissions.evaluator(session, session.getContext().getRealm(), getAdminAuth(session)); } public static AdminAuth getAdminAuth(KeycloakSession session) { AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session).authenticate(); if (authResult == null) { throw ErrorResponse.error("invalid_token", Response.Status.UNAUTHORIZED); } return new AdminAuth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), authResult.getClient()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/ConfigUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.models.AuthenticatorConfigModel; import java.util.Map; public class ConfigUtils { public static Map getConfig(AuthenticatorConfigModel configModel, Map defaultConfig) { if (configModel == null) { return defaultConfig; } Map config = configModel.getConfig(); if (config == null) { return defaultConfig; } return config; } public static String getConfigValue(AuthenticatorConfigModel configModel, String key, String defaultValue) { if (configModel == null) { return defaultValue; } return getConfigValue(configModel.getConfig(), key, defaultValue); } public static String getConfigValue(Map config, String key, String defaultValue) { if (config == null) { return defaultValue; } return config.getOrDefault(key, defaultValue); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/CookieHelper.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.NewCookie; import org.keycloak.http.HttpResponse; import org.keycloak.models.KeycloakSession; import java.util.Map; public class CookieHelper { public static final String LEGACY_COOKIE = "_LEGACY"; /** * Set a response cookie. This solely exists because JAX-RS 1.1 does not support setting HttpOnly cookies * @param name * @param value * @param path * @param domain * @param comment * @param maxAge * @param secure * @param httpOnly * @param sameSite */ public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, NewCookie.SameSite sameSite, KeycloakSession session) { NewCookie.SameSite sameSiteParam = sameSite; // when expiring a cookie we shouldn't set the sameSite attribute; if we set e.g. SameSite=None when expiring a cookie, the new cookie (with maxAge == 0) // might be rejected by the browser in some cases resulting in leaving the original cookie untouched; that can even prevent user from accessing their application if (maxAge == 0) { sameSite = null; } boolean secure_sameSite = sameSite == NewCookie.SameSite.NONE || secure; // when SameSite=None, Secure attribute must be set HttpResponse response = session.getContext().getHttpResponse(); NewCookie cookie = new NewCookie.Builder(name) // .value(value) // .path(path) // .domain(domain) // .comment(comment) // .maxAge(maxAge) // .secure(secure_sameSite) // .sameSite(sameSite) // .httpOnly(httpOnly) .build(); response.setCookieIfAbsent(cookie); // a workaround for browser in older Apple OSs – browsers ignore cookies with SameSite=None if (sameSiteParam == NewCookie.SameSite.NONE) { addCookie(name + LEGACY_COOKIE, value, path, domain, comment, maxAge, secure, httpOnly, null, session); } } /** * Set a response cookie avoiding SameSite parameter * @param name * @param value * @param path * @param domain * @param comment * @param maxAge * @param secure * @param httpOnly */ public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly, KeycloakSession session) { addCookie(name, value, path, domain, comment, maxAge, secure, httpOnly, null, session); } public static String getCookieValue(KeycloakSession session, String name) { Map cookies = session.getContext().getRequestHeaders().getCookies(); Cookie cookie = cookies.get(name); if (cookie == null) { String legacy = name + LEGACY_COOKIE; cookie = cookies.get(legacy); } return cookie != null ? cookie.getValue() : null; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/CookieUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.UriBuilder; import org.keycloak.common.ClientConnection; import org.keycloak.http.HttpRequest; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; public class CookieUtils { public static String parseCookie(String cookieName, HttpRequest httpRequest) { Cookie cookie = httpRequest.getHttpHeaders().getCookies().get(cookieName); if (cookie == null) { return null; } return cookie.getValue(); } public static void addCookie(String cookieName, String cookieValue, KeycloakSession session, RealmModel realm, int maxAge) { UriBuilder baseUriBuilder = session.getContext().getUri().getBaseUriBuilder(); // TODO think about narrowing the cookie-path to only contain the /auth path. String path = baseUriBuilder.path("realms").path(realm.getName()).path("/").build().getPath(); ClientConnection connection = session.getContext().getConnection(); boolean secure = realm.getSslRequired().isRequired(connection); CookieHelper.addCookie(cookieName, cookieValue, // path, // null,// domain null, // comment maxAge, // secure, // true, // httponly secure ? NewCookie.SameSite.NONE : null, // same-site session); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/CredentialUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.credential.CredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; import java.util.Optional; public class CredentialUtils { public static Optional findFirstOtpCredential(UserModel user) { return findFirstCredentialOfType(user, OTPCredentialModel.TYPE); } public static Optional findFirstCredentialOfType(UserModel user, String type) { return user.credentialManager().getStoredCredentialsByTypeStream(type).findFirst(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/LocaleUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.http.HttpRequest; import org.keycloak.models.RealmModel; import java.util.Locale; public class LocaleUtils { public static Locale extractLocaleWithFallbackToRealmLocale(HttpRequest request, RealmModel realm) { if (request == null && realm == null) { return Locale.getDefault(); } if (request == null) { return new Locale(realm.getDefaultLocale()); } String kcLocale = request.getUri().getQueryParameters().getFirst("kc_locale"); if (kcLocale != null ){ return new Locale(kcLocale); } return request.getHttpHeaders().getAcceptableLanguages().stream().findFirst().orElseGet(() -> new Locale(realm.getDefaultLocale())); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/RealmUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.models.RealmModel; public class RealmUtils { public static String getDisplayName(RealmModel realm) { var displayName = realm.getDisplayName(); if (displayName == null) { displayName = realm.getName(); } return displayName; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/RequiredActionUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.http.HttpRequest; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.sessions.AuthenticationSessionModel; import jakarta.ws.rs.core.MultivaluedMap; import java.util.function.Consumer; public class RequiredActionUtils { public static boolean isCancelApplicationInitiatedAction(RequiredActionContext context) { HttpRequest httpRequest = context.getHttpRequest(); MultivaluedMap formParams = httpRequest.getDecodedFormParameters(); return formParams.containsKey(LoginActionsService.CANCEL_AIA); } public static void cancelApplicationInitiatedAction(RequiredActionContext context, String actionProviderId, Consumer cleanup) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); AuthenticationManager.setKcActionStatus(actionProviderId, RequiredActionContext.KcActionStatus.CANCELLED, authSession); cleanup.accept(authSession); context.success(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/ScopeUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import java.util.List; public class ScopeUtils { public static final String SCOPE_ACME_AGE_INFO = "acme.ageinfo"; public static boolean hasScope(String requiredScope, String scopeParam) { if (scopeParam == null || scopeParam.isBlank()) { return false; } return List.of(scopeParam.split(" ")).contains(requiredScope); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/TokenUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.common.ClientConnection; import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel; import java.util.function.Consumer; import static org.keycloak.models.UserSessionModel.SessionPersistenceState.TRANSIENT; public class TokenUtils { /** * Generates a service account access token for the given clientId. * * @param session * @param clientId * @param scope * @param tokenAdjuster * @return */ public static String generateServiceAccountAccessToken(KeycloakSession session, String clientId, String scope, Consumer tokenAdjuster) { var context = session.getContext(); var realm = context.getRealm(); var client = session.clients().getClientByClientId(realm, clientId); if (client == null) { throw new IllegalStateException("client not found"); } if (!client.isServiceAccountsEnabled()) { throw new IllegalStateException("service account not enabled"); } var clientUser = session.users().getServiceAccount(client); var clientUsername = clientUser.getUsername(); // we need to remember the current authSession since createAuthenticationSession changes the current authSession in the context var currentAuthSession = context.getAuthenticationSession(); try { var rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); var authSession = rootAuthSession.createAuthenticationSession(client); authSession.setAuthenticatedUser(clientUser); authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName())); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); var clientConnection = context.getConnection(); var sessionId = authSession.getParentSession().getId(); var remoteAddr = clientConnection.getRemoteAddr(); var userSession = session.sessions().createUserSession(sessionId, realm, clientUser, clientUsername, // remoteAddr, ServiceAccountConstants.CLIENT_AUTH, false, null, null, TRANSIENT); AuthenticationManager.setClientScopesInSession(session, authSession); var clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession); // Notes about client details userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId()); userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost()); userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, remoteAddr); var tokenManager = new TokenManager(); var event = new EventBuilder(realm, session, clientConnection); var responseBuilder = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSessionCtx); responseBuilder.generateAccessToken(); if (tokenAdjuster != null) { tokenAdjuster.accept(responseBuilder.getAccessToken()); } var accessTokenResponse = responseBuilder.build(); return accessTokenResponse.getToken(); } finally { // reset current authentication session context.setAuthenticationSession(currentAuthSession); } } public static String generateAccessToken(KeycloakSession session, UserSessionModel userSession, String clientId, String scope, Consumer tokenAdjuster) { KeycloakContext context = session.getContext(); RealmModel realm = userSession.getRealm(); ClientModel client = session.clients().getClientByClientId(realm, clientId); String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()); RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); AuthenticationSessionModel iamAuthSession = rootAuthSession.createAuthenticationSession(client); iamAuthSession.setAuthenticatedUser(userSession.getUser()); iamAuthSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); iamAuthSession.setClientNote(OIDCLoginProtocol.ISSUER, issuer); iamAuthSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); ClientConnection connection = context.getConnection(); UserSessionModel iamUserSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, userSession.getUser(), userSession.getUser().getUsername(), connection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, TRANSIENT); AuthenticationManager.setClientScopesInSession(session, iamAuthSession); ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, iamUserSession, iamAuthSession); // Notes about client details userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId()); userSession.setNote(ServiceAccountConstants.CLIENT_HOST, connection.getRemoteHost()); userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, connection.getRemoteAddr()); TokenManager tokenManager = new TokenManager(); EventBuilder eventBuilder = new EventBuilder(realm, session, connection); TokenManager.AccessTokenResponseBuilder tokenResponseBuilder = tokenManager.responseBuilder(realm, client, eventBuilder, session, iamUserSession, clientSessionCtx); AccessToken accessToken = tokenResponseBuilder.generateAccessToken().getAccessToken(); if (tokenAdjuster != null) { tokenAdjuster.accept(accessToken); } AccessTokenResponse tokenResponse = tokenResponseBuilder.build(); return tokenResponse.getToken(); } public static String generateAccessToken(KeycloakSession session, RealmModel realm, UserModel user, String clientId, String scope, Consumer tokenAdjuster) { KeycloakContext context = session.getContext(); ClientModel client = session.clients().getClientByClientId(realm, clientId); String issuer = Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()); RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); AuthenticationSessionModel iamAuthSession = rootAuthSession.createAuthenticationSession(client); iamAuthSession.setAuthenticatedUser(user); iamAuthSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); iamAuthSession.setClientNote(OIDCLoginProtocol.ISSUER, issuer); iamAuthSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); ClientConnection connection = context.getConnection(); UserSessionModel iamUserSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, user, user.getUsername(), connection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, TRANSIENT); AuthenticationManager.setClientScopesInSession(session, iamAuthSession); ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, iamUserSession, iamAuthSession); // Notes about client details iamUserSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId()); iamUserSession.setNote(ServiceAccountConstants.CLIENT_HOST, connection.getRemoteHost()); iamUserSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, connection.getRemoteAddr()); TokenManager tokenManager = new TokenManager(); EventBuilder eventBuilder = new EventBuilder(realm, session, connection); TokenManager.AccessTokenResponseBuilder tokenResponseBuilder = tokenManager.responseBuilder(realm, client, eventBuilder, session, iamUserSession, clientSessionCtx); AccessToken accessToken = tokenResponseBuilder.generateAccessToken().getAccessToken(); if (tokenAdjuster != null) { tokenAdjuster.accept(accessToken); } AccessTokenResponse tokenResponse = tokenResponseBuilder.build(); return tokenResponse.getToken(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/UserSessionUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; import org.keycloak.sessions.AuthenticationSessionModel; public class UserSessionUtils { public static UserSessionModel getUserSessionFromAuthenticationSession(KeycloakSession session, AuthenticationSessionModel authSession) { return session.sessions().getUserSession(authSession.getRealm(), authSession.getParentSession().getId()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/support/UserUtils.java ================================================ package com.github.thomasdarimont.keycloak.custom.support; import org.keycloak.models.UserModel; public class UserUtils { public static String deriveDisplayName(UserModel user) { String displayName; if (user.getFirstName() != null && user.getLastName() != null) { displayName = user.getFirstName().trim() + " " + user.getLastName().trim(); } else if(user.getFirstName() != null) { displayName = user.getFirstName().trim(); } else if (user.getUsername() != null) { displayName = user.getUsername(); } else { displayName = user.getEmail(); } return displayName; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/terms/AcmeTermsAndConditionsAction.java ================================================ package com.github.thomasdarimont.keycloak.custom.terms; import com.google.auto.service.AutoService; import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import jakarta.ws.rs.core.Response; import java.util.List; import java.util.Optional; @AutoService(RequiredActionFactory.class) public class AcmeTermsAndConditionsAction implements RequiredActionProvider, RequiredActionFactory { public static final String PROVIDER_ID = "acme_terms_and_conditions"; public static final String TERMS_USER_ATTRIBUTE = "acme_terms_accepted"; public static final String TERMS_FORM_FTL = "terms.ftl"; public static final String TERMS_DEFAULT_ID = "acme_terms"; private static final String TERMS_CURRENT_ID = Optional.ofNullable(System.getenv("KEYCLOAK_ACME_TERMS_ID")).orElse(TERMS_DEFAULT_ID); private static final String TERMS_ID_SPLITTER = "@"; @Override public RequiredActionProvider create(KeycloakSession session) { return this; } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public String getId() { return PROVIDER_ID; } @Override public void evaluateTriggers(RequiredActionContext context) { if (hasUserAcceptedCurrentTerms(context)) { context.getUser().removeRequiredAction(PROVIDER_ID); } else { context.getUser().addRequiredAction(PROVIDER_ID); } } private boolean hasUserAcceptedCurrentTerms(RequiredActionContext context) { String termsAttribute = context.getUser().getFirstAttribute(TERMS_USER_ATTRIBUTE); return termsAttribute != null // user has accepted terms at all && termsAttribute.startsWith(getActiveTermsId() + TERMS_ID_SPLITTER); // user has accepted current terms } private String getActiveTermsId() { return TERMS_CURRENT_ID; } @Override public void requiredActionChallenge(RequiredActionContext context) { Response challenge = context.form() .setAttribute("terms_id", getActiveTermsId()) .createForm(TERMS_FORM_FTL); context.challenge(challenge); } @Override public void processAction(RequiredActionContext context) { if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) { context.getUser().removeAttribute(TERMS_USER_ATTRIBUTE); context.failure(); return; } // Record acceptance of current version of terms and conditions context.getUser().setAttribute(TERMS_USER_ATTRIBUTE, List.of(getActiveTermsId() + TERMS_ID_SPLITTER + Time.currentTime())); context.success(); } @Override public String getDisplayText() { return "Acme: Terms and Conditions"; } @Override public void close() { // NOOP } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/themes/login/AcmeFreeMarkerLoginFormsProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.themes.login; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProviderFactory; import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProvider; import org.keycloak.forms.login.freemarker.FreeMarkerLoginFormsProviderFactory; import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean; import org.keycloak.forms.login.freemarker.model.ClientBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.theme.Theme; import jakarta.ws.rs.core.Response; import java.util.Locale; /** * Custom {@link FreeMarkerLoginFormsProvider} to adjust the login form rendering context. */ @JBossLog public class AcmeFreeMarkerLoginFormsProvider extends FreeMarkerLoginFormsProvider { public AcmeFreeMarkerLoginFormsProvider(KeycloakSession session) { super(session); } @Override protected Response processTemplate(Theme theme, String templateName, Locale locale) { // expose custom objects in the template rendering via super.attributes var authBean = (AuthenticationContextBean) attributes.get("auth"); attributes.put("acmeLogin", new AcmeLoginBean(session, authBean)); var clientBean = (ClientBean) attributes.get("client"); attributes.put("acmeUrl", new AcmeUrlBean(session, clientBean)); // TODO remove hack for custom profile fields if (attributes.containsKey("customProfile")) { attributes.put("profile", attributes.get("customProfile")); } return super.processTemplate(theme, templateName, locale); } @AutoService(LoginFormsProviderFactory.class) public static class Factory extends FreeMarkerLoginFormsProviderFactory { @Override public LoginFormsProvider create(KeycloakSession session) { return new AcmeFreeMarkerLoginFormsProvider(session); } @Override public void init(Config.Scope config) { // NOOP } @Override public void postInit(KeycloakSessionFactory factory) { // NOOP } @Override public void close() { // NOOP } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/themes/login/AcmeLoginBean.java ================================================ package com.github.thomasdarimont.keycloak.custom.themes.login; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticationSelectionOption; import org.keycloak.authentication.Authenticator; import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.PasswordPolicy; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class AcmeLoginBean { private final KeycloakSession session; private final AuthenticationContextBean authBean; public AcmeLoginBean(KeycloakSession session, AuthenticationContextBean authBean) { this.session = session; this.authBean = authBean; } /** * Called from "select-authenticator.ftl" to narrow the available authentication options for the current user. * * @return */ public List getAuthenticationSelections() { return narrowUserAuthenticationOptions(authBean.getAuthenticationSelections()); } private List narrowUserAuthenticationOptions(List availableOptions) { KeycloakContext context = session.getContext(); RealmModel realm = context.getRealm(); UserModel user = context.getAuthenticationSession().getAuthenticatedUser(); List elegibleOptions = availableOptions.stream() // filter elegible options for user .filter(option -> { AuthenticationExecutionModel authExecution = option.getAuthenticationExecution(); Authenticator authenticator = session.getProvider(Authenticator.class, authExecution.getAuthenticator()); if (!authenticator.requiresUser()) { return true; } boolean configured = authenticator.configuredFor(session, realm, user); return configured; }) // sort by priority from authentication flow .sorted(Comparator.comparing(option -> option.getAuthenticationExecution().getPriority())) .collect(Collectors.toList()); return elegibleOptions; } public String getPasswordPolicy() { PasswordPolicy passwordPolicy = session.getContext().getRealm().getPasswordPolicy(); if (passwordPolicy == null) { return null; } return passwordPolicy.toString(); } public String getLastProcessedAction() { return session.getContext().getAuthenticationSession().getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/themes/login/AcmeUrlBean.java ================================================ package com.github.thomasdarimont.keycloak.custom.themes.login; import com.github.thomasdarimont.keycloak.custom.config.ClientConfig; import com.github.thomasdarimont.keycloak.custom.config.RealmConfig; import org.keycloak.forms.login.freemarker.model.ClientBean; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import java.util.Optional; public class AcmeUrlBean { private static final String ACME_SITE_URL_KEY = "acme_site_url"; private static final String ACME_TERMS_URL_REALM_ATTRIBUTE_KEY = "acme_terms_url"; private static final String ACME_TERMS_URL_CLIENT_ATTRIBUTE_KEY = "tosUri"; private static final String ACME_IMPRINT_URL_KEY = "acme_imprint_url"; private static final String ACME_PRIVACY_URL_REALM_ATTRIBUTE_KEY = "acme_privacy_url"; private static final String ACME_PRIVACY_URL_CLIENT_ATTRIBUTE_KEY = "policyUri"; private static final String ACME_LOGO_URL_REALM_ATTRIBUTE_KEY = "acme_logo_url"; private static final String ACME_LOGO_URL_CLIENT_ATTRIBUTE_KEY = "logoUri"; private static final String ACME_ACCOUNT_DELETE_URL_KEY = "acme_account_deleted_url"; private final ClientConfig clientConfig; private final RealmConfig realmConfig; public AcmeUrlBean(KeycloakSession session) { this(session, null); } public AcmeUrlBean(KeycloakSession session, ClientBean clientBean) { var realm = session.getContext().getRealm(); this.realmConfig = new RealmConfig(realm); if (clientBean != null) { this.clientConfig = new ClientConfig(realm.getClientByClientId(clientBean.getClientId())); } else { this.clientConfig = null; } } /** * BEGIN: Used from freemarker */ public String getSiteUrl() { return realmConfig.getValue(ACME_SITE_URL_KEY); } public String getTermsUrl() { return clientAttribute(ACME_TERMS_URL_CLIENT_ATTRIBUTE_KEY) // .orElse(realmConfig.getValue(ACME_TERMS_URL_REALM_ATTRIBUTE_KEY)); } public String getPrivacyUrl() { return clientAttribute(ACME_PRIVACY_URL_CLIENT_ATTRIBUTE_KEY) // .orElse(realmConfig.getValue(ACME_PRIVACY_URL_REALM_ATTRIBUTE_KEY)); } public String getImprintUrl() { // there is no client specific imprint return realmConfig.getValue(ACME_IMPRINT_URL_KEY); } public String getLogoUrl() { return clientAttribute(ACME_LOGO_URL_CLIENT_ATTRIBUTE_KEY) // .orElse(realmConfig.getValue(ACME_LOGO_URL_REALM_ATTRIBUTE_KEY)); } public String getAccountDeletedUrl() { // there is no client specific delete url return realmConfig.getValue(ACME_ACCOUNT_DELETE_URL_KEY); } /** * END: Used from freemarker */ private Optional clientAttribute(String key) { if (this.clientConfig != null) { return Optional.ofNullable(this.clientConfig.getValue(key)); } return Optional.empty(); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/adhoc/AdhocUserStorageProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.adhoc; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.user.ImportSynchronization; import org.keycloak.storage.user.ImportedUserValidation; import org.keycloak.storage.user.SynchronizationResult; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserRegistrationProvider; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; import java.util.UUID; /** * Adhoc User storage that dynamically generates a local user for a lookup to ease load-tests, every password is valid, unless it starts with "invalid". * Lookups for usernames that starts with "notfound" will always fail. */ @JBossLog public class AdhocUserStorageProvider implements UserStorageProvider, // UserLookupProvider, // UserRegistrationProvider, // CredentialInputValidator, // ImportSynchronization, // ImportedUserValidation // validate imported users { public static final String ID = "adhoc"; private final KeycloakSession session; private final ComponentModel model; public AdhocUserStorageProvider(KeycloakSession session, ComponentModel model) { this.session = session; this.model = model; } @Override public void close() { // NOOP } @Override public UserModel getUserById(RealmModel realm, String id) { var jpaUserProvider = session.getProvider(UserProvider.class); return jpaUserProvider.getUserById(realm, id); } @Override public UserModel getUserByUsername(RealmModel realm, String username) { if (username.startsWith("notfound")) { return null; } var jpaUserProvider = session.getProvider(UserProvider.class); var jpaUser = jpaUserProvider.getUserByUsername(realm, username); if (jpaUser != null) { return jpaUser; } var userId = UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8)).toString(); var email = username + "@acme.test"; try { jpaUser = jpaUserProvider.addUser(realm, userId, username, true, false); jpaUser.setEmail(email); jpaUser.setFirstName("First " + username); jpaUser.setLastName("Last " + username); jpaUser.setEnabled(true); jpaUser.setFederationLink(model.getId()); } catch (Exception ex) { log.errorf(ex, "Failed to create ad-hoc local user during lookup. username=%s", username); } return jpaUser; } @Override public UserModel getUserByEmail(RealmModel realm, String email) { return getUserByUsername(realm, email); } @Override public boolean supportsCredentialType(String credentialType) { return PasswordCredentialModel.TYPE.equals(credentialType); } @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { return true; } @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { // accept all password for load test, except if the password starts with "invalid", then always reject the password. String challengeResponse = credentialInput.getChallengeResponse(); return challengeResponse == null || !challengeResponse.startsWith("invalid"); } @Override public UserModel addUser(RealmModel realm, String username) { // var jpaUserProvider = session.getProvider(UserProvider.class); // UserModel userModel = jpaUserProvider.addUser(realm, username); // userModel.setFederationLink(model.getId()); return null; } @Override public boolean removeUser(RealmModel realm, UserModel user) { var jpaUserProvider = session.getProvider(UserProvider.class); return jpaUserProvider.removeUser(realm, user); } @Override public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) { return null; } @Override public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) { return null; } @Override public UserModel validate(RealmModel realm, UserModel user) { log.debugf("Validate user. realm=%s userId=%s", realm.getName(), user.getId()); return user; } @SuppressWarnings("rawtypes") @AutoService(UserStorageProviderFactory.class) public static class Factory implements UserStorageProviderFactory { @Override public String getId() { return ID; } @Override public String getHelpText() { return "Generates requested users on the fly. Useful for load-testing. Username lookup will fail for username and emails beginning with 'notfound'. All provided passwords will be considered valid, unless they begin with 'invalid'."; } @Override public UserStorageProvider create(KeycloakSession session) { // incorrectly callend when session.getComponentProvider(...) is used. return UserStorageProviderFactory.super.create(session); } @Override public AdhocUserStorageProvider create(KeycloakSession session, ComponentModel model) { return new AdhocUserStorageProvider(session, model); } @Override public List getConfigProperties() { return UserStorageProviderFactory.super.getConfigProperties(); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/ldap/AcmeLDAPStorageProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.ldap; import com.google.auto.service.AutoService; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; import org.keycloak.component.ComponentModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.ldap.LDAPIdentityStoreRegistry; import org.keycloak.storage.ldap.LDAPStorageProvider; import org.keycloak.storage.ldap.LDAPStorageProviderFactory; import org.keycloak.storage.ldap.idm.model.LDAPObject; import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore; import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator; import java.util.Map; import java.util.regex.Pattern; /** * Example for a custom {@link LDAPStorageProvider} which supports storing user attributes locally despite a read-only ldap connection. */ public class AcmeLDAPStorageProvider extends LDAPStorageProvider { private final Pattern localCustomAttributePattern; public AcmeLDAPStorageProvider(LDAPStorageProviderFactory factory, KeycloakSession session, ComponentModel model, LDAPIdentityStore ldapIdentityStore, Pattern localCustomAttributePattern) { super(factory, session, model, ldapIdentityStore); this.localCustomAttributePattern = localCustomAttributePattern; } @Override protected UserModel proxy(RealmModel realm, UserModel local, LDAPObject ldapObject, boolean newUser) { UserModel proxy = super.proxy(realm, local, ldapObject, newUser); return new AcmeReadonlyLDAPUserModelDelegate(proxy, localCustomAttributePattern); } @JBossLog @AutoService(UserStorageProviderFactory.class) public static class Factory extends LDAPStorageProviderFactory { private LDAPIdentityStoreRegistry ldapStoreRegistry; private Pattern localCustomAttributePattern; @Override public void init(Config.Scope config) { this.ldapStoreRegistry = new LDAPIdentityStoreRegistry(); String localCustomAttributePatternString = config.get("localCustomAttributePattern", "(custom-.*|foo)"); log.infof("Using local custom attribute pattern: %s", localCustomAttributePatternString); this.localCustomAttributePattern = Pattern.compile(localCustomAttributePatternString); } @Override public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) { Map configDecorators = getLDAPConfigDecorators(session, model); LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(session, model, configDecorators); return new AcmeLDAPStorageProvider(this, session, model, ldapIdentityStore, localCustomAttributePattern); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/ldap/AcmeReadonlyLDAPUserModelDelegate.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.ldap; import org.keycloak.models.UserModel; import org.keycloak.models.utils.UserModelDelegate; import org.keycloak.storage.ldap.ReadonlyLDAPUserModelDelegate; import java.util.List; import java.util.Set; import java.util.regex.Pattern; public class AcmeReadonlyLDAPUserModelDelegate extends ReadonlyLDAPUserModelDelegate { private final Pattern localCustomAttributePattern; public AcmeReadonlyLDAPUserModelDelegate(UserModel delegate, Pattern localCustomAttributePattern) { super(delegate); this.localCustomAttributePattern = localCustomAttributePattern; } @Override public void setAttribute(String name, List values) { if (localCustomAttributePattern.matcher(name).matches()) { UserModel rootDelegate = getRootDelegate(delegate); rootDelegate.setSingleAttribute(name, values.get(0)); return; } super.setAttribute(name, values); } @Override public void removeAttribute(String name) { if (localCustomAttributePattern.matcher(name).matches()) { UserModel rootDelegate = getRootDelegate(delegate); rootDelegate.removeAttribute(name); return; } super.removeAttribute(name); } /** * Unwrap deeply nested {@link UserModelDelegate UserModelDelegate's} * * @param delegate * @return */ private UserModel getRootDelegate(UserModel delegate) { UserModel current = delegate; while (current instanceof UserModelDelegate del) { current = del.getDelegate(); } return current; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/AcmeUserAdapter.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AcmeUser; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.storage.UserStorageUtil; import org.keycloak.storage.adapter.InMemoryUserAdapter; import org.keycloak.storage.federated.UserFederatedStorageProvider; import java.util.stream.Stream; public class AcmeUserAdapter extends InMemoryUserAdapter { public AcmeUserAdapter(KeycloakSession session, RealmModel realm, String id, AcmeUser acmeUser) { super(session, realm, id); setUsername(acmeUser.getUsername()); setFirstName(acmeUser.getFirstname()); setLastName(acmeUser.getLastname()); setEnabled(acmeUser.isEnabled()); setEmail(acmeUser.getEmail()); setEmailVerified(acmeUser.isEmailVerified()); } public UserFederatedStorageProvider getFederatedStorage() { return UserStorageUtil.userFederatedStorage(session); } @Override public void addRequiredAction(String action) { checkReadonly(); getFederatedStorage().addRequiredAction(realm, getId(), action); } @Override public void addRequiredAction(RequiredAction action) { addRequiredAction(action.name()); } @Override public void removeRequiredAction(String action) { checkReadonly(); getFederatedStorage().removeRequiredAction(realm, getId(), action); } @Override public void removeRequiredAction(RequiredAction action) { removeRequiredAction(action.name()); } @Override public Stream getRequiredActionsStream() { return getFederatedStorage().getRequiredActionsStream(realm, getId()); } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/AcmeUserStorageProvider.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote; import com.google.auto.service.AutoService; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AccountClientOptions; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AcmeAccountClient; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.AcmeUser; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.SimpleAcmeAccountClient; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.UserSearchInput; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.UserSearchInput.UserSearchOptions; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.VerifyCredentialsInput; import com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient.VerifyCredentialsOutput; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.keycloak.component.ComponentModel; import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.storage.StorageId; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.storage.user.ImportSynchronization; import org.keycloak.storage.user.SynchronizationResult; import org.keycloak.storage.user.UserCountMethodsProvider; import org.keycloak.storage.user.UserLookupProvider; import org.keycloak.storage.user.UserQueryProvider; import org.keycloak.storage.user.UserRegistrationProvider; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.stream.Stream; /** * Adhoc User storage that dynamically generates a local user for a lookup to ease load-tests, every password is valid, unless it starts with "invalid". * Lookups for usernames that starts with "notfound" will always fail. */ @JBossLog @RequiredArgsConstructor public class AcmeUserStorageProvider implements // UserStorageProvider, // marker interface UserLookupProvider, // lookup by id, username, email UserQueryProvider, // find / search for users UserRegistrationProvider, // add users UserCountMethodsProvider, // count users efficiently // CredentialInputValidator, // validate credentials ImportSynchronization // perform sync (sync, syncSince) // UserAttributeFederatedStorage { public static final String ID = "acme-user-storage"; public static final String ACCOUNT_SERVICE_URL_CONFIG_PROPERTY = "accountServiceUrl"; public static final String CONNECT_TIMEOUT_CONFIG_PROPERTY = "connectTimeout"; public static final String READ_TIMEOUT_CONFIG_PROPERTY = "readTimeout"; public static final String WRITE_TIMEOUT_CONFIG_PROPERTY = "writeTimeout"; private final KeycloakSession session; private final ComponentModel model; private final AcmeAccountClient accountClient; @Override public void close() { // NOOP } @Override public UserModel getUserById(RealmModel realm, String id) { if (StorageId.isLocalStorage(id)) { return null; } AcmeUser acmeUser = accountClient.getUserById(StorageId.externalId(id)); return wrap(realm, acmeUser); } @Override public UserModel getUserByUsername(RealmModel realm, String username) { AcmeUser acmeUser = accountClient.getUserByUsername(username); return wrap(realm, acmeUser); } @Override public UserModel getUserByEmail(RealmModel realm, String email) { AcmeUser acmeUser = accountClient.getUserByEmail(email); return wrap(realm, acmeUser); } private AcmeUserAdapter wrap(RealmModel realm, AcmeUser acmeUser) { if (acmeUser == null) { return null; } AcmeUserAdapter acmeUserAdapter = new AcmeUserAdapter(session, realm, new StorageId(model.getId(), acmeUser.getId()).toString(), acmeUser); acmeUserAdapter.setFederationLink(model.getId()); RoleModel defaultRoles = realm.getDefaultRole(); acmeUserAdapter.grantRole(defaultRoles); return acmeUserAdapter; } // @Override public boolean supportsCredentialType(String credentialType) { return PasswordCredentialModel.TYPE.equals(credentialType); } // @Override public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { return true; } // @Override public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { VerifyCredentialsOutput output = accountClient.verifyCredentials(StorageId.externalId(user.getId()), new VerifyCredentialsInput(credentialInput.getChallengeResponse())); return output != null && output.isValid(); } @Override public UserModel addUser(RealmModel realm, String username) { return null; } @Override public boolean removeUser(RealmModel realm, UserModel user) { return true; } /* UserCountMethodsProvider */ public int getUsersCount(RealmModel realm, Map params) { boolean includeServiceAccounts = Boolean.parseBoolean(params.get(UserModel.INCLUDE_SERVICE_ACCOUNT)); var search = params.get(UserModel.SEARCH); var options = EnumSet.noneOf(UserSearchOptions.class); options.add(UserSearchOptions.COUNT_ONLY); if (includeServiceAccounts) { options.add(UserSearchOptions.INCLUDE_SERVICE_ACCOUNTS); } var userSearchOutput = accountClient.searchForUsers(new UserSearchInput(search, null, null, options)); if (userSearchOutput == null) { return 0; } return userSearchOutput.getCount(); } /* UserQueryProvider */ @Override public Stream searchForUserStream(RealmModel realm, Map params, Integer firstResult, Integer maxResults) { var includeServiceAccounts = Boolean.parseBoolean(params.get(UserModel.INCLUDE_SERVICE_ACCOUNT)); var options = EnumSet.noneOf(UserSearchOptions.class); if (includeServiceAccounts) { options.add(UserSearchOptions.INCLUDE_SERVICE_ACCOUNTS); } var search = params.get(UserModel.SEARCH); var userSearchOutput = accountClient.searchForUsers(new UserSearchInput(search, firstResult, maxResults, options)); if (userSearchOutput == null || userSearchOutput.getUsers().isEmpty()) { return Stream.empty(); } return userSearchOutput.getUsers().stream() // .filter(acmeUser -> !acmeUser.getUsername().startsWith("service-account-") || includeServiceAccounts) // .map(acmeUser -> wrap(realm, acmeUser)); } @Override public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { return null; } @Override public Stream searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) { return null; } /* ImportSynchronization */ @Override public SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) { log.infof("Run sync"); return null; } @Override public SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model) { return null; } @AutoService(UserStorageProviderFactory.class) public static class Factory implements UserStorageProviderFactory { @Override public String getId() { return ID; } @Override public String getHelpText() { return "Acme User Storage fetches users from a remote user service"; } @Override public AcmeUserStorageProvider create(KeycloakSession session, ComponentModel model) { var accountServiceUrl = model.getConfig().getFirst(ACCOUNT_SERVICE_URL_CONFIG_PROPERTY); AccountClientOptions options = AccountClientOptions.builder() // .url(accountServiceUrl) // .connectTimeoutMillis(Integer.parseInt(model.getConfig().getFirst(CONNECT_TIMEOUT_CONFIG_PROPERTY))) // .readTimeoutMillis(Integer.parseInt(model.getConfig().getFirst(READ_TIMEOUT_CONFIG_PROPERTY))) // .writeTimeoutMillis(Integer.parseInt(model.getConfig().getFirst(WRITE_TIMEOUT_CONFIG_PROPERTY))) // .build(); var acmeAccountClient = new SimpleAcmeAccountClient(session, options); return new AcmeUserStorageProvider(session, model, acmeAccountClient); } @Override public List getConfigProperties() { return ProviderConfigurationBuilder.create() // .property() // .name(ACCOUNT_SERVICE_URL_CONFIG_PROPERTY) // .label("Account Service URL") // .helpText("Account Service URL") // .type(ProviderConfigProperty.STRING_TYPE) // .defaultValue("http://account-service:7070") // .add() // .property() // .name(CONNECT_TIMEOUT_CONFIG_PROPERTY) // .label("Connect Timeout (MS)") // .helpText("Connect Timeout (MS)") // .type(ProviderConfigProperty.STRING_TYPE) // .defaultValue("20000") // .add() // .property() // .name(READ_TIMEOUT_CONFIG_PROPERTY) // .label("Read Timeout (MS)") // .helpText("Read Timeout (MS)") // .type(ProviderConfigProperty.STRING_TYPE) // .defaultValue("20000") // .add() // .property() // .name(WRITE_TIMEOUT_CONFIG_PROPERTY) // .label("Write Timeout (MS)") // .helpText("Write Timeout (MS)") // .type(ProviderConfigProperty.STRING_TYPE) // .defaultValue("20000") // .add() // .build(); } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/AccountClientOptions.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import lombok.Builder; import lombok.Data; @Data @Builder public class AccountClientOptions { String url; int connectTimeoutMillis; int readTimeoutMillis; int writeTimeoutMillis; } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/AcmeAccountClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; public interface AcmeAccountClient { AcmeUser getUserByUsername(String username); AcmeUser getUserByEmail(String email); AcmeUser getUserById(String userId); VerifyCredentialsOutput verifyCredentials(String userId, VerifyCredentialsInput input); UserSearchOutput searchForUsers(UserSearchInput userSearchInput); } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/AcmeUser.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @NoArgsConstructor public class AcmeUser implements Cloneable { private String id; private String username; private String email; private boolean emailVerified; private String firstname; private String lastname; private boolean enabled; private Long created; private List roles; public AcmeUser(String id, String username, String email, boolean emailVerified, String firstname, String lastname, boolean enabled, List roles) { this.id = id; this.username = username; this.email = email; this.emailVerified = emailVerified; this.firstname = firstname; this.lastname = lastname; this.enabled = enabled; this.created = System.currentTimeMillis(); this.roles = roles; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/SimpleAcmeAccountClient.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import lombok.RequiredArgsConstructor; import lombok.extern.jbosslog.JBossLog; import org.apache.http.client.config.RequestConfig; import org.keycloak.http.simple.SimpleHttp; import org.keycloak.models.KeycloakSession; import java.io.IOException; import java.util.Map; @JBossLog @RequiredArgsConstructor public class SimpleAcmeAccountClient implements AcmeAccountClient { private final KeycloakSession session; private final AccountClientOptions options; @Override public AcmeUser getUserByUsername(String username) { var http = createHttpClient(session); var request = http.doPost(options.getUrl() + "/api/users/lookup/username"); request.json(Map.of("username", username)); try (var response = request.asResponse()) { AcmeUser user = response.asJson(AcmeUser.class); return user; } catch (Exception e) { log.warn("Failed to parse user response", e); return null; } } protected SimpleHttp createHttpClient(KeycloakSession session) { var http = SimpleHttp.create(session); var requestConfig = RequestConfig.custom() // .setConnectTimeout(options.getConnectTimeoutMillis()) // .setConnectionRequestTimeout(options.getReadTimeoutMillis()) // .setSocketTimeout(options.getWriteTimeoutMillis()) .build(); http.withRequestConfig(requestConfig); return http; } @Override public AcmeUser getUserByEmail(String email) { var http = createHttpClient(session); var request = http.doPost(options.getUrl() + "/api/users/lookup/email"); request.json(Map.of("email", email)); try (var response = request.asResponse()) { return response.asJson(AcmeUser.class); } catch (IOException e) { log.warn("Failed to parse user response", e); return null; } } @Override public AcmeUser getUserById(String userId) { var http = createHttpClient(session); var request = http.doGet(options.getUrl() + "/api/users/" + userId); try (var response = request.asResponse()) { return response.asJson(AcmeUser.class); } catch (IOException e) { log.warn("Failed to parse user response", e); return null; } } @Override public VerifyCredentialsOutput verifyCredentials(String userId, VerifyCredentialsInput input) { var http = createHttpClient(session); var request = http.doPost(options.getUrl() + "/api/users/" + userId + "/credentials/verify"); request.json(input); try (var response = request.asResponse()) { return response.asJson(VerifyCredentialsOutput.class); } catch (IOException e) { log.warn("Failed to parse user response", e); return null; } } @Override public UserSearchOutput searchForUsers(UserSearchInput userSearchInput) { var http = createHttpClient(session); var request = http.doPost(options.getUrl() + "/api/users/search"); request.json(userSearchInput); try (var response = request.asResponse()) { return response.asJson(UserSearchOutput.class); } catch (IOException e) { log.warn("Failed to parse user response", e); return null; } } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/UserSearchInput.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import lombok.Data; import java.util.EnumSet; @Data public class UserSearchInput { private final String search; private final Integer firstResult; private final Integer maxResults; private final EnumSet options; public UserSearchInput(String search, Integer firstResult, Integer maxResults, EnumSet options) { this.search = search; this.firstResult = firstResult; this.maxResults = maxResults; this.options = options; } public enum UserSearchOptions { COUNT_ONLY,INCLUDE_SERVICE_ACCOUNTS; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/UserSearchOutput.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import java.util.List; public class UserSearchOutput { List users; int count; public List getUsers() { return users; } public int getCount() { return count; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/VerifyCredentialsInput.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class VerifyCredentialsInput { private String password; public VerifyCredentialsInput(String password) { this.password = password; } } ================================================ FILE: keycloak/extensions/src/main/java/com/github/thomasdarimont/keycloak/custom/userstorage/remote/accountclient/VerifyCredentialsOutput.java ================================================ package com.github.thomasdarimont.keycloak.custom.userstorage.remote.accountclient; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class VerifyCredentialsOutput { private boolean valid; public VerifyCredentialsOutput(boolean valid) { this.valid = valid; } } ================================================ FILE: keycloak/extensions/src/main/resources/META-INF/keycloak-scripts.json ================================================ { "authenticators": [ { "name": "Acme JavaScript Authenticator", "fileName": "my-script-authenticator.js", "description": "My Authenticator from a JS file" } ], "mappers": [ { "name": "Acme JavaScript Mapper", "fileName": "my-script-mapper.js", "description": "My Mapper from a JS file" } ] } ================================================ FILE: keycloak/extensions/src/main/resources/ignore_default-persistence.xml ================================================ org.keycloak.models.jpa.entities.ClientEntity org.keycloak.models.jpa.entities.ClientAttributeEntity org.keycloak.models.jpa.entities.CredentialEntity org.keycloak.models.jpa.entities.RealmEntity org.keycloak.models.jpa.entities.RealmAttributeEntity org.keycloak.models.jpa.entities.RequiredCredentialEntity org.keycloak.models.jpa.entities.ComponentConfigEntity org.keycloak.models.jpa.entities.ComponentEntity org.keycloak.models.jpa.entities.UserFederationProviderEntity org.keycloak.models.jpa.entities.UserFederationMapperEntity org.keycloak.models.jpa.entities.RoleEntity org.keycloak.models.jpa.entities.RoleAttributeEntity org.keycloak.models.jpa.entities.FederatedIdentityEntity org.keycloak.models.jpa.entities.MigrationModelEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.RealmLocalizationTextsEntity org.keycloak.models.jpa.entities.UserRequiredActionEntity org.keycloak.models.jpa.entities.UserAttributeEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.IdentityProviderEntity org.keycloak.models.jpa.entities.IdentityProviderMapperEntity org.keycloak.models.jpa.entities.ProtocolMapperEntity org.keycloak.models.jpa.entities.UserConsentEntity org.keycloak.models.jpa.entities.UserConsentClientScopeEntity org.keycloak.models.jpa.entities.AuthenticationFlowEntity org.keycloak.models.jpa.entities.AuthenticationExecutionEntity org.keycloak.models.jpa.entities.AuthenticatorConfigEntity org.keycloak.models.jpa.entities.RequiredActionProviderEntity org.keycloak.models.jpa.session.PersistentUserSessionEntity org.keycloak.models.jpa.session.PersistentClientSessionEntity org.keycloak.models.jpa.entities.RevokedTokenEntity org.keycloak.models.jpa.entities.GroupEntity org.keycloak.models.jpa.entities.GroupAttributeEntity org.keycloak.models.jpa.entities.GroupRoleMappingEntity org.keycloak.models.jpa.entities.UserGroupMembershipEntity org.keycloak.models.jpa.entities.ClientScopeEntity org.keycloak.models.jpa.entities.ClientScopeAttributeEntity org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity org.keycloak.models.jpa.entities.DefaultClientScopeRealmMappingEntity org.keycloak.models.jpa.entities.ClientInitialAccessEntity org.keycloak.events.jpa.EventEntity org.keycloak.events.jpa.AdminEventEntity org.keycloak.authorization.jpa.entities.ResourceServerEntity org.keycloak.authorization.jpa.entities.ResourceEntity org.keycloak.authorization.jpa.entities.ScopeEntity org.keycloak.authorization.jpa.entities.PolicyEntity org.keycloak.authorization.jpa.entities.PermissionTicketEntity org.keycloak.authorization.jpa.entities.ResourceAttributeEntity org.keycloak.storage.jpa.entity.BrokerLinkEntity org.keycloak.storage.jpa.entity.FederatedUser org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity org.keycloak.storage.jpa.entity.FederatedUserConsentEntity org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity org.keycloak.models.jpa.entities.OrganizationEntity org.keycloak.models.jpa.entities.OrganizationDomainEntity true ================================================ FILE: keycloak/extensions/src/main/resources/my-script-authenticator.js ================================================ AuthenticationFlowError = Java.type("org.keycloak.authentication.AuthenticationFlowError"); function authenticate(context) { LOG.info(script.name + " --> trace auth for: " + user.username); /* if ( user.username === "tester" && user.getAttribute("someAttribute") && user.getAttribute("someAttribute").contains("someValue")) { context.failure(AuthenticationFlowError.INVALID_USER); return; } */ LOG.info(script.name + " --> trace auth for: " + user.username); // LOG.info(script.name + " --> parameter: " + context.httpRequest.decodedFormParameters.getFirst("username")); context.success(); } ================================================ FILE: keycloak/extensions/src/main/resources/my-script-mapper.js ================================================ ================================================ FILE: keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/BoostrapTest.java ================================================ package com.github.thomasdarimont.keycloak.custom; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class BoostrapTest { @Test public void shouldRunAsUnitTest() { Assertions.assertTrue(true); } } ================================================ FILE: keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/KeycloakEnvironment.java ================================================ package com.github.thomasdarimont.keycloak.custom; import dasniko.testcontainers.keycloak.KeycloakContainer; import lombok.extern.slf4j.Slf4j; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.token.TokenService; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.output.Slf4jLogConsumer; @Slf4j public class KeycloakEnvironment { public KeycloakContainer keycloak; public GenericContainer keycloakConfigCli; private String authServerUrl = "http://localhost:8080/auth"; private String adminUsername = "admin"; private String adminPassword = "admin"; private Mode mode = Mode.TESTCONTAINERS; private boolean runConfigCli = true; public KeycloakEnvironment local() { KeycloakEnvironment keycloakEnvironment = new KeycloakEnvironment(); keycloakEnvironment.setMode(Mode.LOCAL); keycloakEnvironment.setRunConfigCli(false); return keycloakEnvironment; } public KeycloakEnvironment custom(String authServerUrl, String adminUsername, String adminPassword) { KeycloakEnvironment keycloakEnvironment = new KeycloakEnvironment(); keycloakEnvironment.setAuthServerUrl(authServerUrl); keycloakEnvironment.setAdminUsername(adminUsername); keycloakEnvironment.setAdminPassword(adminPassword); keycloakEnvironment.setMode(Mode.CUSTOM); keycloakEnvironment.setRunConfigCli(false); return keycloakEnvironment; } public void start() { switch (mode) { case LOCAL: case CUSTOM: { keycloak = new KeycloakTestSupport.CustomKeycloak(authServerUrl, adminUsername, adminPassword); return; } case TESTCONTAINERS: default: break; } keycloak = KeycloakTestSupport.createKeycloakContainer(); keycloak.withReuse(true); log.info("Starting Keycloak Container"); keycloak.start(); log.info("Keycloak Container started."); keycloak.followOutput(new Slf4jLogConsumer(log)); if (runConfigCli) { keycloakConfigCli = KeycloakTestSupport.createKeycloakConfigCliContainer(keycloak); keycloakConfigCli.start(); keycloakConfigCli.followOutput(new Slf4jLogConsumer(log)); } } public void stop() { if (keycloak != null) { keycloak.stop(); } if (keycloakConfigCli != null) { keycloakConfigCli.stop(); } } public KeycloakContainer getKeycloak() { return keycloak; } public GenericContainer getKeycloakConfigCli() { return keycloakConfigCli; } public Keycloak getAdminClient() { return keycloak.getKeycloakAdminClient(); } public TokenService getTokenService() { return getClientProxy(TokenService.class); } public T getClientProxy(Class iface) { return iface.cast(KeycloakTestSupport.getResteasyWebTarget(keycloak).proxy(iface)); } public String getAuthServerUrl() { return authServerUrl; } public KeycloakEnvironment setAuthServerUrl(String authServerUrl) { this.authServerUrl = authServerUrl; return this; } public String getAdminUsername() { return adminUsername; } public KeycloakEnvironment setAdminUsername(String adminUsername) { this.adminUsername = adminUsername; return this; } public String getAdminPassword() { return adminPassword; } public KeycloakEnvironment setAdminPassword(String adminPassword) { this.adminPassword = adminPassword; return this; } public Mode getMode() { return mode; } public void setMode(Mode mode) { this.mode = mode; } public boolean isRunConfigCli() { return runConfigCli; } public void setRunConfigCli(boolean runConfigCli) { this.runConfigCli = runConfigCli; } enum Mode { CUSTOM, LOCAL, TESTCONTAINERS } } ================================================ FILE: keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/KeycloakIntegrationTest.java ================================================ package com.github.thomasdarimont.keycloak.custom; import com.github.thomasdarimont.keycloak.custom.KeycloakTestSupport.UserRef; import com.github.thomasdarimont.keycloak.custom.oidc.ageinfo.AgeInfoMapper; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Form; import jakarta.ws.rs.core.MediaType; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.keycloak.TokenVerifier; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.token.TokenService; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.testcontainers.containers.output.ToStringConsumer; import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @Slf4j public class KeycloakIntegrationTest { public static final String TEST_REALM = "acme-internal"; public static final String TEST_CLIENT = "test-client"; public static final String TEST_USER_PASSWORD = "test"; public static final KeycloakEnvironment KEYCLOAK_ENVIRONMENT = new KeycloakEnvironment(); @BeforeAll public static void beforeAll() { KEYCLOAK_ENVIRONMENT.start(); } @AfterAll public static void afterAll() { KEYCLOAK_ENVIRONMENT.stop(); } @Test public void ageInfoMapperShouldAddAgeClassClaim() throws Exception { Keycloak adminClient = KEYCLOAK_ENVIRONMENT.getAdminClient(); RealmResource acmeRealm = adminClient.realm(TEST_REALM); UserRef user22Years = KeycloakTestSupport.createOrUpdateTestUser(acmeRealm, "test-user-age22", TEST_USER_PASSWORD, user -> { user.setFirstName("Firstname"); user.setLastName("Lastname"); user.setAttributes(Map.of("birthdate", List.of(LocalDate.now().minusYears(22).toString()))); }); TokenService tokenService = KEYCLOAK_ENVIRONMENT.getTokenService(); AccessTokenResponse accessTokenResponse = tokenService.grantToken(TEST_REALM, new Form() .param("grant_type", "password") .param("username", user22Years.getUsername()) .param("password", TEST_USER_PASSWORD) .param("client_id", TEST_CLIENT) .param("scope", "openid acme.profile acme.ageinfo") .asMap()); // System.out.println("Token: " + accessTokenResponse.getToken()); // parse the received id-token TokenVerifier verifier = TokenVerifier.create(accessTokenResponse.getIdToken(), IDToken.class); verifier.parse(); // check for the custom claim IDToken accessToken = verifier.getToken(); String ageInfoClaim = (String) accessToken.getOtherClaims().get(AgeInfoMapper.AGE_CLASS_CLAIM); assertThat(ageInfoClaim).isNotNull(); assertThat(ageInfoClaim).isEqualTo("over21"); } @Test public void auditListenerShouldPrintLogMessage() throws Exception { Assumptions.assumeTrue(KEYCLOAK_ENVIRONMENT.getMode() == KeycloakEnvironment.Mode.TESTCONTAINERS); ToStringConsumer consumer = new ToStringConsumer(); KEYCLOAK_ENVIRONMENT.getKeycloak().followOutput(consumer); TokenService tokenService = KEYCLOAK_ENVIRONMENT.getTokenService(); // trigger user login via ROPC tokenService.grantToken(TEST_REALM, new Form() .param("grant_type", "password") .param("username", "tester") .param("password", TEST_USER_PASSWORD) .param("client_id", TEST_CLIENT) .param("scope", "openid acme.profile acme.ageinfo") .asMap()); // Allow the container log to flush TimeUnit.MILLISECONDS.sleep(750); assertThat(consumer.toUtf8String()).contains("audit userEvent"); } @Test public void pingResourceShouldBeAccessibleForUser() { TokenService tokenService = KEYCLOAK_ENVIRONMENT.getTokenService(); AccessTokenResponse accessTokenResponse = tokenService.grantToken(TEST_REALM, new Form() .param("grant_type", "password") .param("username", "tester") .param("password", TEST_USER_PASSWORD) .param("client_id", TEST_CLIENT) .param("scope", "openid") .asMap()); String accessToken = accessTokenResponse.getToken(); System.out.println("Token: " + accessToken); CustomResources customResources = KEYCLOAK_ENVIRONMENT.getClientProxy(CustomResources.class); Map response = customResources.ping(TEST_REALM, "Bearer " + accessToken); System.out.println(response); assertThat(response).isNotNull(); assertThat(response.get("user")).isEqualTo("tester"); } interface CustomResources { @GET @Consumes(MediaType.APPLICATION_JSON) @jakarta.ws.rs.Path("/realms/{realm}/custom-resources/ping") Map ping(@PathParam("realm") String realm, @HeaderParam("Authorization") String token); } } ================================================ FILE: keycloak/extensions/src/test/java/com/github/thomasdarimont/keycloak/custom/KeycloakTestSupport.java ================================================ package com.github.thomasdarimont.keycloak.custom; import dasniko.testcontainers.keycloak.KeycloakContainer; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.SelinuxContext; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.ImageFromDockerfile; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriBuilder; import java.io.File; import java.nio.file.Paths; import java.util.List; import java.util.function.Consumer; @Slf4j public class KeycloakTestSupport { public static final String MASTER_REALM = "master"; public static final String ADMIN_CLI = "admin-cli"; public static final String CONTEXT_PATH = "/auth"; public static KeycloakContainer createKeycloakContainer() { return createKeycloakContainer(null); } public static KeycloakContainer createKeycloakContainer(String realmImportFileName) { return createKeycloakContainer("quay.io/keycloak/keycloak:26.5.7", realmImportFileName); } public static KeycloakContainer createKeycloakContainer(String imageName, String realmImportFileName) { KeycloakContainer keycloakContainer; if (imageName != null) { keycloakContainer = new KeycloakContainer(imageName); keycloakContainer.addEnv("KC_FEATURES", "preview"); } else { // building custom Keycloak docker image with additional libraries String customDockerFileName = "../docker/src/main/docker/keycloakx/Dockerfile.ci.plain"; ImageFromDockerfile imageFromDockerfile = new ImageFromDockerfile(); imageFromDockerfile.withDockerfile(Paths.get(customDockerFileName)); keycloakContainer = new KeycloakContainer(); keycloakContainer.setImage(imageFromDockerfile); keycloakContainer.withContextPath(CONTEXT_PATH); } keycloakContainer.withProviderLibsFrom(List.of(new File("target/extensions-jar-with-dependencies.jar"))); return keycloakContainer.withProviderClassesFrom("target/classes"); } public static ResteasyWebTarget getResteasyWebTarget(KeycloakContainer keycloak) { Client client = ResteasyClientBuilder.newBuilder().build(); return (ResteasyWebTarget) client.target(UriBuilder.fromPath(keycloak.getAuthServerUrl())); } public static UserRef createOrUpdateTestUser(RealmResource realm, String username, String password, Consumer adjuster) { List existingUsers = realm.users().search(username, true); String userId; UserRepresentation userRep; if (existingUsers.isEmpty()) { userRep = new UserRepresentation(); userRep.setUsername(username); userRep.setEnabled(true); adjuster.accept(userRep); try (Response response = realm.users().create(userRep)) { userId = CreatedResponseUtil.getCreatedId(response); } catch (Exception ex) { throw new RuntimeException(ex); } } else { userRep = existingUsers.get(0); adjuster.accept(userRep); userId = userRep.getId(); } CredentialRepresentation passwordRep = new CredentialRepresentation(); passwordRep.setType(CredentialRepresentation.PASSWORD); passwordRep.setValue(password); realm.users().get(userId).resetPassword(passwordRep); return new UserRef(userId, username); } public static GenericContainer createKeycloakConfigCliContainer(KeycloakContainer keycloakContainer) { var keycloakConfigCli = new GenericContainer<>( "quay.io/adorsys/keycloak-config-cli:6.5.0-26.5.4" ); keycloakConfigCli.addEnv("KEYCLOAK_AVAILABILITYCHECK_ENABLED", "true"); keycloakConfigCli.addEnv("KEYCLOAK_AVAILABILITYCHECK_TIMEOUT", "90s"); keycloakConfigCli.addEnv("IMPORT_FILES_LOCATION", "/config/acme-internal.yaml"); keycloakConfigCli.addEnv("IMPORT_CACHE_ENABLED", "true"); keycloakConfigCli.addEnv("IMPORT_VAR_SUBSTITUTION_ENABLED", "true"); keycloakConfigCli.addEnv("KEYCLOAK_USER", keycloakContainer.getAdminUsername()); keycloakConfigCli.addEnv("KEYCLOAK_PASSWORD", keycloakContainer.getAdminPassword()); keycloakConfigCli.addEnv("KEYCLOAK_URL", keycloakContainer.getAuthServerUrl()); keycloakConfigCli.addEnv("KEYCLOAK_FRONTEND_URL", keycloakContainer.getAuthServerUrl()); keycloakConfigCli.addEnv("APPS_FRONTEND_URL_MINISPA", "http://localhost:4000"); keycloakConfigCli.addEnv("APPS_FRONTEND_URL_GREETME", "http://localhost:4000"); keycloakConfigCli.addEnv("ACME_AZURE_AAD_TENANT_URL", "https://login.microsoftonline.com/dummy-azuread-tenant-id"); keycloakConfigCli.addEnv("LOGGING_LEVEL_ROOT", "INFO"); // TODO make the realm config folder parameterizable keycloakConfigCli.addFileSystemBind("../../config/stage/dev/realms", "/config", BindMode.READ_ONLY, SelinuxContext.SHARED); keycloakConfigCli.setWaitStrategy(Wait.forLogMessage(".*keycloak-config-cli ran in.*", 1)); keycloakConfigCli.setNetworkMode("host"); return keycloakConfigCli; } @Data @AllArgsConstructor public static class UserRef { String userId; String username; } @Getter @Setter @AllArgsConstructor public static class CustomKeycloak extends KeycloakContainer { String authServerUrl; String adminUsername; String adminPassword; public void start() { // NOOP } @Override public void followOutput(Consumer consumer) { // NOOP } } } ================================================ FILE: keycloak/extensions/src/test/resources/log4j.properties ================================================ log4j.rootLogger=INFO,stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t[%t]\t%m%n ================================================ FILE: keycloak/extensions/src/test/resources/testcontainers.properties ================================================ testcontainers.reuse.enable=true ================================================ FILE: keycloak/http-tests/advanced_oauth_par.http ================================================ ### Auth Code Flow Pushed Authorization Request (PAR) POST {{ISSUER}}/protocol/openid-connect/ext/par/request Content-Type: application/x-www-form-urlencoded response_type=code&client_id={{TEST_CLIENT4_ID}}&client_secret={{TEST_CLIENT_SECRET}}&nonce=abc123456&redirect_uri=https%3A%2F%2Fapps.acme.test%3A4633%2Fwebapp%2Flogin%2Foauth2%2Fcode%2Fkeycloak&scope=openid%20profile > {% client.global.set("KC_REQUEST_URI", response.body.request_uri); %} ### Exchange PAR Request URI GET {{ISSUER}}/protocol/openid-connect/auth?client_id={{TEST_CLIENT4_ID}}&nonce=abc123456&request_uri={{KC_REQUEST_URI}} ================================================ FILE: keycloak/http-tests/advanced_oauth_resources.http ================================================ ### Auth Code Flow PAR Request GET {{ISSUER}}/protocol/openid-connect/auth?response_type=code&client_id=acme-client-spa-app&redirect_uri=https%3A%2F%2Fflowsimulator.pragmaticwebsecurity.com&state=j5aSlzUCiH7kvX37MU9Q&scope=openid%20email&code_challenge=M9eXaGhPVUSVDIig4aUW25qYlpVrM4WvzjH_1x00Ngg&code_challenge_method=S256&prompt=login&resource=acme-client-api-resource-server&resource=acme-client-api-resource-server2 ### Exchange PAR Request URI GET {{ISSUER}}/protocol/openid-connect/auth?client_id={{TEST_CLIENT4_ID}}&nonce=abc123456&request_uri={{KC_REQUEST_URI}} ================================================ FILE: keycloak/http-tests/custom-token-migration.http ================================================ ### Resource Owner Password Credentials Grant Flow with Public Client POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CLIENT_ID_1}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile offline_access > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} #### ### Call custom token migration endpoint POST {{ISSUER}}/custom-resources/migration/token Content-Type: application/json Authorization: Bearer {{KC_ACCESS_TOKEN}} { "target_client_id": "client-2" } > {% client.global.set("KC_ACCESS_TOKEN_NEW", response.body.access_token); client.global.set("KC_REFRESH_TOKEN_NEW", response.body.refresh_token); %} ### Obtain new Tokens via RefreshToken POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CLIENT_ID_2}}&grant_type=refresh_token&refresh_token={{KC_REFRESH_TOKEN_NEW}} ================================================ FILE: keycloak/http-tests/custom_token_exchange.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{PUBLIC_CLIENT_CLI_APP}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform custom token exchange POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{PUBLIC_CLIENT_CLI_APP}}&subject_token={{KC_ACCESS_TOKEN}}&requested_issuer=https://id.acme.test/offline > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform custom token exchange with API Key: Translate an API key with into an access-token with an API-Gateway POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{API_GATEWAY_CLIENT}}&client_secret={{API_GATEWAY_CLIENT_SECRET}}&api_key={{APIKEY}}&requested_token_type=access_token > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ================================================ FILE: keycloak/http-tests/dynamic-client-registration.http ================================================ ### Obtain App Token POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = client_credentials & client_id = {{ACME_API_MGMT_CLIENT_ID}} & client_secret = {{ACME_API_MGMT_CLIENT_SECRET}} & scope = openid+profile > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); %} ### Register Client POST {{ISSUER}}/clients-registrations/openid-connect Authorization: Bearer {{KC_ACCESS_TOKEN}} Content-Type: application/json { "client_name": "acme-subs-sub1", "client_secret": "secret", "scope": "acme domain1" } ================================================ FILE: keycloak/http-tests/example-requests.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{TEST_CLIENT_ID}}&client_secret={{TEST_CLIENT_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Obtain User info from User-Info Endpoint GET {{ISSUER}}/protocol/openid-connect/userinfo Authorization: Bearer {{KC_ACCESS_TOKEN}} ### Obtain Token info from Token Introspection Endpoint POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id={{TEST_CLIENT_ID}}&client_secret={{TEST_CLIENT_SECRET}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token ### Refresh Tokens POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{TEST_CLIENT_ID}}&client_secret={{TEST_CLIENT_SECRET}}&grant_type=refresh_token&refresh_token={{KC_REFRESH_TOKEN}} > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Client Credentials Grant POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{TEST_CLIENT3_ID}}&client_secret={{TEST_CLIENT_SECRET}}&grant_type=client_credentials > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Obtain tokens via ROPC Grant Flow for Public CLient POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{TEST_CLIENT2_ID}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ================================================ FILE: keycloak/http-tests/grant_type_client_credentials-requests.http ================================================ ### Client Credentials Grant POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret={{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}&grant_type=client_credentials > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); %} ### Client Credentials Grant with Invalid Secret POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret=INVALID&grant_type=client_credentials > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); %} ### Obtain Token info from Token Introspection Endpoint POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret={{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token ### Revoke Token POST {{ISSUER}}/protocol/openid-connect/revoke Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_SERVICE}}&client_secret={{CONFIDENTIAL_CLIENT_SERVICE_SECRET}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token ### Client Credentials Grant with Basic Auth POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded Authorization: Basic {{CONFIDENTIAL_CLIENT_SERVICE}} {{CONFIDENTIAL_CLIENT_SERVICE_SECRET}} grant_type=client_credentials > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); %} ================================================ FILE: keycloak/http-tests/grant_type_password-requests.http ================================================ ### Resource Owner Password Credentials Grant Flow with Public Client POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{PUBLIC_CLIENT_CLI_APP}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile ### Resource Owner Password Credentials Grant Flow with Confidential Client POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_LEGACY_APP}}&client_secret={{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ================================================ FILE: keycloak/http-tests/grant_type_refreshtoken-requests.http ================================================ ### Resource Owner Password Credentials Grant Flow with Confidential Client POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_LEGACY_APP}}&client_secret={{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Obtain new Tokens via RefreshToken POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_LEGACY_APP}}&client_secret={{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}}&grant_type=refresh_token&refresh_token={{KC_REFRESH_TOKEN}} > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ================================================ FILE: keycloak/http-tests/http-client.env.json ================================================ { "acme-internal": { "ISSUER": "https://id.acme.test:8443/auth/realms/acme-internal", "ADMIN_USERNAME": "admin", "ADMIN_PASSWORD": "admin", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "TEST_CLIENT_ID": "test-client-ropc", "TEST_CLIENT_SECRET": "secret", "TEST_CLIENT2_ID": "test-client", "TEST_CLIENT2_SECRET": "", "TEST_CLIENT2_CALLBACK_URI": "https://apps.acme.test:4443/acme-account/", "TEST_CLIENT3_ID": "app-demo-service", "TEST_CLIENT4_ID": "frontend-webapp-springboot" }, "acme-client-examples": { "ISSUER": "https://id.acme.test:8443/auth/realms/acme-client-examples", "ADMIN_USERNAME": "admin", "ADMIN_PASSWORD": "admin", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "PUBLIC_CLIENT_CLI_APP": "acme-client-cli-app", "CONFIDENTIAL_CLIENT_LEGACY_APP": "acme-client-legacy-app", "CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET": "secret", "CONFIDENTIAL_CLIENT_SERVICE": "acme-client-service-app", "CONFIDENTIAL_CLIENT_SERVICE_SECRET": "secret", "CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP": "acme-client-classic-web-app", "CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET": "secret", "TEST_CLIENT2_CALLBACK_URI": "https://apps.acme.test:4443/acme-account/", "APIKEY": "api-user-42:7FTt1Q0PG5yv3YqaZGhEB19KIollpNFurA", "API_GATEWAY_CLIENT": "acme-api-gateway", "API_GATEWAY_CLIENT_SECRET": "secret" }, "custom-token-migration": { "ISSUER": "https://id.acme.test:8443/auth/realms/acme-token-migration", "ADMIN_USERNAME": "admin", "ADMIN_PASSWORD": "admin", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "CLIENT_SECRET": "secret", "CLIENT_ID_1": "client-1", "CLIENT_ID_2": "client-2", "CLIENT_ID_3": "client-3" }, "acme-workshop": { "ISSUER": "https://id.acme.test:8443/auth/realms/acme-workshop", "ADMIN_ENDPOINT_BASE": "https://id.acme.test:8443/auth/admin/realms/acme-workshop", "ADMIN_USERNAME": "admin", "ADMIN_PASSWORD": "admin", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "ADMIN_SERVICE_CLIENT_ID": "keycloak-inspector", "ADMIN_SERVICE_CLIENT_SECRET": "Ft5bvSf4d3nJqD9MF491npDjnmUd1LCf", "CLIENT_SECRET": "secret", "CLIENT_ID_1": "client-1", "CLIENT_ID_2": "client-2", "CLIENT_ID_3": "client-3" }, "acme-apps": { "ISSUER": "https://id.acme.test:8443/auth/realms/acme-apps", "TXCHG_CLIENT_ID": "token-exchanger", "TXCHG_CLIENT_SECRET": "M8u5OJuCQ6VASdgOc8MhxVXYikWFLVL0", "CLIENT_ID_1": "client-1", "CLIENT_ID_2": "client-2", "CLIENT_ID_3": "client-3" }, "acme-token-exchange-v2": { "ISSUER": "https://id.acme.test:8443/auth/realms/acme-token-exchange", "REQUESTER_CLIENT_ID": "acme-requester-client", "REQUESTER_CLIENT_SECRET": "kW9i41aTsCwgPejIWSsKvKn2KiPrkwvQ", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "INITIAL_CLIENT_ID": "acme-initial-client", "INITIAL_CLIENT_SECRET": "LiIu5NZIbosLyzwbrafNQTIydmexp9eS", "TARGET_CLIENT_ID": "acme-target-client" }, "acme-token-exchange-v3": { "ISSUER_A": "https://id.acme.test:8443/auth/realms/fed-token-xchg-domain-a", "ISSUER_B": "https://id.acme.test:8443/auth/realms/fed-token-xchg-domain-b", "INITIAL_CLIENT_ID": "initial-client", "INITIAL_CLIENT_SECRET": "N4uk5szELVhMnhCu7CmNRRtYZWcPyqbh", "REQUESTER_CLIENT_ID": "initial-client", "REQUESTER_CLIENT_SECRET": "N4uk5szELVhMnhCu7CmNRRtYZWcPyqbh", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "CLIENT_ID_B": "clientb", "CLIENT_SECRET_B": "9k0N6pv5aJuDnFZarl5ddTxDOBZtv48Z" }, "acme-token-exchange": { "ISSUER": "http://localhost:8080/auth/realms/acme-token-exchange", "ACME_APP_CLIENT_ID": "acme-app", "ACME_APP_CLIENT_SECRET": "e8kjabhEF5BVInJqaYfpRMbx2lXC6YBF", "ACME_API_MGMT_CLIENT_ID": "acme-api-mgmt", "ACME_API_MGMT_CLIENT_SECRET": "3uJg7T6aeAut3toCdK5OXRfx2K8Uja58", "ACME_API_GATEWAY_CLIENT_ID": "acme-api-gateway", "ACME_API_GATEWAY_CLIENT_SECRET": "xuJC6ucgWfqTTBo3v78hv1RTHQqwcksO", "ACME_API_1_CLIENT_ID": "acme-api-1", "ACME_API_2_CLIENT_ID": "acme-api-2", "USER_USERNAME": "tester", "USER_PASSWORD": "test", "Security": { "Auth": { "default3": { "Type": "OAuth2", "Grant Type": "Authorization Code", "Client ID": "acme-app", "Client Secret": "e8kjabhEF5BVInJqaYfpRMbx2lXC6YBF", "Scope": "openid profile", "PKCE": true, "Auth URL": "https://id.acme.test:8443/auth/realms/acme-token-exchange/protocol/openid-connect/auth", "Token URL": "https://id.acme.test:8443/auth/realms/acme-token-exchange/protocol/openid-connect/token", "Redirect URL": "http://127.0.0.1:12345/idea/httpclient" } } } } } ================================================ FILE: keycloak/http-tests/implicit-flow-request.http ================================================ ### Implicit flow request GET {{ISSUER}}/protocol/openid-connect/auth?client_id={{TEST_CLIENT_IMPLICIT}}&redirect_uri={{TEST_CLIENT_IMPLICIT_REDIRECT}}&state=12345678&response_type=token&scope=openid profile email ================================================ FILE: keycloak/http-tests/keycloak-lightweight-token-requests.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow @client_id=app-lightweight-token-demo @client_secret=GetdqvQnNSLVRNU8QojCmBNfKIPqkfJt POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{client_id}}&client_secret={{client_secret}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Obtain User info from User-Info Endpoint GET {{ISSUER}}/protocol/openid-connect/userinfo Authorization: Bearer {{KC_ACCESS_TOKEN}} ### Obtain Token info from Token Introspection Endpoint POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id={{client_id}}&client_secret={{client_secret}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token ### Obtain Token info from Token Introspection Endpoint as JWT # Needs "Always use lightweight access token: on" in Advanced Client Settings # Needs "Support JWT claim in Introspection Response : on" in Advanced Client Settings POST {{ISSUER}}/protocol/openid-connect/token/introspect Accept: application/jwt Content-Type: application/x-www-form-urlencoded client_id={{client_id}}&client_secret={{client_secret}}&token={{KC_ACCESS_TOKEN}}&token_type_hint=access_token ================================================ FILE: keycloak/http-tests/oidc-endpoint-requests.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id = {{CONFIDENTIAL_CLIENT_LEGACY_APP}} & client_secret = {{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}} & username = {{USER_USERNAME}} & password = {{USER_PASSWORD}} & grant_type = password & scope = profile+openid > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Obtain User info from User-Info Endpoint GET {{ISSUER}}/protocol/openid-connect/userinfo Authorization: Bearer {{KC_ACCESS_TOKEN}} ### Obtain Token info from Token Introspection Endpoint POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id = {{CONFIDENTIAL_CLIENT_LEGACY_APP}} & client_secret = {{CONFIDENTIAL_CLIENT_LEGACY_APP_SECRET}} & token = {{KC_ACCESS_TOKEN}} & token_type_hint = access_token ================================================ FILE: keycloak/http-tests/token_exchange.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid > {% client.global.set("KC_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform (internal-to-internal) token exchange with audience extension # ensure token-exchange permission is configured for target client (acme-client-service-app) -> we must explicitly allow the source-client to use token-exchange # currently Keycloak generates an access token AND refresh token by default. To only request an access token use requested_token_type=urn:ietf:params:oauth:token-type:access_token # An ID token also generated by default (since the openid scope is included explicitly) POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&audience={{CONFIDENTIAL_CLIENT_SERVICE}}&requested_token_type=urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform (internal-to-internal) token exchange to SAML assertion POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&audience=acme-client-saml-webapp&requested_token_type=urn:ietf:params:oauth:token-type:saml2 > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform (internal-to-internal) token exchange with scope extension POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&audience={{CONFIDENTIAL_CLIENT_SERVICE}}&requested_token_type=urn:ietf:params:oauth:token-type:access_token&scope=openid+profile+email+phone > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform (impersonation) token exchange with user switch # ensure token-exchange permission is configured for target client (acme-client-service-app) -> we must explicitly allow the source-client to use token-exchange # currently Keycloak generates an access token AND refresh token by default. To only request an access token use requested_token_type=urn:ietf:params:oauth:token-type:access_token # An ID token also generated by default (since the openid scope is included explicitly) POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP}}&client_secret={{CONFIDENTIAL_CLIENT_CLASSIC_WEB_APP_SECRET}}&subject_token={{KC_ACCESS_TOKEN}}&requested_subject=a27f947d-2be4-4532-bd5b-af574f2f6449&requested_token_type=urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform custom token exchange with API Key: Translate an API key with into an access-token with an API-Gateway POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{API_GATEWAY_CLIENT}}&client_secret={{API_GATEWAY_CLIENT_SECRET}}&api_key={{APIKEY}}&requested_token_type=access_token > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ### External token to internal token exchange: EntraID to Keycloak @EXTERNAL_ACCESS_TOKEN=xxx POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange &client_id= {{TXCHG_CLIENT_ID}} &client_secret= {{TXCHG_CLIENT_SECRET}} &subject_issuer=idp-acme-azuread &subject_token={{EXTERNAL_ACCESS_TOKEN}} &requested_token_type=urn:ietf:params:oauth:token-type:access_token #&subject_token_type=urn:ietf:params:oauth:token-type:jwt ### Obtain Stored Token from IdP # see https://wjw465150.gitbooks.io/keycloak-documentation/content/server_admin/topics/identity-broker/tokens.html GET {{ISSUER}}/broker/idp-acme-azuread/token Authorization: Bearer xxx ================================================ FILE: keycloak/http-tests/token_exchange_api_gateway.http ================================================ ### Obtain App Token # via Resource Owner Password Credentials Grant Flow for the sake of example POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id = {{ACME_APP_CLIENT_ID}} & client_secret = {{ACME_APP_CLIENT_SECRET}} & username = {{USER_USERNAME}} & password = {{USER_PASSWORD}} & grant_type = password & scope = openid+profile > {% client.global.set("INITIAL_ACCESS_TOKEN", response.body.access_token); client.global.set("INITIAL_REFRESH_TOKEN", response.body.refresh_token); %} #### Access User Info GET {{ISSUER}}/protocol/openid-connect/userinfo Authorization: Bearer {{INITIAL_ACCESS_TOKEN}} #### Call Token Introspection with Initial App Access Token POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id = {{ACME_APP_CLIENT_ID}} & client_secret = {{ACME_APP_CLIENT_SECRET}} & token = {{INITIAL_ACCESS_TOKEN}} & token_type_hint = access_token #### Call Token Introspection with Initial App Access Token with JWT generation POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded Accept: application/jwt client_id = {{ACME_APP_CLIENT_ID}} & client_secret = {{ACME_APP_CLIENT_SECRET}} & token = {{INITIAL_ACCESS_TOKEN}} & token_type_hint = access_token ### Perform Token-exchange for acme-api-1 # INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2 # Perform (internal-to-internal) token exchange with audience extension POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = urn:ietf:params:oauth:grant-type:token-exchange & client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & subject_token = {{INITIAL_ACCESS_TOKEN}} & audience = {{ACME_API_1_CLIENT_ID}} & requested_token_type = urn:ietf:params:oauth:token-type:access_token & subject_token_type = urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN_1", response.body.access_token); %} #### Call Token Introspection with Token 1 POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & token = {{XCHD_ACCESS_TOKEN_1}} & token_type_hint = access_token ### Perform Token-exchange for acme-api-2 # INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2 # Perform (internal-to-internal) token exchange with audience extension POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = urn:ietf:params:oauth:grant-type:token-exchange & client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & subject_token = {{INITIAL_ACCESS_TOKEN}} & audience = {{ACME_API_2_CLIENT_ID}} & requested_token_type = urn:ietf:params:oauth:token-type:access_token & subject_token_type = urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN_2", response.body.access_token); %} ### Perform Token-exchange for acme-api-1 + acme-api-2 # INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2 # Perform (internal-to-internal) token exchange with audience extension POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = urn:ietf:params:oauth:grant-type:token-exchange & client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & subject_token = {{INITIAL_ACCESS_TOKEN}} & audience = {{ACME_API_1_CLIENT_ID}} & audience = {{ACME_API_2_CLIENT_ID}} & requested_token_type = urn:ietf:params:oauth:token-type:access_token & subject_token_type = urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN_1_2", response.body.access_token); %} ### Perform Token-exchange for acme-api-1 + acme-api-2 + scope domain1 # INITIAL_CLIENT (acme-app) -> REQUESTER_CLIENT (acme-api-gateway) -> acme-api-1, acme-api-2 # Perform (internal-to-internal) token exchange with audience extension POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = urn:ietf:params:oauth:grant-type:token-exchange & client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & subject_token = {{INITIAL_ACCESS_TOKEN}} & scope = profile+acme+domain1 & requested_token_type = urn:ietf:params:oauth:token-type:access_token & subject_token_type = urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN_1_2_3_4", response.body.access_token); %} ### Revoke Access token POST {{ISSUER}}/protocol/openid-connect/revoke Content-Type: application/x-www-form-urlencoded client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & token={{XCHD_ACCESS_TOKEN_1}} #### Call Token Introspection with Token 1 POST {{ISSUER}}/protocol/openid-connect/token/introspect Content-Type: application/x-www-form-urlencoded client_id = {{ACME_API_GATEWAY_CLIENT_ID}} & client_secret = {{ACME_API_GATEWAY_CLIENT_SECRET}} & token = {{XCHD_ACCESS_TOKEN_1}} & token_type_hint = access_token ================================================ FILE: keycloak/http-tests/token_exchange_fed-identity-chaining.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow POST {{ISSUER_A}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id = {{INITIAL_CLIENT_ID}} & client_secret = {{INITIAL_CLIENT_SECRET}} & username = {{USER_USERNAME}} & password = {{USER_PASSWORD}} & grant_type = password & scope = profile+openid > {% client.global.set("KC_INITIAL_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_INITIAL_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform (internal-to-internal) token exchange with audience extension # INITIAL_CLIENT -> REQUESTER_CLIENT (API-GATEWAY) -> TARGET_CLIENT POST {{ISSUER_A}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type = urn:ietf:params:oauth:grant-type:token-exchange & client_id = {{REQUESTER_CLIENT_ID}} & client_secret = {{REQUESTER_CLIENT_SECRET}} & subject_token = {{KC_INITIAL_ACCESS_TOKEN}} & audience = {{ISSUER_B}} & requested_token_type = urn:ietf:params:oauth:token-type:access_token & subject_token_type = urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN_A", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN_A", response.body.refresh_token); %} ### Perform JWT Authorization Grant in Domain B # POST {{ISSUER_B}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id = {{CLIENT_ID_B}} & client_secret = {{CLIENT_SECRET_B}} & grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer & assertion={{XCHD_ACCESS_TOKEN_A}} > {% client.global.set("XCHD_ACCESS_TOKEN_B", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN_B", response.body.refresh_token); %} ================================================ FILE: keycloak/http-tests/token_exchange_v2.http ================================================ ### Obtain tokens via Resource Owner Password Credentials Grant Flow POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded client_id={{INITIAL_CLIENT_ID}}&client_secret={{INITIAL_CLIENT_SECRET}}&username={{USER_USERNAME}}&password={{USER_PASSWORD}}&grant_type=password&scope=profile+openid > {% client.global.set("KC_INITIAL_ACCESS_TOKEN", response.body.access_token); client.global.set("KC_INITIAL_REFRESH_TOKEN", response.body.refresh_token); %} ### Perform (internal-to-internal) token exchange with audience extension # INITIAL_CLIENT -> REQUESTER_CLIENT (API-GATEWAY) -> TARGET_CLIENT POST {{ISSUER}}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange&client_id={{REQUESTER_CLIENT_ID}}&client_secret={{REQUESTER_CLIENT_SECRET}}&subject_token={{KC_INITIAL_ACCESS_TOKEN}}&audience={{TARGET_CLIENT_ID}}&requested_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token_type=urn:ietf:params:oauth:token-type:access_token > {% client.global.set("XCHD_ACCESS_TOKEN", response.body.access_token); client.global.set("XCHD_REFRESH_TOKEN", response.body.refresh_token); %} ================================================ FILE: keycloak/misc/custom-keycloak-server/pom.xml ================================================ 4.0.0 com.github.thomasdarimont.keycloak custom-keycloak-server 1.0.0-SNAPSHOT jar UTF-8 11 ${java.version} ${java.version} 18.0.0 2.7.5.Final mutable-jar com.google.auto.service auto-service 1.0.1 true provided org.keycloak keycloak-quarkus-dist ${keycloak.version} zip org.keycloak keycloak-quarkus-server ${keycloak.version} mysql mysql-connector-java io.quarkus quarkus-jdbc-mysql io.quarkus quarkus-jdbc-mysql-deployment com.microsoft.sqlserver mssql-jdbc io.quarkus quarkus-jdbc-mssql io.quarkus quarkus-jdbc-mssql-deployment com.oracle.database.jdbc ojdbc11 io.quarkus quarkus-jdbc-oracle io.quarkus quarkus-jdbc-oracle-deployment org.mariadb.jdbc mariadb-java-client io.quarkus quarkus-jdbc-mariadb io.quarkus quarkus-jdbc-mariadb-deployment com.h2database h2 io.quarkus quarkus-jdbc-h2 io.quarkus quarkus-jdbc-h2-deployment io.quarkus quarkus-logging-gelf ${quarkus.version} io.quarkus quarkus-logging-gelf-deployment ${quarkus.version} com.thoughtworks.xstream xstream 1.4.19 org.postgresql postgresql 42.3.5 com.fasterxml.jackson.core jackson-databind 2.13.2.2 org.jsoup jsoup 1.14.2 org.bouncycastle bcprov-jdk15on 1.70 org.apache.commons commons-compress 1.21 keycloak-${project.version} org.apache.maven.plugins maven-dependency-plugin unpack-keycloak-server-distribution package unpack org.keycloak keycloak-quarkus-dist zip target **/lib/** org.apache.maven.plugins maven-resources-plugin 3.2.0 add-additional-keycloak-resources package copy-resources ${project.build.directory}/keycloak-${keycloak.version} true ${project.basedir}/src/main/copy-to-keycloak false io.quarkus quarkus-maven-plugin ${quarkus.version} keycloak ${project.build.directory}/keycloak-${keycloak.version} build ================================================ FILE: keycloak/misc/custom-keycloak-server/readme.md ================================================ Custom Keycloak Server ---- Simple example for creating a custom Quarkus based Keycloak Distribution. Unwanted features can be removed via maven dependency excludes. # Build ``` mvn clean verify -DskipTests ``` # Run ``` target/keycloak-18.0.0/bin/kc.sh \ start-dev \ --db postgres \ --db-url-host localhost \ --db-username keycloak \ --db-password keycloak \ --http-port=8080 \ --http-relative-path=auth \ --spi-events-listener-jboss-logging-success-level=info \ --spi-events-listener-jboss-logging-error-level=warn \ --https-certificate-file=../../../config/stage/dev/tls/acme.test+1.pem \ --https-certificate-key-file=../../../config/stage/dev/tls/acme.test+1-key.pem ``` ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/copy-to-keycloak/conf/quarkus.properties ================================================ #quarkus.log.handler.gelf.enabled=true #quarkus.log.handler.gelf.host=localhost #quarkus.log.handler.gelf.port=12201 #quarkus.log.handler.gelf.facility=iam # Not yet supported by quarkus 2.7.5 #quarkus.log.console.json.additional-field."appSvc"=iam-keycloak #quarkus.log.console.json.additional-field."appGrp".value=iam #quarkus.log.console.json.additional-field."appStage".value=${KC_STAGE:dev} ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/java/demo/events/MyEventListener.java ================================================ package demo.events; import com.google.auto.service.AutoService; import org.keycloak.Config; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; import org.keycloak.events.EventListenerProviderFactory; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @AutoService(EventListenerProviderFactory.class) public class MyEventListener implements EventListenerProvider, EventListenerProviderFactory { @Override public String getId() { return "myevents"; } @Override public void onEvent(Event event) { System.out.println("UserEvent: " + event); } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { System.out.println("AdminEvent: " + event); } @Override public EventListenerProvider create(KeycloakSession session) { return new MyEventListener(); } @Override public void init(Config.Scope config) { } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } } ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/resources/META-INF/keycloak-themes.json ================================================ { "themes": [ { "name": "custom", "types": [ "login" ] } ] } ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/resources/META-INF/keycloak.conf ================================================ #db=postgres ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/messages/messages_en.properties ================================================ ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/resources/css/custom-login.css ================================================ /* white-login.css */ /* see: https://leaverou.github.io/css3patterns/ */ .login-pf body { background: radial-gradient(black 15%, transparent 16%) 0 0, radial-gradient(black 15%, transparent 16%) 8px 8px, radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 0 1px, radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 8px 9px !important; background-color: #282828 !important; background-size: 16px 16px !important; } ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/resources/js/custom-login.js ================================================ // custom-login.js (function onCustomLogin() { console.log("custom login"); })(); ================================================ FILE: keycloak/misc/custom-keycloak-server/src/main/resources/theme/custom/login/theme.properties ================================================ parent=keycloak import=common/keycloak # Custom Styles styles=css/login.css css/custom-login.css # Custom JavaScript scripts=js/custom-login.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 ================================================ FILE: keycloak/misc/snippets/create-keycloak-config-cli-client.txt ================================================ # Login bin/kcadm.sh config credentials \ --server http://localhost:8080/auth \ --realm master \ --user admin \ --password admin # Create Client bin/kcadm.sh create clients \ -r master \ -s clientId=keycloak-config-cli \ -s enabled=true \ -s clientAuthenticatorType=client-secret \ -s secret=mysecret \ -s standardFlowEnabled=false \ -s directAccessGrantsEnabled=false \ -s serviceAccountsEnabled=true # Add realm admin role to Service-Account bin/kcadm.sh add-roles \ -r master \ --uusername service-account-keycloak-config-cli \ --rolename admin ================================================ FILE: keycloak/misc/snippets/jgroups-debugging.txt ================================================ # Keycloak JAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/modules/system/layers/base/org/jgroups/main/jgroups-*.Final.jar org.jgroups.tests.Probe -v # Keycloak.X JAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/lib/lib/main/org.jgroups.jgroups-*.Final.jar org.jgroups.tests.Probe -v jmx ---- Keycloak Wildfly config (Not working!) /socket-binding-group=standard-sockets/socket-binding=jgroups-diagnostics:add(multicast-address="${jboss.jgroups.diagnostics_addr:224.0.0.75}",multicast-port="${jboss.jgroups.diagnostics_port:7500}") /subsystem=jgroups/stack=tcp/transport=TCP:write-attribute(name=diagnostics-socket-binding,value=jgroups-diagnostics) /subsystem=jgroups/stack=tcp/transport=TCP:write-attribute(name=properties.diag_enable_tcp,value=true) /subsystem=jgroups/stack=tcp/transport=TCP:write-attribute(name=properties.diagnostics_addr,value="224.0.0.75") /subsystem=jgroups/stack=udp/transport=UDP:write-attribute(name=diagnostics-socket-binding,value=jgroups-diagnostics) /subsystem=jgroups/stack=udp/transport=UDP:write-attribute(name=properties.diag_enable_udp,value=true) /subsystem=jgroups/stack=udp/transport=UDP:write-attribute(name=properties.diagnostics_addr,value="224.0.0.75") transport --- Probe output on Keycloak.x with jgroups-multicast-diag.xml ``` bash-4.4$ JAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/lib/lib/main/org.jgroups.jgroups-*.Final.jar org.jgroups.tests.Probe -v Picked up JAVA_TOOL_OPTIONS: addrs: [/224.0.75.75] udp: true, tcp: false #1 (176 bytes): local_addr=e78a8195221d-33544 physical_addr=172.18.0.3:35427 view=[e78a8195221d-33544|1] (2) [e78a8195221d-33544, 80a8d38a5520-59062] cluster=ISPN version=4.2.9.Final (Julier) #2 (176 bytes): local_addr=80a8d38a5520-59062 physical_addr=172.18.0.4:58324 view=[e78a8195221d-33544|1] (2) [e78a8195221d-33544, 80a8d38a5520-59062] cluster=ISPN version=4.2.9.Final (Julier) 2 responses (2 matches, 0 non matches) ``` ``` bash-4.4$ JAVA_TOOL_OPTIONS= java -cp /opt/jboss/keycloak/lib/lib/main/org.jgroups.jgroups-*.Final.jar org.jgroups.tests.Probe keys Picked up JAVA_TOOL_OPTIONS: #1 (445 bytes): local_addr=e78a8195221d-33544 [ip=172.18.0.3:35427, version=4.2.9.Final (Julier), cluster=ISPN, 2 mbr(s)] keys=digest-history dump-digest fix-digests dump keys uuids member-addrs props max-list-print-size[=number] print-protocols remove-protocol= insert-protocol==above | below= reset-stats jmx op=[] ops threads[=[=]] enable-cpu enable-contention disable-cpu disable-contention ispn-remote #2 (445 bytes): local_addr=80a8d38a5520-59062 [ip=172.18.0.4:58324, version=4.2.9.Final (Julier), cluster=ISPN, 2 mbr(s)] keys=digest-history dump-digest fix-digests dump keys uuids member-addrs props max-list-print-size[=number] print-protocols remove-protocol= insert-protocol==above | below= reset-stats jmx op=[] ops threads[=[=]] enable-cpu enable-contention disable-cpu disable-contention ispn-remote 2 responses (2 matches, 0 non matches) ``` ================================================ FILE: keycloak/misc/snippets/jmx-config-keycloakx.md ================================================ How to connect to Keycloak.X via JMX ---- # Setup ## Create a management user for JMX See `keycloak/config/jmxremote.password`, e.g. `controlRole`. # VisualVM [VisualVM](https://visualvm.github.io/) ## Start VisualVM ``` visualvm ``` ## Create new JMX Connection in VisualVM - JMX URL: `localhost:8790` or `service:jmx:rmi:///jndi/rmi://localhost:8790/jmxrmi` - Username: `controlRole` - Password: `password` - Do not require SSL: `on` (for the demo...) # Java Mission Control (JMC) [Java Mission Control](https://openjdk.java.net/projects/jmc/) ## Start Java Mission Control ``` jmc ``` ## Create new JMX Connection in Java Mission Control - JMX URL: `localhost:8790` or `service:jmx:rmi:///jndi/rmi://localhost:8790/jmxrmi` - Username: `controlRole` - Password: `password` ================================================ FILE: keycloak/misc/snippets/jmx-config-wildfly.md ================================================ How to connect to Keycloak via JMX ---- # Setup ## Create a management user for JMX See deployments/local/dev/keycloak/Dockerfile ``` docker exec -it dev_acme-keycloak_1 /opt/jboss/keycloak/bin/add-user.sh jmxuser password ``` ## Export jboss-client.jar locally ``` docker cp dev_acme-keycloak_1:/opt/jboss/keycloak/bin/client/jboss-client.jar . ``` # VisualVM [VisualVM](https://visualvm.github.io/) ## Start VisualVM with jboss-client.jar ``` visualvm -cp:a ./jboss-client.jar ``` ## Create new JMX Connection in VisualVM - JMX URL: `service:jmx:remote+http://localhost:9990` - Username: `jmxuser` - Password: `password` - Do not require SSL: `on` (for the demo...) # Java Mission Control (JMC) [Java Mission Control](https://openjdk.java.net/projects/jmc/) ## Add jboss-client.jar bundle to JMC Currently, JMC cannot be used with the plain `jboss-client.jar` since it is lacking some osgi bundle metadata. As a workaround we create a patched `jboss-client.jar` with the missing osgi bundle metadata. We create a file with the additional osgi bundle metadata, e.g.: `jboss-jmx.mf`: ``` Bundle-ManifestVersion: 2 Bundle-SymbolicName: org.jboss.client Bundle-Version: 1.0 Bundle-Name: JBoss Client Library Fragment-Host: org.openjdk.jmc.rjmx Export-Package: * Automatic-Module-Name: org.jboss.client ``` Then we create a patched local version of the `jboss-client.jar`. ``` cp /home/tom/dev/playground/keycloak/keycloak-16.1.0/bin/client/jboss-client.jar . # docker cp dev_acme-keycloak_1:/opt/jboss/keycloak/bin/client/jboss-client.jar . jar -ufm ./jboss-client.jar jboss-jmx.mf cp ./jboss-client.jar "/home/tom/.sdkman/candidates/jmc/8.1.1.51-zulu/Azul Mission Control/dropins" ``` We then copy the `jboss-client.jar` file into the `dropins` folder of JMC: ``` cp ./jboss-client.jar /path/to/jmc/dropins/ ``` We can then start JMC and create a new JMX connection as shown below. See: - https://access.redhat.com/solutions/5897561 - https://github.com/thomasdarimont/keycloak-jmx-jmc-poc ## Create new JMX Connection in Java Mission Control - JMX URL: `service:jmx:remote+http://localhost:9990` - Username: `jmxuser` - Password: `password` ================================================ FILE: keycloak/misc/snippets/jvm-settings.txt ================================================ # see https://support.datastax.com/s/article/FAQ-How-to-disable-client-initiated-TLS-renegotiation -Djdk.tls.rejectClientInitiatedRenegotiation=true -Djdk.tls.rejectClientInitializedRenego=true ================================================ FILE: keycloak/misc/snippets/keycloakx-cli.md ================================================ Keycloak.X CLI Examples ---- # Run Keycloak.X with HTTPS ``` bin/kc.sh \ --verbose \ start \ --auto-build \ --http-enabled=true \ --http-relative-path=/auth \ --hostname=id.acme.test:8443 \ --https-certificate-file=/home/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-template/config/stage/dev/tls/acme.test+1.pem \ --https-certificate-key-file=/home/tom/dev/repos/gh/thomasdarimont/keycloak-dev/keycloak-project-template/config/stage/dev/tls/acme.test+1-key.pem \ --https-protocols=TLSv1.3,TLSv1.2 \ --proxy=passthrough \ --metrics-enabled=false \ --cache=local ``` --https-trust-store-file=/path/to/file --https.trust-store.password= ================================================ FILE: keycloak/misc/snippets/metrics-examples.txt ================================================ # HELP application_keycloak_admin_event_UPDATE_total Generic KeyCloak Admin event # TYPE application_keycloak_admin_event_UPDATE_total counter application_keycloak_admin_event_UPDATE_total{realm="acme-internal",resource="USER"} 2.0 # HELP application_keycloak_clients_total Total clients # TYPE application_keycloak_clients_total gauge application_keycloak_clients_total{realm="acme-apps"} 8.0 application_keycloak_clients_total{realm="acme-demo"} 9.0 application_keycloak_clients_total{realm="acme-internal"} 12.0 application_keycloak_clients_total{realm="acme-ldap"} 9.0 application_keycloak_clients_total{realm="acme-ops"} 7.0 application_keycloak_clients_total{realm="acme-saml"} 7.0 application_keycloak_clients_total{realm="master"} 13.0 application_keycloak_clients_total{realm="workshop"} 6.0 # HELP application_keycloak_groups_total Total groups # TYPE application_keycloak_groups_total gauge application_keycloak_groups_total{realm="acme-apps"} 0.0 application_keycloak_groups_total{realm="acme-demo"} 0.0 application_keycloak_groups_total{realm="acme-internal"} 1.0 application_keycloak_groups_total{realm="acme-ldap"} 0.0 application_keycloak_groups_total{realm="acme-ops"} 0.0 application_keycloak_groups_total{realm="acme-saml"} 0.0 application_keycloak_groups_total{realm="master"} 0.0 application_keycloak_groups_total{realm="workshop"} 2.0 # HELP application_keycloak_metrics_refresh_total_milliseconds Duration of Keycloak Metrics refresh in milliseconds. # TYPE application_keycloak_metrics_refresh_total_milliseconds gauge application_keycloak_metrics_refresh_total_milliseconds 7.0 # HELP application_keycloak_oauth_code_to_token_success_total Total code to token exchanges # TYPE application_keycloak_oauth_code_to_token_success_total counter application_keycloak_oauth_code_to_token_success_total{client_id="app-minispa",provider="keycloak",realm="acme-internal"} 3.0 # HELP application_keycloak_oauth_token_refresh_error_total Total errors during token refreshes # TYPE application_keycloak_oauth_token_refresh_error_total counter application_keycloak_oauth_token_refresh_error_total{client_id="app-minispa",error="invalid_token",provider="keycloak",realm="acme-internal"} 1.0 # HELP application_keycloak_oauth_token_refresh_success_total Total token refreshes # TYPE application_keycloak_oauth_token_refresh_success_total counter application_keycloak_oauth_token_refresh_success_total{client_id="app-minispa",realm="acme-internal"} 1.0 # HELP application_keycloak_realms_total Total realms # TYPE application_keycloak_realms_total gauge application_keycloak_realms_total 8.0 # HELP application_keycloak_server_version Keycloak Server Version # TYPE application_keycloak_server_version gauge application_keycloak_server_version{version="15.0.2"} 0.0 # HELP application_keycloak_user_login_error_total Total errors during user logins # TYPE application_keycloak_user_login_error_total counter application_keycloak_user_login_error_total{client_id="app-minispa",error="invalid_user_credentials",provider="keycloak",realm="acme-internal"} 3.0 application_keycloak_user_login_error_total{client_id="app-minispa",error="user_disabled",provider="keycloak",realm="acme-internal"} 1.0 application_keycloak_user_login_error_total{client_id="app-minispa",error="user_not_found",provider="keycloak",realm="acme-internal"} 1.0 # HELP application_keycloak_user_login_success_total Total successful user logins # TYPE application_keycloak_user_login_success_total counter application_keycloak_user_login_success_total{client_id="app-minispa",provider="keycloak",realm="acme-internal"} 3.0 # HELP application_keycloak_user_logout_success_total Total successful user logouts # TYPE application_keycloak_user_logout_success_total counter application_keycloak_user_logout_success_total{provider="keycloak",realm="acme-internal"} 2.0 # HELP application_keycloak_users_total Total users # TYPE application_keycloak_users_total gauge application_keycloak_users_total{realm="acme-apps"} 2.0 application_keycloak_users_total{realm="acme-demo"} 1.0 application_keycloak_users_total{realm="acme-internal"} 3.0 application_keycloak_users_total{realm="acme-ldap"} 0.0 application_keycloak_users_total{realm="acme-ops"} 1.0 application_keycloak_users_total{realm="acme-saml"} 1.0 application_keycloak_users_total{realm="master"} 1.0 application_keycloak_users_total{realm="workshop"} 3.0 # HELP base_classloader_loadedClasses_total Displays the total number of classes that have been loaded since the Java virtual machine has started execution. # TYPE base_classloader_loadedClasses_total counter base_classloader_loadedClasses_total 34122.0 # HELP base_classloader_loadedClasses_count Displays the number of classes that are currently loaded in the Java virtual machine. # TYPE base_classloader_loadedClasses_count gauge base_classloader_loadedClasses_count 33296.0 # HELP base_classloader_unloadedClasses_total Displays the total number of classes unloaded since the Java virtual machine has started execution. # TYPE base_classloader_unloadedClasses_total counter base_classloader_unloadedClasses_total 827.0 # HELP base_cpu_availableProcessors Displays the number of processors available to the Java virtual machine. This value may change during a particular invocation of the virtual machine. # TYPE base_cpu_availableProcessors gauge base_cpu_availableProcessors 12.0 # HELP base_cpu_processCpuLoad Displays the "recent cpu usage" for the Java Virtual Machine process. # TYPE base_cpu_processCpuLoad gauge base_cpu_processCpuLoad 0.0 # HELP base_cpu_processCpuTime_seconds Displays the CPU time used by the process on which the Java virtual machine is running in nanoseconds # TYPE base_cpu_processCpuTime_seconds gauge base_cpu_processCpuTime_seconds 74.44 # HELP base_cpu_systemLoadAverage Displays the system load average for the last minute. The system load average is the sum of the number of runnable entities queued to the available processors and the number of runnable entities running on the available processors averaged over a period of time. The way in which the load average is calculated is operating system specific but is typically a damped time-dependent average. If the load average is not available, a negative value is displayed. This attribute is designed to provide a hint about the system load and may be queried frequently. The load average may be unavailable on some platform where it is expensive to implement this method. # TYPE base_cpu_systemLoadAverage gauge base_cpu_systemLoadAverage 2.55 # HELP base_gc_total Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this collector. # TYPE base_gc_total counter base_gc_total{name="G1 Old Generation"} 0.0 base_gc_total{name="G1 Young Generation"} 49.0 # HELP base_gc_time_total Displays the approximate accumulated collection elapsed time in milliseconds. This attribute displays -1 if the collection elapsed time is undefined for this collector. The Java virtual machine implementation may use a high resolution timer to measure the elapsed time. This attribute may display the same value even if the collection count has been incremented if the collection elapsed time is very short. # TYPE base_gc_time_total counter base_gc_time_total_seconds{name="G1 Old Generation"} 0.0 base_gc_time_total_seconds{name="G1 Young Generation"} 0.47600000000000003 # HELP base_jvm_uptime_seconds Displays the uptime of the Java virtual machine # TYPE base_jvm_uptime_seconds gauge base_jvm_uptime_seconds 95.424 # HELP base_memory_committedHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use. # TYPE base_memory_committedHeap_bytes gauge base_memory_committedHeap_bytes 1.68820736E8 # HELP base_memory_committedNonHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use. # TYPE base_memory_committedNonHeap_bytes gauge base_memory_committedNonHeap_bytes 2.7222016E8 # HELP base_memory_maxHeap_bytes Displays the maximum amount of memory in bytes that can be used for memory management. # TYPE base_memory_maxHeap_bytes gauge base_memory_maxHeap_bytes 5.36870912E8 # HELP base_memory_maxNonHeap_bytes Displays the maximum amount of memory in bytes that can be used for memory management. # TYPE base_memory_maxNonHeap_bytes gauge base_memory_maxNonHeap_bytes 7.80140544E8 # HELP base_memory_usedHeap_bytes Displays the amount of used memory. # TYPE base_memory_usedHeap_bytes gauge base_memory_usedHeap_bytes 1.02858256E8 # HELP base_memory_usedNonHeap_bytes Displays the amount of used memory. # TYPE base_memory_usedNonHeap_bytes gauge base_memory_usedNonHeap_bytes 2.515282E8 # HELP base_thread_count Number of currently deployed threads # TYPE base_thread_count gauge base_thread_count 157.0 # HELP base_thread_daemon_count Displays the current number of live daemon threads. # TYPE base_thread_daemon_count gauge base_thread_daemon_count 90.0 # HELP base_thread_max_count Displays the peak live thread count since the Java virtual machine started or peak was reset. This includes daemon and non-daemon threads. # TYPE base_thread_max_count gauge base_thread_max_count 224.0 # HELP vendor_BufferPool_used_memory_bytes The memory used by the NIO pool # TYPE vendor_BufferPool_used_memory_bytes gauge vendor_BufferPool_used_memory_bytes{name="direct"} 2151424.0 vendor_BufferPool_used_memory_bytes{name="mapped"} 0.0 # HELP vendor_memoryPool_usage_bytes Current usage of the memory pool # TYPE vendor_memoryPool_usage_bytes gauge vendor_memoryPool_usage_bytes{name="CodeHeap 'non-nmethods'"} 1652096.0 vendor_memoryPool_usage_bytes{name="CodeHeap 'non-profiled nmethods'"} 1.2118656E7 vendor_memoryPool_usage_bytes{name="CodeHeap 'profiled nmethods'"} 4.4642688E7 vendor_memoryPool_usage_bytes{name="Compressed Class Space"} 2.2511248E7 vendor_memoryPool_usage_bytes{name="G1 Eden Space"} 1048576.0 vendor_memoryPool_usage_bytes{name="G1 Old Gen"} 9.8663952E7 vendor_memoryPool_usage_bytes{name="G1 Survivor Space"} 3145728.0 vendor_memoryPool_usage_bytes{name="Metaspace"} 1.70610664E8 # HELP vendor_memoryPool_usage_max_bytes Peak usage of the memory pool # TYPE vendor_memoryPool_usage_max_bytes gauge vendor_memoryPool_usage_max_bytes{name="CodeHeap 'non-nmethods'"} 1747328.0 vendor_memoryPool_usage_max_bytes{name="CodeHeap 'non-profiled nmethods'"} 1.2118656E7 vendor_memoryPool_usage_max_bytes{name="CodeHeap 'profiled nmethods'"} 4.4642688E7 vendor_memoryPool_usage_max_bytes{name="Compressed Class Space"} 2.2513456E7 vendor_memoryPool_usage_max_bytes{name="G1 Eden Space"} 5.5574528E7 vendor_memoryPool_usage_max_bytes{name="G1 Old Gen"} 9.8663952E7 vendor_memoryPool_usage_max_bytes{name="G1 Survivor Space"} 8388608.0 vendor_memoryPool_usage_max_bytes{name="Metaspace"} 1.7062692E8 # HELP wildfly_datasources_jdbc_prepared_statement_cache_access_count The number of times that the statement cache was accessed # TYPE wildfly_datasources_jdbc_prepared_statement_cache_access_count gauge wildfly_datasources_jdbc_prepared_statement_cache_access_count{data_source="ExampleDS"} 0.0 wildfly_datasources_jdbc_prepared_statement_cache_access_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_jdbc_prepared_statement_cache_add_count The number of statements added to the statement cache # TYPE wildfly_datasources_jdbc_prepared_statement_cache_add_count gauge wildfly_datasources_jdbc_prepared_statement_cache_add_count{data_source="ExampleDS"} 0.0 wildfly_datasources_jdbc_prepared_statement_cache_add_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_jdbc_prepared_statement_cache_current_size The number of prepared and callable statements currently cached in the statement cache # TYPE wildfly_datasources_jdbc_prepared_statement_cache_current_size gauge wildfly_datasources_jdbc_prepared_statement_cache_current_size{data_source="ExampleDS"} 0.0 wildfly_datasources_jdbc_prepared_statement_cache_current_size{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_jdbc_prepared_statement_cache_delete_count The number of statements discarded from the cache # TYPE wildfly_datasources_jdbc_prepared_statement_cache_delete_count gauge wildfly_datasources_jdbc_prepared_statement_cache_delete_count{data_source="ExampleDS"} 0.0 wildfly_datasources_jdbc_prepared_statement_cache_delete_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_jdbc_prepared_statement_cache_hit_count The number of times that statements from the cache were used # TYPE wildfly_datasources_jdbc_prepared_statement_cache_hit_count gauge wildfly_datasources_jdbc_prepared_statement_cache_hit_count{data_source="ExampleDS"} 0.0 wildfly_datasources_jdbc_prepared_statement_cache_hit_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_jdbc_prepared_statement_cache_miss_count The number of times that a statement request could not be satisfied with a statement from the cache # TYPE wildfly_datasources_jdbc_prepared_statement_cache_miss_count gauge wildfly_datasources_jdbc_prepared_statement_cache_miss_count{data_source="ExampleDS"} 0.0 wildfly_datasources_jdbc_prepared_statement_cache_miss_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_active_count The active count # TYPE wildfly_datasources_pool_active_count gauge wildfly_datasources_pool_active_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_active_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_available_count The available count # TYPE wildfly_datasources_pool_available_count gauge wildfly_datasources_pool_available_count{data_source="ExampleDS"} 20.0 wildfly_datasources_pool_available_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_average_blocking_time Average Blocking Time for pool # TYPE wildfly_datasources_pool_average_blocking_time gauge wildfly_datasources_pool_average_blocking_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_average_blocking_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_average_creation_time The average time spent creating a physical connection # TYPE wildfly_datasources_pool_average_creation_time gauge wildfly_datasources_pool_average_creation_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_average_creation_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_average_get_time The average time spent obtaining a physical connection # TYPE wildfly_datasources_pool_average_get_time gauge wildfly_datasources_pool_average_get_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_average_get_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_average_pool_time The average time for a physical connection spent in the pool # TYPE wildfly_datasources_pool_average_pool_time gauge wildfly_datasources_pool_average_pool_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_average_pool_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_average_usage_time The average time spent using a physical connection # TYPE wildfly_datasources_pool_average_usage_time gauge wildfly_datasources_pool_average_usage_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_average_usage_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_blocking_failure_count The number of failures trying to obtain a physical connection # TYPE wildfly_datasources_pool_blocking_failure_count gauge wildfly_datasources_pool_blocking_failure_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_blocking_failure_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_created_count The created count # TYPE wildfly_datasources_pool_created_count gauge wildfly_datasources_pool_created_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_created_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_destroyed_count The destroyed count # TYPE wildfly_datasources_pool_destroyed_count gauge wildfly_datasources_pool_destroyed_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_destroyed_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_idle_count The number of physical connections currently idle # TYPE wildfly_datasources_pool_idle_count gauge wildfly_datasources_pool_idle_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_idle_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_in_use_count The number of physical connections currently in use # TYPE wildfly_datasources_pool_in_use_count gauge wildfly_datasources_pool_in_use_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_in_use_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_creation_time The maximum time for creating a physical connection # TYPE wildfly_datasources_pool_max_creation_time gauge wildfly_datasources_pool_max_creation_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_creation_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_get_time The maximum time for obtaining a physical connection # TYPE wildfly_datasources_pool_max_get_time gauge wildfly_datasources_pool_max_get_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_get_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_pool_time The maximum time for a physical connection in the pool # TYPE wildfly_datasources_pool_max_pool_time gauge wildfly_datasources_pool_max_pool_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_pool_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_usage_time The maximum time using a physical connection # TYPE wildfly_datasources_pool_max_usage_time gauge wildfly_datasources_pool_max_usage_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_usage_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_used_count The maximum number of connections used # TYPE wildfly_datasources_pool_max_used_count gauge wildfly_datasources_pool_max_used_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_used_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_wait_count The maximum number of threads waiting for a connection # TYPE wildfly_datasources_pool_max_wait_count gauge wildfly_datasources_pool_max_wait_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_wait_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_max_wait_time The maximum wait time for a connection # TYPE wildfly_datasources_pool_max_wait_time gauge wildfly_datasources_pool_max_wait_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_max_wait_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_timed_out The timed out count # TYPE wildfly_datasources_pool_timed_out gauge wildfly_datasources_pool_timed_out{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_timed_out{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_total_blocking_time The total blocking time # TYPE wildfly_datasources_pool_total_blocking_time gauge wildfly_datasources_pool_total_blocking_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_total_blocking_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_total_creation_time The total time spent creating physical connections # TYPE wildfly_datasources_pool_total_creation_time gauge wildfly_datasources_pool_total_creation_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_total_creation_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_total_get_time The total time spent obtaining physical connections # TYPE wildfly_datasources_pool_total_get_time gauge wildfly_datasources_pool_total_get_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_total_get_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_total_pool_time The total time spent by physical connections in the pool # TYPE wildfly_datasources_pool_total_pool_time gauge wildfly_datasources_pool_total_pool_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_total_pool_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_total_usage_time The total time spent using physical connections # TYPE wildfly_datasources_pool_total_usage_time gauge wildfly_datasources_pool_total_usage_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_total_usage_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_wait_count The number of requests that had to wait to obtain a physical connection # TYPE wildfly_datasources_pool_wait_count gauge wildfly_datasources_pool_wait_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_wait_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xacommit_average_time The average time for a XAResource commit invocation # TYPE wildfly_datasources_pool_xacommit_average_time gauge wildfly_datasources_pool_xacommit_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xacommit_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xacommit_count The number of XAResource commit invocations # TYPE wildfly_datasources_pool_xacommit_count gauge wildfly_datasources_pool_xacommit_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xacommit_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xacommit_max_time The maximum time for a XAResource commit invocation # TYPE wildfly_datasources_pool_xacommit_max_time gauge wildfly_datasources_pool_xacommit_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xacommit_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xacommit_total_time The total time for all XAResource commit invocations # TYPE wildfly_datasources_pool_xacommit_total_time gauge wildfly_datasources_pool_xacommit_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xacommit_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaend_average_time The average time for a XAResource end invocation # TYPE wildfly_datasources_pool_xaend_average_time gauge wildfly_datasources_pool_xaend_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaend_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaend_count The number of XAResource end invocations # TYPE wildfly_datasources_pool_xaend_count gauge wildfly_datasources_pool_xaend_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaend_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaend_max_time The maximum time for a XAResource end invocation # TYPE wildfly_datasources_pool_xaend_max_time gauge wildfly_datasources_pool_xaend_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaend_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaend_total_time The total time for all XAResource end invocations # TYPE wildfly_datasources_pool_xaend_total_time gauge wildfly_datasources_pool_xaend_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaend_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaforget_average_time The average time for a XAResource forget invocation # TYPE wildfly_datasources_pool_xaforget_average_time gauge wildfly_datasources_pool_xaforget_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaforget_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaforget_count The number of XAResource forget invocations # TYPE wildfly_datasources_pool_xaforget_count gauge wildfly_datasources_pool_xaforget_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaforget_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaforget_max_time The maximum time for a XAResource forget invocation # TYPE wildfly_datasources_pool_xaforget_max_time gauge wildfly_datasources_pool_xaforget_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaforget_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaforget_total_time The total time for all XAResource forget invocations # TYPE wildfly_datasources_pool_xaforget_total_time gauge wildfly_datasources_pool_xaforget_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaforget_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaprepare_average_time The average time for a XAResource prepare invocation # TYPE wildfly_datasources_pool_xaprepare_average_time gauge wildfly_datasources_pool_xaprepare_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaprepare_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaprepare_count The number of XAResource prepare invocations # TYPE wildfly_datasources_pool_xaprepare_count gauge wildfly_datasources_pool_xaprepare_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaprepare_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaprepare_max_time The maximum time for a XAResource prepare invocation # TYPE wildfly_datasources_pool_xaprepare_max_time gauge wildfly_datasources_pool_xaprepare_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaprepare_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xaprepare_total_time The total time for all XAResource prepare invocations # TYPE wildfly_datasources_pool_xaprepare_total_time gauge wildfly_datasources_pool_xaprepare_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xaprepare_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarecover_average_time The average time for a XAResource recover invocation # TYPE wildfly_datasources_pool_xarecover_average_time gauge wildfly_datasources_pool_xarecover_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarecover_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarecover_count The number of XAResource recover invocations # TYPE wildfly_datasources_pool_xarecover_count gauge wildfly_datasources_pool_xarecover_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarecover_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarecover_max_time The maximum time for a XAResource recover invocation # TYPE wildfly_datasources_pool_xarecover_max_time gauge wildfly_datasources_pool_xarecover_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarecover_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarecover_total_time The total time for all XAResource recover invocations # TYPE wildfly_datasources_pool_xarecover_total_time gauge wildfly_datasources_pool_xarecover_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarecover_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarollback_average_time The average time for a XAResource rollback invocation # TYPE wildfly_datasources_pool_xarollback_average_time gauge wildfly_datasources_pool_xarollback_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarollback_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarollback_count The number of XAResource rollback invocations # TYPE wildfly_datasources_pool_xarollback_count gauge wildfly_datasources_pool_xarollback_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarollback_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarollback_max_time The maximum time for a XAResource rollback invocation # TYPE wildfly_datasources_pool_xarollback_max_time gauge wildfly_datasources_pool_xarollback_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarollback_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xarollback_total_time The total time for all XAResource rollback invocations # TYPE wildfly_datasources_pool_xarollback_total_time gauge wildfly_datasources_pool_xarollback_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xarollback_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xastart_average_time The average time for a XAResource start invocation # TYPE wildfly_datasources_pool_xastart_average_time gauge wildfly_datasources_pool_xastart_average_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xastart_average_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xastart_count The number of XAResource start invocations # TYPE wildfly_datasources_pool_xastart_count gauge wildfly_datasources_pool_xastart_count{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xastart_count{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xastart_max_time The maximum time for a XAResource start invocation # TYPE wildfly_datasources_pool_xastart_max_time gauge wildfly_datasources_pool_xastart_max_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xastart_max_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_datasources_pool_xastart_total_time The total time for all XAResource start invocations # TYPE wildfly_datasources_pool_xastart_total_time gauge wildfly_datasources_pool_xastart_total_time{data_source="ExampleDS"} 0.0 wildfly_datasources_pool_xastart_total_time{data_source="KeycloakDS"} 0.0 # HELP wildfly_ee_active_thread_count The approximate number of threads that are actively executing tasks. # TYPE wildfly_ee_active_thread_count gauge wildfly_ee_active_thread_count{managed_executor_service="default"} 0.0 wildfly_ee_active_thread_count{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ee_completed_task_count The approximate total number of tasks that have completed execution. # TYPE wildfly_ee_completed_task_count gauge wildfly_ee_completed_task_count{managed_executor_service="default"} 0.0 wildfly_ee_completed_task_count{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ee_current_queue_size The current size of the executor's task queue. # TYPE wildfly_ee_current_queue_size gauge wildfly_ee_current_queue_size{managed_executor_service="default"} 0.0 wildfly_ee_current_queue_size{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ee_hung_thread_count The number of executor threads that are hung. # TYPE wildfly_ee_hung_thread_count gauge wildfly_ee_hung_thread_count{managed_executor_service="default"} 0.0 wildfly_ee_hung_thread_count{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ee_max_thread_count The largest number of executor threads. # TYPE wildfly_ee_max_thread_count gauge wildfly_ee_max_thread_count{managed_executor_service="default"} 0.0 wildfly_ee_max_thread_count{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ee_task_count The approximate total number of tasks that have ever been submitted for execution. # TYPE wildfly_ee_task_count gauge wildfly_ee_task_count{managed_executor_service="default"} 0.0 wildfly_ee_task_count{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ee_thread_count The current number of executor threads. # TYPE wildfly_ee_thread_count gauge wildfly_ee_thread_count{managed_executor_service="default"} 0.0 wildfly_ee_thread_count{managed_scheduled_executor_service="default"} 0.0 # HELP wildfly_ejb3_active_count The approximate number of threads that are actively executing tasks. # TYPE wildfly_ejb3_active_count gauge wildfly_ejb3_active_count{thread_pool="default"} 0.0 # HELP wildfly_ejb3_completed_task_count The approximate total number of tasks that have completed execution. # TYPE wildfly_ejb3_completed_task_count gauge wildfly_ejb3_completed_task_count{thread_pool="default"} 0.0 # HELP wildfly_ejb3_current_thread_count The current number of threads in the pool. # TYPE wildfly_ejb3_current_thread_count gauge wildfly_ejb3_current_thread_count{thread_pool="default"} 0.0 # HELP wildfly_ejb3_largest_thread_count The largest number of threads that have ever simultaneously been in the pool. # TYPE wildfly_ejb3_largest_thread_count gauge wildfly_ejb3_largest_thread_count{thread_pool="default"} 0.0 # HELP wildfly_ejb3_queue_size The queue size. # TYPE wildfly_ejb3_queue_size gauge wildfly_ejb3_queue_size{thread_pool="default"} 0.0 # HELP wildfly_ejb3_rejected_count The number of tasks that have been rejected. # TYPE wildfly_ejb3_rejected_count gauge wildfly_ejb3_rejected_count{thread_pool="default"} 0.0 # HELP wildfly_ejb3_task_count The approximate total number of tasks that have ever been scheduled for execution. # TYPE wildfly_ejb3_task_count gauge wildfly_ejb3_task_count{thread_pool="default"} 0.0 # HELP wildfly_infinispan_activations_total The number of cache node activations (bringing a node into memory from a cache store). # TYPE wildfly_infinispan_activations_total counter wildfly_infinispan_activations_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="authorization"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_activations_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="keys"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="realms"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="users"} 0.0 wildfly_infinispan_activations_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_average_read_time_seconds Average time (in ms) for cache reads. Includes hits and misses. # TYPE wildfly_infinispan_average_read_time_seconds gauge wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_average_read_time_seconds{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_average_remove_time_seconds Average time (in ms) for cache removes. # TYPE wildfly_infinispan_average_remove_time_seconds gauge wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_average_remove_time_seconds{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_average_replication_time_total The average time taken to replicate data around the cluster. # TYPE wildfly_infinispan_average_replication_time_total counter wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_average_replication_time_total_seconds{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_average_write_time_seconds Average time (in ms) for cache writes. # TYPE wildfly_infinispan_average_write_time_seconds gauge wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_average_write_time_seconds{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_commits_total The number of transaction commits. # TYPE wildfly_infinispan_commits_total counter wildfly_infinispan_commits_total{cache_container="ejb",cache="http-remoting-connector",component="transaction"} 0.0 # HELP wildfly_infinispan_current_concurrency_level The estimated number of concurrently updating threads which this cache can support. # TYPE wildfly_infinispan_current_concurrency_level gauge wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="actionTokens",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="authenticationSessions",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="clientSessions",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="ejb",cache="http-remoting-connector",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="loginFailures",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="offlineClientSessions",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="offlineSessions",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="sessions",component="locking"} 1000.0 wildfly_infinispan_current_concurrency_level{cache_container="keycloak",cache="work",component="locking"} 1000.0 # HELP wildfly_infinispan_evictions_total The number of cache eviction operations. # TYPE wildfly_infinispan_evictions_total counter wildfly_infinispan_evictions_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_evictions_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_evictions_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_hit_ratio The hit/miss ratio for the cache (hits/hits+misses). # TYPE wildfly_infinispan_hit_ratio gauge wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_hit_ratio{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_hit_ratio{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_hits_total The number of cache attribute hits. # TYPE wildfly_infinispan_hits_total counter wildfly_infinispan_hits_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_hits_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_hits_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_misses_total The number of cache attribute misses. # TYPE wildfly_infinispan_misses_total counter wildfly_infinispan_misses_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_misses_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_misses_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_number_of_entries The number of entries in the cache including passivated entries. # TYPE wildfly_infinispan_number_of_entries gauge wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="clientSessions"} 1.0 wildfly_infinispan_number_of_entries{cache_container="ejb",cache="http-remoting-connector"} 2.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="sessions"} 1.0 wildfly_infinispan_number_of_entries{cache_container="keycloak",cache="work"} 79.0 # HELP wildfly_infinispan_number_of_entries_in_memory The number of entries in the cache excluding passivated entries. # TYPE wildfly_infinispan_number_of_entries_in_memory gauge wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="clientSessions"} 1.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="ejb",cache="http-remoting-connector"} 2.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="sessions"} 1.0 wildfly_infinispan_number_of_entries_in_memory{cache_container="keycloak",cache="work"} 79.0 # HELP wildfly_infinispan_number_of_locks_available The number of locks available to this cache. # TYPE wildfly_infinispan_number_of_locks_available gauge wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="actionTokens",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="authenticationSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="clientSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="ejb",cache="http-remoting-connector",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="loginFailures",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="offlineClientSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="offlineSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="sessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_available{cache_container="keycloak",cache="work",component="locking"} 0.0 # HELP wildfly_infinispan_number_of_locks_held The number of locks currently in use by this cache. # TYPE wildfly_infinispan_number_of_locks_held gauge wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="actionTokens",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="authenticationSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="clientSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="ejb",cache="http-remoting-connector",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="loginFailures",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="offlineClientSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="offlineSessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="sessions",component="locking"} 0.0 wildfly_infinispan_number_of_locks_held{cache_container="keycloak",cache="work",component="locking"} 0.0 # HELP wildfly_infinispan_passivations_total The number of cache node passivations (passivating a node from memory to a cache store). # TYPE wildfly_infinispan_passivations_total counter wildfly_infinispan_passivations_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="authorization"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_passivations_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="keys"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="realms"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="users"} 0.0 wildfly_infinispan_passivations_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_prepares_total The number of transaction prepares. # TYPE wildfly_infinispan_prepares_total counter wildfly_infinispan_prepares_total{cache_container="ejb",cache="http-remoting-connector",component="transaction"} 0.0 # HELP wildfly_infinispan_read_write_ratio The read/write ratio of the cache ((hits+misses)/stores). # TYPE wildfly_infinispan_read_write_ratio gauge wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_read_write_ratio{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_remove_hits_total The number of cache attribute remove hits. # TYPE wildfly_infinispan_remove_hits_total counter wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_remove_hits_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_remove_misses_total The number of cache attribute remove misses. # TYPE wildfly_infinispan_remove_misses_total counter wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_remove_misses_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_replication_count_total The number of times data was replicated around the cluster. # TYPE wildfly_infinispan_replication_count_total counter wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="actionTokens"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="authenticationSessions"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="clientSessions"} -1.0 wildfly_infinispan_replication_count_total{cache_container="ejb",cache="http-remoting-connector"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="loginFailures"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="offlineClientSessions"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="offlineSessions"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="sessions"} -1.0 wildfly_infinispan_replication_count_total{cache_container="keycloak",cache="work"} -1.0 # HELP wildfly_infinispan_replication_failures_total The number of data replication failures. # TYPE wildfly_infinispan_replication_failures_total counter wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="actionTokens"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="authenticationSessions"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="clientSessions"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="ejb",cache="http-remoting-connector"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="loginFailures"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="offlineClientSessions"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="offlineSessions"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="sessions"} -1.0 wildfly_infinispan_replication_failures_total{cache_container="keycloak",cache="work"} -1.0 # HELP wildfly_infinispan_rollbacks_total The number of transaction rollbacks. # TYPE wildfly_infinispan_rollbacks_total counter wildfly_infinispan_rollbacks_total{cache_container="ejb",cache="http-remoting-connector",component="transaction"} 0.0 # HELP wildfly_infinispan_success_ratio The data replication success ratio (successes/successes+failures). # TYPE wildfly_infinispan_success_ratio gauge wildfly_infinispan_success_ratio{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_success_ratio{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_success_ratio{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_infinispan_time_since_reset_seconds Time (in secs) since cache statistics were reset. # TYPE wildfly_infinispan_time_since_reset_seconds gauge wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="actionTokens"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="authenticationSessions"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="clientSessions"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="ejb",cache="http-remoting-connector"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="loginFailures"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="offlineClientSessions"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="offlineSessions"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="sessions"} 87.0 wildfly_infinispan_time_since_reset_seconds{cache_container="keycloak",cache="work"} 87.0 # HELP wildfly_infinispan_time_since_start_seconds Time (in secs) since cache was started. # TYPE wildfly_infinispan_time_since_start_seconds gauge wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="actionTokens"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="authenticationSessions"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="clientSessions"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="ejb",cache="http-remoting-connector"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="loginFailures"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="offlineClientSessions"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="offlineSessions"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="sessions"} 87.0 wildfly_infinispan_time_since_start_seconds{cache_container="keycloak",cache="work"} 87.0 # HELP wildfly_infinispan_writes_total The number of cache attribute put operations. # TYPE wildfly_infinispan_writes_total counter wildfly_infinispan_writes_total{cache_container="keycloak",cache="actionTokens"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="authenticationSessions"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="clientSessions"} 0.0 wildfly_infinispan_writes_total{cache_container="ejb",cache="http-remoting-connector"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="loginFailures"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="offlineClientSessions"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="offlineSessions"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="sessions"} 0.0 wildfly_infinispan_writes_total{cache_container="keycloak",cache="work"} 0.0 # HELP wildfly_io_busy_task_thread_count An estimate of busy threads in the task worker thread pool # TYPE wildfly_io_busy_task_thread_count gauge wildfly_io_busy_task_thread_count{worker="default"} 0.0 # HELP wildfly_io_connection_count Estimate of the current connection count # TYPE wildfly_io_connection_count gauge wildfly_io_connection_count{worker="default",server="/0.0.0.0:8009"} 0.0 wildfly_io_connection_count{worker="default",server="/0.0.0.0:8080"} 0.0 wildfly_io_connection_count{worker="default",server="/0.0.0.0:8443"} 0.0 # HELP wildfly_io_core_pool_size Minimum number of threads to keep in the underlying thread pool even if they are idle. Threads over this limit will be terminated over time specified by task-keepalive attribute. # TYPE wildfly_io_core_pool_size gauge wildfly_io_core_pool_size{worker="default"} 2.0 # HELP wildfly_io_io_thread_count I/O thread count # TYPE wildfly_io_io_thread_count gauge wildfly_io_io_thread_count{worker="default"} 24.0 # HELP wildfly_io_max_pool_size The maximum number of threads allowed in the worker task thread pool. Depending on the pool implementation, when this limit is reached tasks which cannot be queued may be rejected. This can be configured using the 'task-max-threads' attribute; see the description of that attribute for details on how this value is determined. # TYPE wildfly_io_max_pool_size gauge wildfly_io_max_pool_size{worker="default"} 192.0 # HELP wildfly_io_queue_size An estimate of the number of tasks in the worker queue. # TYPE wildfly_io_queue_size gauge wildfly_io_queue_size{worker="default"} 0.0 # HELP wildfly_jca_current_thread_count The current number of threads in the pool. # TYPE wildfly_jca_current_thread_count gauge wildfly_jca_current_thread_count{workmanager="default",long_running_threads="default"} 0.0 wildfly_jca_current_thread_count{workmanager="default",short_running_threads="default"} 0.0 # HELP wildfly_jca_largest_thread_count The largest number of threads that have ever simultaneously been in the pool. # TYPE wildfly_jca_largest_thread_count gauge wildfly_jca_largest_thread_count{workmanager="default",long_running_threads="default"} 0.0 wildfly_jca_largest_thread_count{workmanager="default",short_running_threads="default"} 0.0 # HELP wildfly_jca_local_dowork_accepted Number of doWork calls accepted # TYPE wildfly_jca_local_dowork_accepted gauge wildfly_jca_local_dowork_accepted{workmanager="default"} 0.0 # HELP wildfly_jca_local_dowork_rejected Number of doWork calls rejected # TYPE wildfly_jca_local_dowork_rejected gauge wildfly_jca_local_dowork_rejected{workmanager="default"} 0.0 # HELP wildfly_jca_local_schedulework_accepted Number of scheduleWork calls accepted # TYPE wildfly_jca_local_schedulework_accepted gauge wildfly_jca_local_schedulework_accepted{workmanager="default"} 0.0 # HELP wildfly_jca_local_schedulework_rejected Number of scheduleWork calls rejected # TYPE wildfly_jca_local_schedulework_rejected gauge wildfly_jca_local_schedulework_rejected{workmanager="default"} 0.0 # HELP wildfly_jca_local_startwork_accepted Number of startWork calls accepted # TYPE wildfly_jca_local_startwork_accepted gauge wildfly_jca_local_startwork_accepted{workmanager="default"} 0.0 # HELP wildfly_jca_local_startwork_rejected Number of startWork calls rejected # TYPE wildfly_jca_local_startwork_rejected gauge wildfly_jca_local_startwork_rejected{workmanager="default"} 0.0 # HELP wildfly_jca_local_work_active Number of current active works # TYPE wildfly_jca_local_work_active gauge wildfly_jca_local_work_active{workmanager="default"} 0.0 # HELP wildfly_jca_local_work_failed Number of works failed # TYPE wildfly_jca_local_work_failed gauge wildfly_jca_local_work_failed{workmanager="default"} 0.0 # HELP wildfly_jca_local_work_successful Number of works completed successfully # TYPE wildfly_jca_local_work_successful gauge wildfly_jca_local_work_successful{workmanager="default"} 0.0 # HELP wildfly_jca_queue_size The queue size. # TYPE wildfly_jca_queue_size gauge wildfly_jca_queue_size{workmanager="default",long_running_threads="default"} 0.0 wildfly_jca_queue_size{workmanager="default",short_running_threads="default"} 0.0 # HELP wildfly_jca_rejected_count The number of tasks that have been passed to the handoff-executor (if one is specified) or discarded. # TYPE wildfly_jca_rejected_count gauge wildfly_jca_rejected_count{workmanager="default",long_running_threads="default"} 0.0 wildfly_jca_rejected_count{workmanager="default",short_running_threads="default"} 0.0 # HELP wildfly_jgroups_ack_threshold Send an ack immediately when a batch of ack_threshold (or more) messages is received. Otherwise send delayed acks. If 1, ack single messages (similar to UNICAST) # TYPE wildfly_jgroups_ack_threshold gauge wildfly_jgroups_ack_threshold{channel="ee",protocol="UNICAST3"} 100.0 # HELP wildfly_jgroups_age_out_cache_size # TYPE wildfly_jgroups_age_out_cache_size gauge wildfly_jgroups_age_out_cache_size{channel="ee",protocol="UNICAST3"} 0.0 # HELP wildfly_jgroups_all_clients_retry_timeout Time (in ms) to wait for another discovery round when all discovery responses were clients. A timeout of 0 means don't wait at all. # TYPE wildfly_jgroups_all_clients_retry_timeout gauge wildfly_jgroups_all_clients_retry_timeout{channel="ee",protocol="pbcast.GMS"} 100.0 # HELP wildfly_jgroups_average_time_blocked Average time blocked (in ms) in flow control when trying to send a message # TYPE wildfly_jgroups_average_time_blocked gauge wildfly_jgroups_average_time_blocked{channel="ee",protocol="MFC"} 0.0 wildfly_jgroups_average_time_blocked{channel="ee",protocol="UFC"} 0.0 # HELP wildfly_jgroups_become_server_queue_size Size of the queue to hold messages received after creating the channel, but before being connected (is_server=false). After becoming the server, the messages in the queue are fed into up() and the queue is cleared. The motivation is to avoid retransmissions (see https://issues.jboss.org/browse/JGRP-1509 for details). 0 disables the queue. # TYPE wildfly_jgroups_become_server_queue_size gauge wildfly_jgroups_become_server_queue_size{channel="ee",protocol="pbcast.NAKACK2"} 50.0 # HELP wildfly_jgroups_become_server_queue_size_actual Actual size of the become_server_queue # TYPE wildfly_jgroups_become_server_queue_size_actual gauge wildfly_jgroups_become_server_queue_size_actual{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_bind_port The port to which the transport binds. Default of 0 binds to any (ephemeral) port. See also port_range # TYPE wildfly_jgroups_bind_port gauge wildfly_jgroups_bind_port{channel="ee",protocol="UDP"} 55200.0 # HELP wildfly_jgroups_bundler_buffer_size # TYPE wildfly_jgroups_bundler_buffer_size gauge wildfly_jgroups_bundler_buffer_size{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_bundler_capacity The max number of elements in a bundler if the bundler supports size limitations # TYPE wildfly_jgroups_bundler_capacity gauge wildfly_jgroups_bundler_capacity{channel="ee",protocol="UDP"} 16384.0 # HELP wildfly_jgroups_bundler_num_spins Number of spins before a real lock is acquired # TYPE wildfly_jgroups_bundler_num_spins gauge wildfly_jgroups_bundler_num_spins{channel="ee",protocol="UDP"} 5.0 # HELP wildfly_jgroups_cache_max_age Max age (in ms) an element marked as removed has to have until it is removed # TYPE wildfly_jgroups_cache_max_age gauge wildfly_jgroups_cache_max_age{channel="ee",protocol="FD_SOCK"} 10000.0 # HELP wildfly_jgroups_cache_max_elements Max number of elements in the cache until deleted elements are removed # TYPE wildfly_jgroups_cache_max_elements gauge wildfly_jgroups_cache_max_elements{channel="ee",protocol="FD_SOCK"} 200.0 # HELP wildfly_jgroups_check_interval Interval (in ms) after which we check for view inconsistencies # TYPE wildfly_jgroups_check_interval gauge wildfly_jgroups_check_interval{channel="ee",protocol="MERGE3"} 48000.0 # HELP wildfly_jgroups_client_bind_port Start port for client socket. Default value of 0 picks a random port # TYPE wildfly_jgroups_client_bind_port gauge wildfly_jgroups_client_bind_port{channel="ee",protocol="FD_SOCK"} 0.0 # HELP wildfly_jgroups_client_bind_port_actual The actual client_bind_port # TYPE wildfly_jgroups_client_bind_port_actual gauge wildfly_jgroups_client_bind_port_actual{channel="ee",protocol="FD_SOCK"} 55767.0 # HELP wildfly_jgroups_conn_close_timeout Time (in ms) until a connection marked to be closed will get removed. 0 disables this # TYPE wildfly_jgroups_conn_close_timeout gauge wildfly_jgroups_conn_close_timeout{channel="ee",protocol="UNICAST3"} 240000.0 # HELP wildfly_jgroups_conn_expiry_timeout Time (in milliseconds) after which an idle incoming or outgoing connection is closed. The connection will get re-established when used again. 0 disables connection reaping. Note that this creates lingering connection entries, which increases memory over time. # TYPE wildfly_jgroups_conn_expiry_timeout gauge wildfly_jgroups_conn_expiry_timeout{channel="ee",protocol="UNICAST3"} 120000.0 # HELP wildfly_jgroups_current_seqno # TYPE wildfly_jgroups_current_seqno gauge wildfly_jgroups_current_seqno{channel="ee",protocol="pbcast.NAKACK2"} 100.0 # HELP wildfly_jgroups_desired_avg_gossip Average time to send a STABLE message # TYPE wildfly_jgroups_desired_avg_gossip gauge wildfly_jgroups_desired_avg_gossip{channel="ee",protocol="pbcast.STABLE"} 5000.0 # HELP wildfly_jgroups_diagnostics_port Port for diagnostic probing. Default is 7500 # TYPE wildfly_jgroups_diagnostics_port gauge wildfly_jgroups_diagnostics_port{channel="ee",protocol="UDP"} 7500.0 # HELP wildfly_jgroups_diagnostics_port_range The number of ports to be probed for an available port (TCP) # TYPE wildfly_jgroups_diagnostics_port_range gauge wildfly_jgroups_diagnostics_port_range{channel="ee",protocol="UDP"} 50.0 # HELP wildfly_jgroups_diagnostics_ttl TTL of the diagnostics multicast socket # TYPE wildfly_jgroups_diagnostics_ttl gauge wildfly_jgroups_diagnostics_ttl{channel="ee",protocol="UDP"} 8.0 # HELP wildfly_jgroups_different_cluster_messages Number of messages from members in a different cluster # TYPE wildfly_jgroups_different_cluster_messages gauge wildfly_jgroups_different_cluster_messages{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_different_version_messages Number of messages from members with a different JGroups version # TYPE wildfly_jgroups_different_version_messages gauge wildfly_jgroups_different_version_messages{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_discovery_rsp_expiry_time Expiry time of discovery responses in ms # TYPE wildfly_jgroups_discovery_rsp_expiry_time gauge wildfly_jgroups_discovery_rsp_expiry_time{channel="ee",protocol="PING"} 60000.0 # HELP wildfly_jgroups_dropped_messages Number of messages dropped when sending because of insufficient buffer space # TYPE wildfly_jgroups_dropped_messages gauge wildfly_jgroups_dropped_messages{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_external_port Used to map the internal port (bind_port) to an external port. Only used if > 0 # TYPE wildfly_jgroups_external_port gauge wildfly_jgroups_external_port{channel="ee",protocol="FD_SOCK"} 0.0 wildfly_jgroups_external_port{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_frag_size The max number of bytes in a message. Larger messages will be fragmented # TYPE wildfly_jgroups_frag_size gauge wildfly_jgroups_frag_size{channel="ee",protocol="FRAG3"} 60000.0 # HELP wildfly_jgroups_get_cache_timeout Timeout for getting socket cache from coordinator # TYPE wildfly_jgroups_get_cache_timeout gauge wildfly_jgroups_get_cache_timeout{channel="ee",protocol="FD_SOCK"} 1000.0 # HELP wildfly_jgroups_id Give the protocol a different ID if needed so we can have multiple instances of it in the same stack # TYPE wildfly_jgroups_id gauge wildfly_jgroups_id{channel="ee",protocol="FD_ALL"} 20.0 wildfly_jgroups_id{channel="ee",protocol="FD_SOCK"} 2.0 wildfly_jgroups_id{channel="ee",protocol="FRAG3"} 62.0 wildfly_jgroups_id{channel="ee",protocol="MERGE3"} 38.0 wildfly_jgroups_id{channel="ee",protocol="MFC"} 30.0 wildfly_jgroups_id{channel="ee",protocol="PING"} 5.0 wildfly_jgroups_id{channel="ee",protocol="UDP"} 57.0 wildfly_jgroups_id{channel="ee",protocol="UFC"} 31.0 wildfly_jgroups_id{channel="ee",protocol="UNICAST3"} 48.0 wildfly_jgroups_id{channel="ee",protocol="VERIFY_SUSPECT"} 11.0 wildfly_jgroups_id{channel="ee",protocol="pbcast.GMS"} 12.0 wildfly_jgroups_id{channel="ee",protocol="pbcast.NAKACK2"} 41.0 wildfly_jgroups_id{channel="ee",protocol="pbcast.STABLE"} 13.0 # HELP wildfly_jgroups_internal_thread_pool_size Current number of threads in the internal thread pool # TYPE wildfly_jgroups_internal_thread_pool_size gauge wildfly_jgroups_internal_thread_pool_size{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_internal_thread_pool_size_largest Largest number of threads in the internal thread pool # TYPE wildfly_jgroups_internal_thread_pool_size_largest gauge wildfly_jgroups_internal_thread_pool_size_largest{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_interval Interval at which a HEARTBEAT is sent to the cluster # TYPE wildfly_jgroups_interval gauge wildfly_jgroups_interval{channel="ee",protocol="FD_ALL"} 15000.0 # HELP wildfly_jgroups_ip_ttl The time-to-live (TTL) for multicast datagram packets. Default is 8 # TYPE wildfly_jgroups_ip_ttl gauge wildfly_jgroups_ip_ttl{channel="ee",protocol="UDP"} 2.0 # HELP wildfly_jgroups_join_timeout Join timeout # TYPE wildfly_jgroups_join_timeout gauge wildfly_jgroups_join_timeout{channel="ee",protocol="pbcast.GMS"} 3000.0 # HELP wildfly_jgroups_leave_timeout Max time (in ms) to wait for a LEAVE response after a LEAVE req has been sent to the coord # TYPE wildfly_jgroups_leave_timeout gauge wildfly_jgroups_leave_timeout{channel="ee",protocol="pbcast.GMS"} 2000.0 # HELP wildfly_jgroups_logical_addr_cache_expiration Time (in ms) after which entries in the logical address cache marked as removable can be removed. 0 never removes any entries (not recommended) # TYPE wildfly_jgroups_logical_addr_cache_expiration gauge wildfly_jgroups_logical_addr_cache_expiration{channel="ee",protocol="UDP"} 360000.0 # HELP wildfly_jgroups_logical_addr_cache_max_size Max number of elements in the logical address cache before eviction starts # TYPE wildfly_jgroups_logical_addr_cache_max_size gauge wildfly_jgroups_logical_addr_cache_max_size{channel="ee",protocol="UDP"} 2000.0 # HELP wildfly_jgroups_logical_addr_cache_reaper_interval Interval (in ms) at which the reaper task scans logical_addr_cache and removes entries marked as removable. 0 disables reaping. # TYPE wildfly_jgroups_logical_addr_cache_reaper_interval gauge wildfly_jgroups_logical_addr_cache_reaper_interval{channel="ee",protocol="UDP"} 60000.0 # HELP wildfly_jgroups_max_block_time Max time (in ms) to block # TYPE wildfly_jgroups_max_block_time gauge wildfly_jgroups_max_block_time{channel="ee",protocol="MFC"} 500.0 wildfly_jgroups_max_block_time{channel="ee",protocol="UFC"} 500.0 # HELP wildfly_jgroups_max_bundle_size Maximum number of bytes for messages to be queued until they are sent # TYPE wildfly_jgroups_max_bundle_size gauge wildfly_jgroups_max_bundle_size{channel="ee",protocol="UDP"} 64000.0 # HELP wildfly_jgroups_max_bundling_time Max view bundling timeout if view bundling is turned on # TYPE wildfly_jgroups_max_bundling_time gauge wildfly_jgroups_max_bundling_time{channel="ee",protocol="pbcast.GMS"} 50.0 # HELP wildfly_jgroups_max_bytes Maximum number of bytes received in all messages before sending a STABLE message is triggered # TYPE wildfly_jgroups_max_bytes gauge wildfly_jgroups_max_bytes{channel="ee",protocol="pbcast.STABLE"} 1000000.0 # HELP wildfly_jgroups_max_credits Max number of bytes to send per receiver until an ack must be received to proceed # TYPE wildfly_jgroups_max_credits gauge wildfly_jgroups_max_credits{channel="ee",protocol="MFC"} 2000000.0 wildfly_jgroups_max_credits{channel="ee",protocol="UFC"} 2000000.0 # HELP wildfly_jgroups_max_interval Interval (in milliseconds) when the next info message will be sent. A random value is picked from range [1..max_interval] # TYPE wildfly_jgroups_max_interval gauge wildfly_jgroups_max_interval{channel="ee",protocol="MERGE3"} 30000.0 # HELP wildfly_jgroups_max_join_attempts Number of join attempts before we give up and become a singleton. 0 means 'never give up'. # TYPE wildfly_jgroups_max_join_attempts gauge wildfly_jgroups_max_join_attempts{channel="ee",protocol="pbcast.GMS"} 10.0 # HELP wildfly_jgroups_max_leave_attempts Number of times a LEAVE request is sent to the coordinator (without receiving a LEAVE response, before giving up and leaving anyway (failure detection will eventually exclude the left member). A value of 0 means wait forever # TYPE wildfly_jgroups_max_leave_attempts gauge wildfly_jgroups_max_leave_attempts{channel="ee",protocol="pbcast.GMS"} 10.0 # HELP wildfly_jgroups_max_members_in_discovery_request Max size of the member list shipped with a discovery request. If we have more, the mbrs field in the discovery request header is nulled and members return the entire membership, not individual members # TYPE wildfly_jgroups_max_members_in_discovery_request gauge wildfly_jgroups_max_members_in_discovery_request{channel="ee",protocol="PING"} 500.0 # HELP wildfly_jgroups_max_participants_in_merge The max number of merge participants to be involved in a merge. 0 sets this to unlimited. # TYPE wildfly_jgroups_max_participants_in_merge gauge wildfly_jgroups_max_participants_in_merge{channel="ee",protocol="MERGE3"} 100.0 # HELP wildfly_jgroups_max_rank_to_reply The max rank of this member to respond to discovery requests, e.g. if max_rank_to_reply=2 in {A,B,C,D,E}, only A (rank 1) and B (rank 2) will reply. A value <= 0 means everybody will reply. This attribute is ignored if TP.use_ip_addrs is false. # TYPE wildfly_jgroups_max_rank_to_reply gauge wildfly_jgroups_max_rank_to_reply{channel="ee",protocol="PING"} 0.0 # HELP wildfly_jgroups_max_rebroadcast_timeout Timeout to rebroadcast messages. Default is 2000 msec # TYPE wildfly_jgroups_max_rebroadcast_timeout gauge wildfly_jgroups_max_rebroadcast_timeout{channel="ee",protocol="pbcast.NAKACK2"} 2000.0 # HELP wildfly_jgroups_max_xmit_req_size Max number of messages to ask for in a retransmit request. 0 disables this and uses the max bundle size in the transport # TYPE wildfly_jgroups_max_xmit_req_size gauge wildfly_jgroups_max_xmit_req_size{channel="ee",protocol="UNICAST3"} 511600.0 wildfly_jgroups_max_xmit_req_size{channel="ee",protocol="pbcast.NAKACK2"} 511600.0 # HELP wildfly_jgroups_mcast_port The multicast port used for sending and receiving packets. Default is 7600 # TYPE wildfly_jgroups_mcast_port gauge wildfly_jgroups_mcast_port{channel="ee",protocol="UDP"} 45688.0 # HELP wildfly_jgroups_mcast_receiver_threads Number of multicast receiver threads, all reading from the same MulticastSocket. If de-serialization is slow, increasing the number of receiver threads might yield better performance. # TYPE wildfly_jgroups_mcast_receiver_threads gauge wildfly_jgroups_mcast_receiver_threads{channel="ee",protocol="UDP"} 1.0 # HELP wildfly_jgroups_mcast_recv_buf_size Receive buffer size of the multicast datagram socket # TYPE wildfly_jgroups_mcast_recv_buf_size gauge wildfly_jgroups_mcast_recv_buf_size{channel="ee",protocol="UDP"} 2.5E7 # HELP wildfly_jgroups_mcast_send_buf_size Send buffer size of the multicast datagram socket # TYPE wildfly_jgroups_mcast_send_buf_size gauge wildfly_jgroups_mcast_send_buf_size{channel="ee",protocol="UDP"} 1000000.0 # HELP wildfly_jgroups_merge_timeout Timeout (in ms) to complete merge # TYPE wildfly_jgroups_merge_timeout gauge wildfly_jgroups_merge_timeout{channel="ee",protocol="pbcast.GMS"} 5000.0 # HELP wildfly_jgroups_message_processing_policy_max_buffer_size Max number of messages buffered for consumption of the delivery thread in MaxOneThreadPerSender. 0 creates an unbounded buffer # TYPE wildfly_jgroups_message_processing_policy_max_buffer_size gauge wildfly_jgroups_message_processing_policy_max_buffer_size{channel="ee",protocol="UDP"} 5000.0 # HELP wildfly_jgroups_min_credits Computed as max_credits x min_theshold unless explicitly set # TYPE wildfly_jgroups_min_credits gauge wildfly_jgroups_min_credits{channel="ee",protocol="MFC"} 800000.0 wildfly_jgroups_min_credits{channel="ee",protocol="UFC"} 800000.0 # HELP wildfly_jgroups_min_interval Minimum time in ms before sending an info message # TYPE wildfly_jgroups_min_interval gauge wildfly_jgroups_min_interval{channel="ee",protocol="MERGE3"} 10000.0 # HELP wildfly_jgroups_min_threshold The threshold (as a percentage of max_credits) at which a receiver sends more credits to a sender. Example: if max_credits is 1'000'000, and min_threshold 0.25, then we send ca. 250'000 credits to P once we've got only 250'000 credits left for P (we've received 750'000 bytes from P) # TYPE wildfly_jgroups_min_threshold gauge wildfly_jgroups_min_threshold{channel="ee",protocol="MFC"} 0.4 wildfly_jgroups_min_threshold{channel="ee",protocol="UFC"} 0.4 # HELP wildfly_jgroups_non_member_messages Number of messages from non-members # TYPE wildfly_jgroups_non_member_messages gauge wildfly_jgroups_non_member_messages{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_num_acks_received # TYPE wildfly_jgroups_num_acks_received gauge wildfly_jgroups_num_acks_received{channel="ee",protocol="UNICAST3"} 64.0 # HELP wildfly_jgroups_num_acks_sent # TYPE wildfly_jgroups_num_acks_sent gauge wildfly_jgroups_num_acks_sent{channel="ee",protocol="UNICAST3"} 61.0 # HELP wildfly_jgroups_num_bytes_received Bytes accumulated so far # TYPE wildfly_jgroups_num_bytes_received gauge wildfly_jgroups_num_bytes_received{channel="ee",protocol="pbcast.STABLE"} 0.0 # HELP wildfly_jgroups_num_connections Returns the total number of outgoing (send) and incoming (receive) connections # TYPE wildfly_jgroups_num_connections gauge wildfly_jgroups_num_connections{channel="ee",protocol="UNICAST3"} 2.0 # HELP wildfly_jgroups_num_discovery_requests Total number of discovery requests sent # TYPE wildfly_jgroups_num_discovery_requests gauge wildfly_jgroups_num_discovery_requests{channel="ee",protocol="PING"} 1.0 # HELP wildfly_jgroups_num_discovery_runs The number of times a discovery process is executed when finding initial members (https://issues.jboss.org/browse/JGRP-2317) # TYPE wildfly_jgroups_num_discovery_runs gauge wildfly_jgroups_num_discovery_runs{channel="ee",protocol="PING"} 1.0 # HELP wildfly_jgroups_num_heartbeats_received Number of heartbeats received # TYPE wildfly_jgroups_num_heartbeats_received gauge wildfly_jgroups_num_heartbeats_received{channel="ee",protocol="FD_ALL"} 3.0 # HELP wildfly_jgroups_num_heartbeats_sent Number of heartbeats sent # TYPE wildfly_jgroups_num_heartbeats_sent gauge wildfly_jgroups_num_heartbeats_sent{channel="ee",protocol="FD_ALL"} 0.0 # HELP wildfly_jgroups_num_members # TYPE wildfly_jgroups_num_members gauge wildfly_jgroups_num_members{channel="ee",protocol="pbcast.GMS"} 2.0 # HELP wildfly_jgroups_num_merge_events Number of times a MERGE event was sent up the stack # TYPE wildfly_jgroups_num_merge_events gauge wildfly_jgroups_num_merge_events{channel="ee",protocol="MERGE3"} 0.0 # HELP wildfly_jgroups_num_messages_received Number of messages received # TYPE wildfly_jgroups_num_messages_received gauge wildfly_jgroups_num_messages_received{channel="ee",protocol="UNICAST3"} 320.0 wildfly_jgroups_num_messages_received{channel="ee",protocol="pbcast.NAKACK2"} 43.0 # HELP wildfly_jgroups_num_messages_sent Number of messages sent # TYPE wildfly_jgroups_num_messages_sent gauge wildfly_jgroups_num_messages_sent{channel="ee",protocol="UNICAST3"} 292.0 wildfly_jgroups_num_messages_sent{channel="ee",protocol="pbcast.NAKACK2"} 98.0 # HELP wildfly_jgroups_num_msgs Number of verify heartbeats sent to a suspected member # TYPE wildfly_jgroups_num_msgs gauge wildfly_jgroups_num_msgs{channel="ee",protocol="VERIFY_SUSPECT"} 1.0 # HELP wildfly_jgroups_num_prev_mbrs Max number of old members to keep in history. Default is 50 # TYPE wildfly_jgroups_num_prev_mbrs gauge wildfly_jgroups_num_prev_mbrs{channel="ee",protocol="pbcast.GMS"} 50.0 # HELP wildfly_jgroups_num_prev_views Number of views to store in history # TYPE wildfly_jgroups_num_prev_views gauge wildfly_jgroups_num_prev_views{channel="ee",protocol="pbcast.GMS"} 10.0 # HELP wildfly_jgroups_num_receive_connections Returns the number of incoming (receive) connections # TYPE wildfly_jgroups_num_receive_connections gauge wildfly_jgroups_num_receive_connections{channel="ee",protocol="UNICAST3"} 1.0 # HELP wildfly_jgroups_num_send_connections Returns the number of outgoing (send) connections # TYPE wildfly_jgroups_num_send_connections gauge wildfly_jgroups_num_send_connections{channel="ee",protocol="UNICAST3"} 1.0 # HELP wildfly_jgroups_num_suspect_events Number of suspected events received # TYPE wildfly_jgroups_num_suspect_events gauge wildfly_jgroups_num_suspect_events{channel="ee",protocol="FD_ALL"} 0.0 # HELP wildfly_jgroups_num_suspect_events_generated Number of suspect event generated # TYPE wildfly_jgroups_num_suspect_events_generated gauge wildfly_jgroups_num_suspect_events_generated{channel="ee",protocol="FD_SOCK"} 0.0 # HELP wildfly_jgroups_num_suspected_members The number of currently suspected members # TYPE wildfly_jgroups_num_suspected_members gauge wildfly_jgroups_num_suspected_members{channel="ee",protocol="FD_SOCK"} 0.0 # HELP wildfly_jgroups_num_tasks_in_timer The current number of timer tasks. # TYPE wildfly_jgroups_num_tasks_in_timer gauge wildfly_jgroups_num_tasks_in_timer{channel="ee"} 0.0 # HELP wildfly_jgroups_num_threads Returns the number of live threads in the JVM # TYPE wildfly_jgroups_num_threads gauge wildfly_jgroups_num_threads{channel="ee",protocol="UDP"} 157.0 # HELP wildfly_jgroups_num_timer_threads The number of timer threads. # TYPE wildfly_jgroups_num_timer_threads gauge wildfly_jgroups_num_timer_threads{channel="ee"} 0.0 # HELP wildfly_jgroups_num_tries Number of attempts coordinator is solicited for socket cache until we give up # TYPE wildfly_jgroups_num_tries gauge wildfly_jgroups_num_tries{channel="ee",protocol="FD_SOCK"} 3.0 # HELP wildfly_jgroups_num_unacked_messages # TYPE wildfly_jgroups_num_unacked_messages gauge wildfly_jgroups_num_unacked_messages{channel="ee",protocol="UNICAST3"} 0.0 # HELP wildfly_jgroups_num_xmits # TYPE wildfly_jgroups_num_xmits gauge wildfly_jgroups_num_xmits{channel="ee",protocol="UNICAST3"} 0.0 # HELP wildfly_jgroups_number_of_blockings Number of times flow control blocks sender # TYPE wildfly_jgroups_number_of_blockings gauge wildfly_jgroups_number_of_blockings{channel="ee",protocol="MFC"} 0.0 wildfly_jgroups_number_of_blockings{channel="ee",protocol="UFC"} 0.0 # HELP wildfly_jgroups_number_of_credit_requests_received Number of credit requests received # TYPE wildfly_jgroups_number_of_credit_requests_received gauge wildfly_jgroups_number_of_credit_requests_received{channel="ee",protocol="MFC"} 0.0 wildfly_jgroups_number_of_credit_requests_received{channel="ee",protocol="UFC"} 0.0 # HELP wildfly_jgroups_number_of_credit_requests_sent Number of credit requests sent # TYPE wildfly_jgroups_number_of_credit_requests_sent gauge wildfly_jgroups_number_of_credit_requests_sent{channel="ee",protocol="MFC"} 0.0 wildfly_jgroups_number_of_credit_requests_sent{channel="ee",protocol="UFC"} 0.0 # HELP wildfly_jgroups_number_of_credit_responses_received Number of credit responses received # TYPE wildfly_jgroups_number_of_credit_responses_received gauge wildfly_jgroups_number_of_credit_responses_received{channel="ee",protocol="MFC"} 0.0 wildfly_jgroups_number_of_credit_responses_received{channel="ee",protocol="UFC"} 0.0 # HELP wildfly_jgroups_number_of_credit_responses_sent Number of credit responses sent # TYPE wildfly_jgroups_number_of_credit_responses_sent gauge wildfly_jgroups_number_of_credit_responses_sent{channel="ee",protocol="MFC"} 0.0 wildfly_jgroups_number_of_credit_responses_sent{channel="ee",protocol="UFC"} 0.0 # HELP wildfly_jgroups_number_of_thread_dumps Number of thread dumps # TYPE wildfly_jgroups_number_of_thread_dumps gauge wildfly_jgroups_number_of_thread_dumps{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_number_of_views # TYPE wildfly_jgroups_number_of_views gauge wildfly_jgroups_number_of_views{channel="ee",protocol="pbcast.GMS"} 0.0 # HELP wildfly_jgroups_port_range The range of valid ports: [bind_port .. bind_port+port_range ]. 0 only binds to bind_port and fails if taken # TYPE wildfly_jgroups_port_range gauge wildfly_jgroups_port_range{channel="ee",protocol="FD_SOCK"} 50.0 wildfly_jgroups_port_range{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_received_bytes The number of bytes received by this channel. # TYPE wildfly_jgroups_received_bytes gauge wildfly_jgroups_received_bytes{channel="ee"} 0.0 # HELP wildfly_jgroups_received_messages The number of messages received by this channel. # TYPE wildfly_jgroups_received_messages gauge wildfly_jgroups_received_messages{channel="ee"} 0.0 # HELP wildfly_jgroups_resend_last_seqno_max_times Max number of times the last seqno is resent before acquiescing if last seqno isn't incremented # TYPE wildfly_jgroups_resend_last_seqno_max_times gauge wildfly_jgroups_resend_last_seqno_max_times{channel="ee",protocol="pbcast.NAKACK2"} 1.0 # HELP wildfly_jgroups_sent_bytes The number of bytes sent by this channel. # TYPE wildfly_jgroups_sent_bytes gauge wildfly_jgroups_sent_bytes{channel="ee"} 0.0 # HELP wildfly_jgroups_sent_messages The number of messages sent by this channel. # TYPE wildfly_jgroups_sent_messages gauge wildfly_jgroups_sent_messages{channel="ee"} 0.0 # HELP wildfly_jgroups_size_of_all_messages Returns the number of bytes of all messages in all retransmit buffers. To compute the size, Message.getLength() is used # TYPE wildfly_jgroups_size_of_all_messages gauge wildfly_jgroups_size_of_all_messages{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_size_of_all_messages_incl_headers Returns the number of bytes of all messages in all retransmit buffers. To compute the size, Message.size() is used # TYPE wildfly_jgroups_size_of_all_messages_incl_headers gauge wildfly_jgroups_size_of_all_messages_incl_headers{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_sock_conn_timeout Max time in millis to wait for ping Socket.connect() to return # TYPE wildfly_jgroups_sock_conn_timeout gauge wildfly_jgroups_sock_conn_timeout{channel="ee",protocol="FD_SOCK"} 1000.0 # HELP wildfly_jgroups_stability_delay Delay before stability message is sent # TYPE wildfly_jgroups_stability_delay gauge wildfly_jgroups_stability_delay{channel="ee",protocol="pbcast.STABLE"} 0.0 # HELP wildfly_jgroups_stability_received # TYPE wildfly_jgroups_stability_received gauge wildfly_jgroups_stability_received{channel="ee",protocol="pbcast.STABLE"} 12.0 # HELP wildfly_jgroups_stability_sent # TYPE wildfly_jgroups_stability_sent gauge wildfly_jgroups_stability_sent{channel="ee",protocol="pbcast.STABLE"} 12.0 # HELP wildfly_jgroups_stable_received # TYPE wildfly_jgroups_stable_received gauge wildfly_jgroups_stable_received{channel="ee",protocol="pbcast.STABLE"} 25.0 # HELP wildfly_jgroups_stable_sent # TYPE wildfly_jgroups_stable_sent gauge wildfly_jgroups_stable_sent{channel="ee",protocol="pbcast.STABLE"} 0.0 # HELP wildfly_jgroups_stagger_timeout If greater than 0, we'll wait a random number of milliseconds in range [0..stagger_timeout] before sending a discovery response. This prevents traffic spikes in large clusters when everyone sends their discovery response at the same time # TYPE wildfly_jgroups_stagger_timeout gauge wildfly_jgroups_stagger_timeout{channel="ee",protocol="PING"} 0.0 # HELP wildfly_jgroups_start_port Start port for server socket. Default value of 0 picks a random port # TYPE wildfly_jgroups_start_port gauge wildfly_jgroups_start_port{channel="ee",protocol="FD_SOCK"} 54200.0 # HELP wildfly_jgroups_suppress_time_different_cluster_warnings Time during which identical warnings about messages from a member from a different cluster will be suppressed. 0 disables this (every warning will be logged). Setting the log level to ERROR also disables this. # TYPE wildfly_jgroups_suppress_time_different_cluster_warnings gauge wildfly_jgroups_suppress_time_different_cluster_warnings{channel="ee",protocol="UDP"} 60000.0 # HELP wildfly_jgroups_suppress_time_different_version_warnings Time during which identical warnings about messages from a member with a different version will be suppressed. 0 disables this (every warning will be logged). Setting the log level to ERROR also disables this. # TYPE wildfly_jgroups_suppress_time_different_version_warnings gauge wildfly_jgroups_suppress_time_different_version_warnings{channel="ee",protocol="UDP"} 60000.0 # HELP wildfly_jgroups_suppress_time_non_member_warnings Time during which identical warnings about messages from a non member will be suppressed. 0 disables this (every warning will be logged). Setting the log level to ERROR also disables this. # TYPE wildfly_jgroups_suppress_time_non_member_warnings gauge wildfly_jgroups_suppress_time_non_member_warnings{channel="ee",protocol="pbcast.NAKACK2"} 60000.0 # HELP wildfly_jgroups_suppress_time_out_of_buffer_space Suppresses warnings on Mac OS (for now) about not enough buffer space when sending a datagram packet # TYPE wildfly_jgroups_suppress_time_out_of_buffer_space gauge wildfly_jgroups_suppress_time_out_of_buffer_space{channel="ee",protocol="UDP"} 60000.0 # HELP wildfly_jgroups_suspect_msg_interval Interval for broadcasting suspect messages # TYPE wildfly_jgroups_suspect_msg_interval gauge wildfly_jgroups_suspect_msg_interval{channel="ee",protocol="FD_SOCK"} 5000.0 # HELP wildfly_jgroups_sync_min_interval Min time (in ms) to elapse for successive SEND_FIRST_SEQNO messages to be sent to the same sender # TYPE wildfly_jgroups_sync_min_interval gauge wildfly_jgroups_sync_min_interval{channel="ee",protocol="UNICAST3"} 2000.0 # HELP wildfly_jgroups_thread_dumps_threshold The number of times a thread pool needs to be full before a thread dump is logged # TYPE wildfly_jgroups_thread_dumps_threshold gauge wildfly_jgroups_thread_dumps_threshold{channel="ee",protocol="UDP"} 1.0 # HELP wildfly_jgroups_thread_pool_keep_alive_time Timeout in milliseconds to remove idle threads from pool # TYPE wildfly_jgroups_thread_pool_keep_alive_time gauge wildfly_jgroups_thread_pool_keep_alive_time{channel="ee",protocol="UDP"} 30000.0 # HELP wildfly_jgroups_thread_pool_max_threads Maximum thread pool size for the thread pool # TYPE wildfly_jgroups_thread_pool_max_threads gauge wildfly_jgroups_thread_pool_max_threads{channel="ee",protocol="UDP"} 100.0 # HELP wildfly_jgroups_thread_pool_min_threads Minimum thread pool size for the thread pool # TYPE wildfly_jgroups_thread_pool_min_threads gauge wildfly_jgroups_thread_pool_min_threads{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_thread_pool_size Current number of threads in the thread pool # TYPE wildfly_jgroups_thread_pool_size gauge wildfly_jgroups_thread_pool_size{channel="ee",protocol="UDP"} 3.0 # HELP wildfly_jgroups_thread_pool_size_active Current number of active threads in the thread pool # TYPE wildfly_jgroups_thread_pool_size_active gauge wildfly_jgroups_thread_pool_size_active{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_thread_pool_size_largest Largest number of threads in the thread pool # TYPE wildfly_jgroups_thread_pool_size_largest gauge wildfly_jgroups_thread_pool_size_largest{channel="ee",protocol="UDP"} 8.0 # HELP wildfly_jgroups_time_service_interval Interval (in ms) at which the time service updates its timestamp. 0 disables the time service # TYPE wildfly_jgroups_time_service_interval gauge wildfly_jgroups_time_service_interval{channel="ee",protocol="UDP"} 500.0 # HELP wildfly_jgroups_timeout Timeout after which a node P is suspected if neither a heartbeat nor data were received from P # TYPE wildfly_jgroups_timeout gauge wildfly_jgroups_timeout{channel="ee",protocol="FD_ALL"} 60000.0 wildfly_jgroups_timeout{channel="ee",protocol="VERIFY_SUSPECT"} 1000.0 # HELP wildfly_jgroups_timeout_check_interval Interval at which the HEARTBEAT timeouts are checked # TYPE wildfly_jgroups_timeout_check_interval gauge wildfly_jgroups_timeout_check_interval{channel="ee",protocol="FD_ALL"} 5000.0 # HELP wildfly_jgroups_timer_tasks Number of timer tasks queued up for execution # TYPE wildfly_jgroups_timer_tasks gauge wildfly_jgroups_timer_tasks{channel="ee",protocol="UDP"} 9.0 # HELP wildfly_jgroups_timer_threads Number of threads currently in the pool # TYPE wildfly_jgroups_timer_threads gauge wildfly_jgroups_timer_threads{channel="ee",protocol="UDP"} 3.0 # HELP wildfly_jgroups_timestamper Next seqno issued by the timestamper # TYPE wildfly_jgroups_timestamper gauge wildfly_jgroups_timestamper{channel="ee",protocol="UNICAST3"} 61.0 # HELP wildfly_jgroups_tos Traffic class for sending unicast and multicast datagrams. Default is 0 # TYPE wildfly_jgroups_tos gauge wildfly_jgroups_tos{channel="ee",protocol="UDP"} 0.0 # HELP wildfly_jgroups_ucast_receiver_threads Number of unicast receiver threads, all reading from the same DatagramSocket. If de-serialization is slow, increasing the number of receiver threads might yield better performance. # TYPE wildfly_jgroups_ucast_receiver_threads gauge wildfly_jgroups_ucast_receiver_threads{channel="ee",protocol="UDP"} 1.0 # HELP wildfly_jgroups_ucast_recv_buf_size Receive buffer size of the unicast datagram socket # TYPE wildfly_jgroups_ucast_recv_buf_size gauge wildfly_jgroups_ucast_recv_buf_size{channel="ee",protocol="UDP"} 2.0E7 # HELP wildfly_jgroups_ucast_send_buf_size Send buffer size of the unicast datagram socket # TYPE wildfly_jgroups_ucast_send_buf_size gauge wildfly_jgroups_ucast_send_buf_size{channel="ee",protocol="UDP"} 1000000.0 # HELP wildfly_jgroups_view_ack_collection_timeout Time in ms to wait for all VIEW acks (0 == wait forever. Default is 2000 msec # TYPE wildfly_jgroups_view_ack_collection_timeout gauge wildfly_jgroups_view_ack_collection_timeout{channel="ee",protocol="pbcast.GMS"} 2000.0 # HELP wildfly_jgroups_view_handler_size # TYPE wildfly_jgroups_view_handler_size gauge wildfly_jgroups_view_handler_size{channel="ee",protocol="pbcast.GMS"} 0.0 # HELP wildfly_jgroups_views Number of cached ViewIds # TYPE wildfly_jgroups_views gauge wildfly_jgroups_views{channel="ee",protocol="MERGE3"} 1.0 # HELP wildfly_jgroups_who_has_cache_timeout Timeout (in ms) to determine how long to wait until a request to fetch the physical address for a given logical address will be sent again. Subsequent requests for the same physical address will therefore be spaced at least who_has_cache_timeout ms apart # TYPE wildfly_jgroups_who_has_cache_timeout gauge wildfly_jgroups_who_has_cache_timeout{channel="ee",protocol="UDP"} 2000.0 # HELP wildfly_jgroups_xmit_interval Interval (in milliseconds) at which missing messages (from all retransmit buffers) are retransmitted # TYPE wildfly_jgroups_xmit_interval gauge wildfly_jgroups_xmit_interval{channel="ee",protocol="UNICAST3"} 100.0 wildfly_jgroups_xmit_interval{channel="ee",protocol="pbcast.NAKACK2"} 100.0 # HELP wildfly_jgroups_xmit_table_capacity Capacity of the retransmit buffer. Computed as xmit_table_num_rows * xmit_table_msgs_per_row # TYPE wildfly_jgroups_xmit_table_capacity gauge wildfly_jgroups_xmit_table_capacity{channel="ee",protocol="pbcast.NAKACK2"} 51200.0 # HELP wildfly_jgroups_xmit_table_deliverable_messages Total number of deliverable messages in all receive windows # TYPE wildfly_jgroups_xmit_table_deliverable_messages gauge wildfly_jgroups_xmit_table_deliverable_messages{channel="ee",protocol="UNICAST3"} 0.0 # HELP wildfly_jgroups_xmit_table_max_compaction_time Number of milliseconds after which the matrix in the retransmission table is compacted (only for experts) # TYPE wildfly_jgroups_xmit_table_max_compaction_time gauge wildfly_jgroups_xmit_table_max_compaction_time{channel="ee",protocol="UNICAST3"} 600000.0 wildfly_jgroups_xmit_table_max_compaction_time{channel="ee",protocol="pbcast.NAKACK2"} 10000.0 # HELP wildfly_jgroups_xmit_table_missing_messages Total number of missing (= not received) messages in all retransmit buffers # TYPE wildfly_jgroups_xmit_table_missing_messages gauge wildfly_jgroups_xmit_table_missing_messages{channel="ee",protocol="UNICAST3"} 0.0 wildfly_jgroups_xmit_table_missing_messages{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_xmit_table_msgs_per_row Number of elements of a row of the matrix in the retransmission table; gets rounded to the next power of 2 (only for experts). The capacity of the matrix is xmit_table_num_rows * xmit_table_msgs_per_row # TYPE wildfly_jgroups_xmit_table_msgs_per_row gauge wildfly_jgroups_xmit_table_msgs_per_row{channel="ee",protocol="UNICAST3"} 1024.0 wildfly_jgroups_xmit_table_msgs_per_row{channel="ee",protocol="pbcast.NAKACK2"} 1024.0 # HELP wildfly_jgroups_xmit_table_num_compactions Number of retransmit table compactions # TYPE wildfly_jgroups_xmit_table_num_compactions gauge wildfly_jgroups_xmit_table_num_compactions{channel="ee",protocol="UNICAST3"} 0.0 wildfly_jgroups_xmit_table_num_compactions{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_xmit_table_num_current_rows Prints the number of rows currently allocated in the matrix. This value will not be lower than xmit_table_now_rows # TYPE wildfly_jgroups_xmit_table_num_current_rows gauge wildfly_jgroups_xmit_table_num_current_rows{channel="ee",protocol="pbcast.NAKACK2"} 50.0 # HELP wildfly_jgroups_xmit_table_num_moves Number of retransmit table moves # TYPE wildfly_jgroups_xmit_table_num_moves gauge wildfly_jgroups_xmit_table_num_moves{channel="ee",protocol="UNICAST3"} 0.0 wildfly_jgroups_xmit_table_num_moves{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_xmit_table_num_purges Number of retransmit table purges # TYPE wildfly_jgroups_xmit_table_num_purges gauge wildfly_jgroups_xmit_table_num_purges{channel="ee",protocol="UNICAST3"} 64.0 wildfly_jgroups_xmit_table_num_purges{channel="ee",protocol="pbcast.NAKACK2"} 5.0 # HELP wildfly_jgroups_xmit_table_num_resizes Number of retransmit table resizes # TYPE wildfly_jgroups_xmit_table_num_resizes gauge wildfly_jgroups_xmit_table_num_resizes{channel="ee",protocol="UNICAST3"} 0.0 wildfly_jgroups_xmit_table_num_resizes{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_jgroups_xmit_table_num_rows Number of rows of the matrix in the retransmission table (only for experts) # TYPE wildfly_jgroups_xmit_table_num_rows gauge wildfly_jgroups_xmit_table_num_rows{channel="ee",protocol="UNICAST3"} 50.0 wildfly_jgroups_xmit_table_num_rows{channel="ee",protocol="pbcast.NAKACK2"} 50.0 # HELP wildfly_jgroups_xmit_table_resize_factor Resize factor of the matrix in the retransmission table (only for experts) # TYPE wildfly_jgroups_xmit_table_resize_factor gauge wildfly_jgroups_xmit_table_resize_factor{channel="ee",protocol="UNICAST3"} 1.2 wildfly_jgroups_xmit_table_resize_factor{channel="ee",protocol="pbcast.NAKACK2"} 1.2 # HELP wildfly_jgroups_xmit_table_undelivered_messages Total number of undelivered messages in all receive windows # TYPE wildfly_jgroups_xmit_table_undelivered_messages gauge wildfly_jgroups_xmit_table_undelivered_messages{channel="ee",protocol="UNICAST3"} 0.0 # HELP wildfly_jgroups_xmit_table_undelivered_msgs Total number of undelivered messages in all retransmit buffers # TYPE wildfly_jgroups_xmit_table_undelivered_msgs gauge wildfly_jgroups_xmit_table_undelivered_msgs{channel="ee",protocol="pbcast.NAKACK2"} 0.0 # HELP wildfly_request_controller_active_requests The number of requests that are currently running in the server # TYPE wildfly_request_controller_active_requests gauge wildfly_request_controller_active_requests 0.0 # HELP wildfly_transactions_average_commit_time_seconds The average time of transaction commit, measured from the moment the client calls commit until the transaction manager determines that the commit attempt was successful. # TYPE wildfly_transactions_average_commit_time_seconds gauge wildfly_transactions_average_commit_time_seconds 4.5295000000000006E-5 # HELP wildfly_transactions_number_of_aborted_transactions_total The number of aborted (i.e. rolledback) transactions. # TYPE wildfly_transactions_number_of_aborted_transactions_total counter wildfly_transactions_number_of_aborted_transactions_total 0.0 # HELP wildfly_transactions_number_of_application_rollbacks_total The number of transactions that have been rolled back by application request. This includes those that timeout, since the timeout behavior is considered an attribute of the application configuration. # TYPE wildfly_transactions_number_of_application_rollbacks_total counter wildfly_transactions_number_of_application_rollbacks_total 0.0 # HELP wildfly_transactions_number_of_committed_transactions_total The number of committed transactions. # TYPE wildfly_transactions_number_of_committed_transactions_total counter wildfly_transactions_number_of_committed_transactions_total 309.0 # HELP wildfly_transactions_number_of_heuristics_total The number of transactions which have terminated with heuristic outcomes. # TYPE wildfly_transactions_number_of_heuristics_total counter wildfly_transactions_number_of_heuristics_total 0.0 # HELP wildfly_transactions_number_of_inflight_transactions The number of transactions that have begun but not yet terminated. # TYPE wildfly_transactions_number_of_inflight_transactions gauge wildfly_transactions_number_of_inflight_transactions 0.0 # HELP wildfly_transactions_number_of_nested_transactions_total The total number of nested (sub) transactions created. # TYPE wildfly_transactions_number_of_nested_transactions_total counter wildfly_transactions_number_of_nested_transactions_total 0.0 # HELP wildfly_transactions_number_of_resource_rollbacks_total The number of transactions that rolled back due to resource (participant) failure. # TYPE wildfly_transactions_number_of_resource_rollbacks_total counter wildfly_transactions_number_of_resource_rollbacks_total 0.0 # HELP wildfly_transactions_number_of_system_rollbacks_total The number of transactions that have been rolled back due to internal system errors. # TYPE wildfly_transactions_number_of_system_rollbacks_total counter wildfly_transactions_number_of_system_rollbacks_total 0.0 # HELP wildfly_transactions_number_of_timed_out_transactions_total The number of transactions that have rolled back due to timeout. # TYPE wildfly_transactions_number_of_timed_out_transactions_total counter wildfly_transactions_number_of_timed_out_transactions_total 0.0 # HELP wildfly_transactions_number_of_transactions_total The total number of transactions (top-level and nested) created # TYPE wildfly_transactions_number_of_transactions_total counter wildfly_transactions_number_of_transactions_total 309.0 # HELP wildfly_undertow_active_sessions Number of active sessions # TYPE wildfly_undertow_active_sessions gauge wildfly_undertow_active_sessions{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_bytes_received_total The number of bytes that have been received by this listener # TYPE wildfly_undertow_bytes_received_total counter wildfly_undertow_bytes_received_total_bytes{server="default-server",ajp_listener="ajp"} 0.0 wildfly_undertow_bytes_received_total_bytes{server="default-server",http_listener="default"} 0.0 wildfly_undertow_bytes_received_total_bytes{server="default-server",https_listener="https"} 319697.0 # HELP wildfly_undertow_bytes_sent_total The number of bytes that have been sent out on this listener # TYPE wildfly_undertow_bytes_sent_total counter wildfly_undertow_bytes_sent_total_bytes{server="default-server",ajp_listener="ajp"} 0.0 wildfly_undertow_bytes_sent_total_bytes{server="default-server",http_listener="default"} 0.0 wildfly_undertow_bytes_sent_total_bytes{server="default-server",https_listener="https"} 3658992.0 # HELP wildfly_undertow_error_count_total The number of 500 responses that have been sent by this listener # TYPE wildfly_undertow_error_count_total counter wildfly_undertow_error_count_total{server="default-server",ajp_listener="ajp"} 0.0 wildfly_undertow_error_count_total{server="default-server",http_listener="default"} 0.0 wildfly_undertow_error_count_total{server="default-server",https_listener="https"} 0.0 # HELP wildfly_undertow_expired_sessions_total Number of sessions that have expired # TYPE wildfly_undertow_expired_sessions_total counter wildfly_undertow_expired_sessions_total{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_highest_session_count The maximum number of sessions that have been active simultaneously # TYPE wildfly_undertow_highest_session_count gauge wildfly_undertow_highest_session_count{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_max_active_sessions The maximum allowed number of concurrent sessions that this session manager supports # TYPE wildfly_undertow_max_active_sessions gauge wildfly_undertow_max_active_sessions{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} -1.0 # HELP wildfly_undertow_max_processing_time_seconds The maximum processing time taken by a request on this listener # TYPE wildfly_undertow_max_processing_time_seconds gauge wildfly_undertow_max_processing_time_seconds{server="default-server",ajp_listener="ajp"} 0.0 wildfly_undertow_max_processing_time_seconds{server="default-server",http_listener="default"} 0.0 wildfly_undertow_max_processing_time_seconds{server="default-server",https_listener="https"} 0.0 # HELP wildfly_undertow_max_request_time_seconds Maximal time for processing request # TYPE wildfly_undertow_max_request_time_seconds gauge wildfly_undertow_max_request_time_seconds{deployment="keycloak-server.war",servlet="Keycloak REST Interface",subdeployment="keycloak-server.war"} 1.048 # HELP wildfly_undertow_min_request_time_seconds Minimal time for processing request # TYPE wildfly_undertow_min_request_time_seconds gauge wildfly_undertow_min_request_time_seconds{deployment="keycloak-server.war",servlet="Keycloak REST Interface",subdeployment="keycloak-server.war"} 0.001 # HELP wildfly_undertow_processing_time_total The total processing time of all requests handed by this listener # TYPE wildfly_undertow_processing_time_total counter wildfly_undertow_processing_time_total_seconds{server="default-server",ajp_listener="ajp"} 0.0 wildfly_undertow_processing_time_total_seconds{server="default-server",http_listener="default"} 0.0 wildfly_undertow_processing_time_total_seconds{server="default-server",https_listener="https"} 0.0 # HELP wildfly_undertow_rejected_sessions_total Number of rejected sessions # TYPE wildfly_undertow_rejected_sessions_total counter wildfly_undertow_rejected_sessions_total{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_request_count_total Number of all requests # TYPE wildfly_undertow_request_count_total counter wildfly_undertow_request_count_total{server="default-server",ajp_listener="ajp"} 0.0 wildfly_undertow_request_count_total{server="default-server",http_listener="default"} 0.0 wildfly_undertow_request_count_total{server="default-server",https_listener="https"} 276.0 wildfly_undertow_request_count_total{deployment="keycloak-server.war",servlet="Keycloak REST Interface",subdeployment="keycloak-server.war"} 276.0 # HELP wildfly_undertow_session_avg_alive_time_seconds Average time that expired sessions had been alive # TYPE wildfly_undertow_session_avg_alive_time_seconds gauge wildfly_undertow_session_avg_alive_time_seconds{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_session_max_alive_time_seconds The longest time that an expired session had been alive # TYPE wildfly_undertow_session_max_alive_time_seconds gauge wildfly_undertow_session_max_alive_time_seconds{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_sessions_created_total Total sessions created # TYPE wildfly_undertow_sessions_created_total counter wildfly_undertow_sessions_created_total{deployment="keycloak-server.war",subdeployment="keycloak-server.war"} 0.0 # HELP wildfly_undertow_total_request_time_total Total time spend in processing all requests # TYPE wildfly_undertow_total_request_time_total counter wildfly_undertow_total_request_time_total_seconds{deployment="keycloak-server.war",servlet="Keycloak REST Interface",subdeployment="keycloak-server.war"} 4.434 ================================================ FILE: keycloak/misc/snippets/overlay-keycloak-endpoint-undertow.txt ================================================ # Map Keycloak paths to custom endpoints to work around Keycloak bugs (the UUID regex matches a UUID V4 (random uuid pattern) /subsystem=undertow/configuration=filter/expression-filter=keycloakPathOverrideConsentEndpoint:add( \ expression="regex('/auth/admin/realms/acme-internal/users/([a-f\\d]{8}-[a-f\\d]{4}-4[a-f\\d]{3}-[89ab][a-f\\d]{3}-[a-f\\d]{12})/consents') -> rewrite('/auth/realms/acme-internal/custom-resources/users/$1/consents')" \ ) /subsystem=undertow/server=default-server/host=default-host/filter-ref=keycloakPathOverrideConsentEndpoint:add() ================================================ FILE: keycloak/patches/keycloak-model-infinispan-patch/pom.xml ================================================ 4.0.0 org.example keycloak-model-infinispan-patch 16.1.0.0-SNAPSHOT UTF-8 11 11 16.1.0 org.keycloak keycloak-model-infinispan ${version.keycloak} keycloak-model-infinispan-patch src/main/java **/*.properties org.apache.maven.plugins maven-shade-plugin 3.2.4 package shade org.keycloak:keycloak-model-infinispan ================================================ FILE: keycloak/patches/keycloak-model-infinispan-patch/readme.md ================================================ Keycloak Model Infinispan Patch ---- Patched version of the `keycloak-model-infinispan` library that allows to use dedicated cache store configuration with the embedded infinispan support. As of Keycloak version 15.0.2, it is not supported to use custom cache store configurations with Keycloak, as Keycloak skips writes to configured cache stores by default. See the usage of `org.keycloak.models.sessions.infinispan.CacheDecorators#skipCacheStore`. To work around this, we patch `org.keycloak.models.sessions.infinispan.CacheDecorators` to consider the new system property `keycloak.infinispan.ignoreSkipCacheStore` to control whether it is possible to propagate a cache write to a configured cache store. Setting `-Dkeycloak.infinispan.ignoreSkipCacheStore=true` allows to propagate cache writes to configured cache store to backends like jbdc-datasource, redis etc. An example configuration with this patch can be found in `deployments/local/cluster/haproxy-database-ispn`. ================================================ FILE: keycloak/patches/keycloak-model-infinispan-patch/src/main/java/org/keycloak/models/sessions/infinispan/CacheDecorators.java ================================================ package org.keycloak.models.sessions.infinispan; import org.infinispan.AdvancedCache; import org.infinispan.Cache; import org.infinispan.context.Flag; public class CacheDecorators { // Patch:Begin private static final boolean IGNORE_SKIP_CACHE_STORE = Boolean.getBoolean("keycloak.infinispan.ignoreSkipCacheStore"); // Patch:End public CacheDecorators() { } public static AdvancedCache localCache(Cache cache) { return cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL); } public static AdvancedCache skipCacheLoaders(Cache cache) { return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_LOAD, Flag.SKIP_CACHE_STORE); } public static AdvancedCache skipCacheStore(Cache cache) { // Patch:Begin if (IGNORE_SKIP_CACHE_STORE) { return cache.getAdvancedCache(); } // Patch:End return cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE); } } ================================================ FILE: keycloak/patches/keycloak-model-infinispan-patch/src/main/java/org/keycloak/patch/infinispan/keymappers/CustomDefaultTwoWayKey2StringMapper.java ================================================ package org.keycloak.patch.infinispan.keymappers; import org.infinispan.commons.marshall.WrappedByteArray; import org.infinispan.persistence.keymappers.TwoWayKey2StringMapper; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import java.util.Base64; import java.util.UUID; /** * Patched version of {@link org.infinispan.persistence.keymappers.DefaultTwoWayKey2StringMapper} with added support * for {@link UUID UUID's}. This allows us to store clientSessions cache entries with the infinispan jdbc-store. * * @since 4.1 */ public class CustomDefaultTwoWayKey2StringMapper implements TwoWayKey2StringMapper { private static final Log log = LogFactory.getLog(CustomDefaultTwoWayKey2StringMapper.class); private static final char NON_STRING_PREFIX = '\uFEFF'; private static final char SHORT_IDENTIFIER = '1'; private static final char BYTE_IDENTIFIER = '2'; private static final char LONG_IDENTIFIER = '3'; private static final char INTEGER_IDENTIFIER = '4'; private static final char DOUBLE_IDENTIFIER = '5'; private static final char FLOAT_IDENTIFIER = '6'; private static final char BOOLEAN_IDENTIFIER = '7'; private static final char BYTEARRAYKEY_IDENTIFIER = '8'; private static final char NATIVE_BYTEARRAYKEY_IDENTIFIER = '9'; // PATCH:Begin private static final char UUID_IDENTIFIER = '9' + 1; // PATCH:END @Override public String getStringMapping(Object key) { char identifier; if (key.getClass().equals(String.class)) { return key.toString(); } else if (key.getClass().equals(Short.class)) { identifier = SHORT_IDENTIFIER; } else if (key.getClass().equals(Byte.class)) { identifier = BYTE_IDENTIFIER; } else if (key.getClass().equals(Long.class)) { identifier = LONG_IDENTIFIER; } else if (key.getClass().equals(Integer.class)) { identifier = INTEGER_IDENTIFIER; } else if (key.getClass().equals(Double.class)) { identifier = DOUBLE_IDENTIFIER; } else if (key.getClass().equals(Float.class)) { identifier = FLOAT_IDENTIFIER; } else if (key.getClass().equals(Boolean.class)) { identifier = BOOLEAN_IDENTIFIER; } else if (key.getClass().equals(WrappedByteArray.class)) { return generateString(BYTEARRAYKEY_IDENTIFIER, Base64.getEncoder().encodeToString(((WrappedByteArray) key).getBytes())); } else if (key.getClass().equals(byte[].class)) { return generateString(NATIVE_BYTEARRAYKEY_IDENTIFIER, Base64.getEncoder().encodeToString((byte[]) key)); } // PATCH:Begin else if (key.getClass().equals(UUID.class)) { identifier = UUID_IDENTIFIER; } // PATCH:End else { throw new IllegalArgumentException("Unsupported key type: " + key.getClass().getName()); } return generateString(identifier, key.toString()); } @Override public Object getKeyMapping(String key) { log.tracef("Get mapping for key: %s", key); if (key.length() > 0 && key.charAt(0) == NON_STRING_PREFIX) { char type = key.charAt(1); String value = key.substring(2); switch (type) { case SHORT_IDENTIFIER: return Short.parseShort(value); case BYTE_IDENTIFIER: return Byte.parseByte(value); case LONG_IDENTIFIER: return Long.parseLong(value); case INTEGER_IDENTIFIER: return Integer.parseInt(value); case DOUBLE_IDENTIFIER: return Double.parseDouble(value); case FLOAT_IDENTIFIER: return Float.parseFloat(value); case BOOLEAN_IDENTIFIER: return Boolean.parseBoolean(value); // PATCH:Begin case UUID_IDENTIFIER: return UUID.fromString(value); // PATCH:End case BYTEARRAYKEY_IDENTIFIER: byte[] bytes = Base64.getDecoder().decode(value); return new WrappedByteArray(bytes); case NATIVE_BYTEARRAYKEY_IDENTIFIER: return Base64.getDecoder().decode(value); default: throw new IllegalArgumentException("Unsupported type code: " + type); } } else { return key; } } @Override public boolean isSupportedType(Class keyType) { return isPrimitive(keyType) || keyType == WrappedByteArray.class; } private String generateString(char identifier, String s) { return NON_STRING_PREFIX + String.valueOf(identifier) + s; } private static boolean isPrimitive(Class key) { return key == String.class || key == Short.class || key == Byte.class || key == Long.class || key == Integer.class || key == Double.class || key == Float.class || key == Boolean.class || key == byte[].class // PATCH:Begin || key == UUID.class // PATCH:End ; } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/pom.xml ================================================ 4.0.0 org.example wildfly-clustering-infinispan-extension-patch 1.0-SNAPSHOT UTF-8 11 11 23.0.2.Final 1.1.13.Final 11.0.9.Final 4.3.5.Final 1.0 4.1.63.Final 3.0.9 1.8 org.wildfly wildfly-clustering-infinispan-extension ${version.wildfly} org.wildfly wildfly-clustering-ee-infinispan ${version.wildfly} org.wildfly wildfly-clustering-jgroups-extension ${version.wildfly} org.wildfly wildfly-clustering-infinispan-client ${version.wildfly} org.wildfly wildfly-clustering-infinispan-marshalling ${version.wildfly} org.wildfly wildfly-clustering-infinispan-spi ${version.wildfly} org.wildfly wildfly-clustering-marshalling-jboss ${version.wildfly} org.wildfly wildfly-clustering-spi ${version.wildfly} org.wildfly wildfly-transactions ${version.wildfly} org.wildfly.transaction wildfly-transaction-client ${version.org.wildfly.transaction.client} org.infinispan infinispan-cachestore-jdbc ${version.org.infinispan} org.infinispan infinispan-cachestore-remote ${version.org.infinispan} org.infinispan.protostream protostream ${version.org.infinispan.protostream} net.jcip jcip-annotations ${version.net.jcip} io.netty netty-all ${version.io.netty} io.reactivex.rxjava3 rxjava ${version.io.reactivex.rxjava3} org.kohsuke.metainf-services metainf-services provided ${version.org.kohsuke.metainf-services} wildfly-clustering-infinispan-extension-patch src/main/java **/*.properties org.apache.maven.plugins maven-shade-plugin 3.2.4 package shade org.wildfly:wildfly-clustering-infinispan-extension org/jboss/as/** **/*.properties schema/* subsystem-templates/* META-INF/services/* org.wildfly:wildfly-clustering-infinispan-extension org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/readme.md ================================================ Patched Wildfly Clustering Infinispan Extension --- The current wildfly version 23.0.2 used by Keycloak 14.0.0 does not support the configuration of a `connect-timeout` for infinispan remote cache stores. This repo contains a patched version of [Wildflys Infinispan Extension](https://github.com/wildfly/wildfly/tree/master/clustering/infinispan/extension) with proper support for configuring `connect-timeouts`. See the related wildfly issue: [https://issues.redhat.com/browse/WFLY-15046](https://issues.redhat.com/browse/WFLY-15046). A docker volume mount for the patch could look like this: ``` ./patch/wildfly-clustering-infinispan-extension-patch.jar:/opt/jboss/keycloak/modules/system/layers/base/org/jboss/as/clustering/infinispan/main/wildfly-clustering-infinispan-extension-23.0.2.Final.jar:z ``` An usage example can be found in [haproxy-external-ispn](/deployments/local/cluster/haproxy-external-ispn). ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/InfinispanSubsystemXMLReader.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import static org.jboss.as.clustering.infinispan.InfinispanLogger.ROOT_LOGGER; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import org.jboss.as.clustering.controller.Attribute; import org.jboss.as.clustering.controller.Operations; import org.jboss.as.clustering.controller.ResourceDefinitionProvider; import org.jboss.as.clustering.infinispan.subsystem.TableResourceDefinition.ColumnAttribute; import org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.InvalidationNearCacheResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteTransactionResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition; import org.jboss.as.clustering.jgroups.subsystem.ChannelResourceDefinition; import org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemResourceDefinition; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.AttributeParser; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.operations.common.Util; import org.jboss.as.controller.parsing.Element; import org.jboss.as.controller.parsing.ParseUtils; import org.jboss.dmr.ModelNode; import org.jboss.staxmapper.XMLElementReader; import org.jboss.staxmapper.XMLExtendedStreamReader; /** * XML reader for the Infinispan subsystem. * * @author Paul Ferraro */ @SuppressWarnings({ "deprecation", "static-method" }) public class InfinispanSubsystemXMLReader implements XMLElementReader> { private final InfinispanSchema schema; InfinispanSubsystemXMLReader(InfinispanSchema schema) { this.schema = schema; } @Override public void readElement(XMLExtendedStreamReader reader, List result) throws XMLStreamException { Map operations = new LinkedHashMap<>(); PathAddress address = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case CACHE_CONTAINER: { this.parseContainer(reader, address, operations); break; } case REMOTE_CACHE_CONTAINER: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseRemoteContainer(reader, address, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } result.addAll(operations.values()); } private void parseContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = subsystemAddress.append(CacheContainerResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { ParseUtils.requireNoNamespaceAttribute(reader, i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case DEFAULT_CACHE: { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE); break; } case JNDI_NAME: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME); break; } case LISTENER_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.LISTENER); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER.getName()); break; } case EVICTION_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.EVICTION); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION.getName()); break; } case REPLICATION_QUEUE_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE.getName()); break; } case START: { if (this.schema.since(InfinispanSchema.VERSION_1_1) && !this.schema.since(InfinispanSchema.VERSION_3_0)) { ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); } else { throw ParseUtils.unexpectedAttribute(reader, i); } break; } case ALIASES: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.ALIASES); break; } } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } if (this.schema.since(InfinispanSchema.VERSION_1_3)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.MODULE); break; } } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_1_5)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, i, operation, CacheResourceDefinition.ListAttribute.MODULES); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } if (!this.schema.since(InfinispanSchema.VERSION_1_5)) { operation.get(CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true); } List aliases = new LinkedList<>(); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ALIAS: { if (InfinispanSchema.VERSION_1_0.since(this.schema)) { aliases.add(reader.getElementText()); break; } throw ParseUtils.unexpectedElement(reader); } case TRANSPORT: { this.parseTransport(reader, address, operations); break; } case LOCAL_CACHE: { this.parseLocalCache(reader, address, operations); break; } case INVALIDATION_CACHE: { this.parseInvalidationCache(reader, address, operations); break; } case REPLICATED_CACHE: { this.parseReplicatedCache(reader, address, operations); break; } case DISTRIBUTED_CACHE: { this.parseDistributedCache(reader, address, operations); break; } case EXPIRATION_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseScheduledThreadPool(ScheduledThreadPoolResourceDefinition.EXPIRATION, reader, address, operations); break; } } case ASYNC_OPERATIONS_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.ASYNC_OPERATIONS, reader, address, operations); break; } } case LISTENER_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.LISTENER, reader, address, operations); break; } } case PERSISTENCE_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { if (this.schema.since(InfinispanSchema.VERSION_7_0) && !this.schema.since(InfinispanSchema.VERSION_10_0)) { this.parseScheduledThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations); } else { this.parseThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations); } break; } } case REMOTE_COMMAND_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.REMOTE_COMMAND, reader, address, operations); break; } } case STATE_TRANSFER_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.STATE_TRANSFER, reader, address, operations); break; } } case TRANSPORT_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.TRANSPORT, reader, address, operations); break; } } case SCATTERED_CACHE: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseScatteredCache(reader, address, operations); break; } } case BLOCKING_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.BLOCKING, reader, address, operations); break; } } case NON_BLOCKING_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.NON_BLOCKING, reader, address, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } if (!aliases.isEmpty()) { // Adapt aliases parsed from legacy schema into format expected by the current attribute parser setAttribute(reader, String.join(" ", aliases), operation, CacheContainerResourceDefinition.ListAttribute.ALIASES); } } private void parseTransport(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(JGroupsTransportResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(containerAddress.append(TransportResourceDefinition.WILDCARD_PATH), operation); String stack = null; String cluster = null; for (int i = 0; i < reader.getAttributeCount(); i++) { String value = reader.getAttributeValue(i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STACK: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } stack = value; break; } case EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT); ROOT_LOGGER.executorIgnored(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT.getName()); break; } case LOCK_TIMEOUT: { readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT); break; } case SITE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.SITE.getLocalName()); break; } case RACK: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.RACK.getLocalName()); break; } case MACHINE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.MACHINE.getLocalName()); break; } case CLUSTER: { if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_3_0)) { cluster = value; break; } throw ParseUtils.unexpectedAttribute(reader, i); } case CHANNEL: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } if (!this.schema.since(InfinispanSchema.VERSION_3_0)) { // We need to create a corresponding channel add operation String channel = (cluster != null) ? cluster : ("ee-" + containerAddress.getLastElement().getValue()); setAttribute(reader, channel, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL); PathAddress channelAddress = PathAddress.pathAddress(JGroupsSubsystemResourceDefinition.PATH, ChannelResourceDefinition.pathElement(channel)); ModelNode channelOperation = Util.createAddOperation(channelAddress); if (stack != null) { setAttribute(reader, stack, channelOperation, ChannelResourceDefinition.Attribute.STACK); } operations.put(channelAddress, channelOperation); } ParseUtils.requireNoContent(reader); } private void parseLocalCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(LocalCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseCacheElement(reader, address, operations); } } private void parseReplicatedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(ReplicatedCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseClusteredCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseSharedStateCacheElement(reader, address, operations); } } private void parseScatteredCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(ScatteredCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case BIAS_LIFESPAN: { readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN); break; } case INVALIDATION_BATCH_SIZE: { readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE); break; } default: { this.parseSegmentedCacheAttribute(reader, i, address, operations); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseSharedStateCacheElement(reader, address, operations); } } private void parseDistributedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(DistributedCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case OWNERS: { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.OWNERS); break; } case L1_LIFESPAN: { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN); break; } case VIRTUAL_NODES: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { throw ParseUtils.unexpectedAttribute(reader, i); } // AS7-5753: convert any non-expression virtual nodes value to a segments value, String virtualNodes = readAttribute(reader, i, SegmentedCacheResourceDefinition.Attribute.SEGMENTS).asString(); String segments = SegmentsAndVirtualNodeConverter.virtualNodesToSegments(virtualNodes); setAttribute(reader, segments, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS); break; } case CAPACITY_FACTOR: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR); break; } } default: { this.parseSegmentedCacheAttribute(reader, i, address, operations); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { this.parseSharedStateCacheElement(reader, address, operations); } else { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REHASHING: { this.parseStateTransfer(reader, address, operations); break; } default: { this.parseCacheElement(reader, address, operations); } } } } } private void parseInvalidationCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(InvalidationCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseClusteredCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseCacheElement(reader, address, operations); } } private void parseCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case NAME: { // Already read break; } case START: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case BATCHING: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } PathAddress transactionAddress = address.append(TransactionResourceDefinition.PATH); ModelNode transactionOperation = Util.createAddOperation(transactionAddress); transactionOperation.get(TransactionResourceDefinition.Attribute.MODE.getName()).set(new ModelNode(TransactionMode.BATCH.name())); operations.put(transactionAddress, transactionOperation); break; } case INDEXING: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { throw ParseUtils.unexpectedAttribute(reader, index); } readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING); break; } case JNDI_NAME: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.JNDI_NAME); break; } } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } if (this.schema.since(InfinispanSchema.VERSION_1_3)) { readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.MODULE); break; } } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_1_5)) { readAttribute(reader, index, operation, CacheResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, index, operation, CacheResourceDefinition.ListAttribute.MODULES); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } if (!this.schema.since(InfinispanSchema.VERSION_1_5)) { // We need to explicitly enable statistics (to reproduce old behavior), since the new attribute defaults to false. operation.get(CacheResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true); } } private void parseSegmentedCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SEGMENTS: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS); break; } } case CONSISTENT_HASH_STRATEGY: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY); break; } } default: { this.parseClusteredCacheAttribute(reader, index, address, operations); } } } private void parseClusteredCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case MODE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } break; } case QUEUE_SIZE: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE); break; } case QUEUE_FLUSH_INTERVAL: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL); break; } case REMOTE_TIMEOUT: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT); break; } case ASYNC_MARSHALLING: { if (!this.schema.since(InfinispanSchema.VERSION_1_2) && this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { this.parseCacheAttribute(reader, index, address, operations); } } } private void parseCacheElement(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case EVICTION: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } this.parseEviction(reader, cacheAddress, operations); break; } case EXPIRATION: { this.parseExpiration(reader, cacheAddress, operations); break; } case LOCKING: { this.parseLocking(reader, cacheAddress, operations); break; } case TRANSACTION: { this.parseTransaction(reader, cacheAddress, operations); break; } case STORE: { this.parseCustomStore(reader, cacheAddress, operations); break; } case FILE_STORE: { this.parseFileStore(reader, cacheAddress, operations); break; } case REMOTE_STORE: { this.parseRemoteStore(reader, cacheAddress, operations); break; } case HOTROD_STORE: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseHotRodStore(reader, cacheAddress, operations); break; } } case JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseJDBCStore(reader, cacheAddress, operations); } else { this.parseLegacyJDBCStore(reader, cacheAddress, operations); } break; } case STRING_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseStringKeyedJDBCStore(reader, cacheAddress, operations); break; } } case BINARY_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseBinaryKeyedJDBCStore(reader, cacheAddress, operations); break; } } case MIXED_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseMixedKeyedJDBCStore(reader, cacheAddress, operations); break; } } case INDEXING: { if (this.schema.since(InfinispanSchema.VERSION_1_4) && !this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseIndexing(reader, cacheAddress, operations); break; } } case OBJECT_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseHeapMemory(reader, cacheAddress, operations); break; } } case BINARY_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseBinaryMemory(reader, cacheAddress, operations); break; } } case OFF_HEAP_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseOffHeapMemory(reader, cacheAddress, operations); break; } } case HEAP_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseHeapMemory(reader, cacheAddress, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } private void parseSharedStateCacheElement(XMLExtendedStreamReader reader, PathAddress address, Map operations) throws XMLStreamException { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case STATE_TRANSFER: { this.parseStateTransfer(reader, address, operations); break; } case BACKUPS: { if (this.schema.since(InfinispanSchema.VERSION_2_0)) { this.parseBackups(reader, address, operations); break; } } case BACKUP_FOR: { if (this.schema.since(InfinispanSchema.VERSION_2_0) && !this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseBackupFor(reader, address, operations); break; } throw ParseUtils.unexpectedElement(reader); } case PARTITION_HANDLING: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parsePartitionHandling(reader, address, operations); break; } } default: { this.parseCacheElement(reader, address, operations); } } } private void parsePartitionHandling(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(PartitionHandlingResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ENABLED: { readAttribute(reader, i, operation, PartitionHandlingResourceDefinition.Attribute.ENABLED); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseStateTransfer(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(StateTransferResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case TIMEOUT: { readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.TIMEOUT); break; } case FLUSH_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case CHUNK_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.CHUNK_SIZE); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseBackups(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BackupsResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BACKUP: { this.parseBackup(reader, address, operations); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseBackup(XMLExtendedStreamReader reader, PathAddress backupsAddress, Map operations) throws XMLStreamException { String site = require(reader, XMLAttribute.SITE); PathAddress address = backupsAddress.append(BackupResourceDefinition.pathElement(site)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SITE: { // Already parsed break; } case STRATEGY: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.STRATEGY); break; } case BACKUP_FAILURE_POLICY: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.FAILURE_POLICY); break; } case TIMEOUT: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.TIMEOUT); break; } case ENABLED: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.ENABLED); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case TAKE_OFFLINE: { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case TAKE_OFFLINE_AFTER_FAILURES: { readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES); break; } case TAKE_OFFLINE_MIN_WAIT: { readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseBackupFor(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BackupForResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case REMOTE_CACHE: { readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.CACHE); break; } case REMOTE_SITE: { readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.SITE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseLocking(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(LockingResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ISOLATION: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ISOLATION); break; } case STRIPING: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.STRIPING); break; } case ACQUIRE_TIMEOUT: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT); break; } case CONCURRENCY_LEVEL: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.CONCURRENCY); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseTransaction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(TransactionResourceDefinition.PATH); ModelNode operation = operations.get(address); if (operation == null) { operation = Util.createAddOperation(address); operations.put(address, operation); } for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STOP_TIMEOUT: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.STOP_TIMEOUT); break; } case MODE: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.MODE); break; } case LOCKING: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.LOCKING); break; } case EAGER_LOCKING: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseEviction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STRATEGY: { ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case MAX_ENTRIES: { readAttribute(reader, i, operation, HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES); break; } case INTERVAL: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseExpiration(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(ExpirationResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_IDLE: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.MAX_IDLE); break; } case LIFESPAN: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.LIFESPAN); break; } case INTERVAL: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.INTERVAL); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseIndexing(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { ModelNode operation = operations.get(cacheAddress); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case INDEX: { readAttribute(reader, i, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { Element element = Element.forName(reader.getLocalName()); switch (element) { case PROPERTY: { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING_PROPERTIES); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SIZE_UNIT: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { readAttribute(reader, i, operation, HeapMemoryResourceDefinition.Attribute.SIZE_UNIT); break; } } default: { this.parseMemoryAttribute(reader, i, operation); } } } ParseUtils.requireNoContent(reader); } private void parseBinaryMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.BINARY_PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseBinaryMemoryAttribute(reader, i, operation); } ParseUtils.requireNoContent(reader); } private void parseOffHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CAPACITY: { readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY); break; } case SIZE_UNIT: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.Attribute.SIZE_UNIT); break; } } default: { this.parseBinaryMemoryAttribute(reader, i, operation); } } } ParseUtils.requireNoContent(reader); } private void parseBinaryMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case EVICTION_TYPE: { readAttribute(reader, index, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE); break; } default: { this.parseMemoryAttribute(reader, index, operation); } } } private void parseMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SIZE: { readAttribute(reader, index, operation, MemoryResourceDefinition.Attribute.SIZE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseCustomStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(CustomStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CLASS: { readAttribute(reader, i, operation, CustomStoreResourceDefinition.Attribute.CLASS); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } if (!operation.hasDefined(CustomStoreResourceDefinition.Attribute.CLASS.getName())) { throw ParseUtils.missingRequired(reader, EnumSet.of(XMLAttribute.CLASS)); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseFileStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(FileStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case RELATIVE_TO: { readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_TO); break; } case PATH: { readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_PATH); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseRemoteStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(RemoteStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CACHE: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CACHE); break; } case SOCKET_TIMEOUT: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT); break; } // keycloak patch: begin case CONNECT_TIMEOUT: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CONNECT_TIMEOUT); break; } // keycloak patch: end case TCP_NO_DELAY: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY); break; } case REMOTE_SERVERS: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS); break; } } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REMOTE_SERVER: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedElement(reader); } for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case OUTBOUND_SOCKET_BINDING: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); break; } default: { this.parseStoreElement(reader, address, operations); } } } if (!operation.hasDefined(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS.getName())) { throw ParseUtils.missingRequired(reader, Collections.singleton(XMLAttribute.REMOTE_SERVERS.getLocalName())); } } private void parseHotRodStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HotRodStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CACHE_CONFIGURATION: { readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION); break; } case REMOTE_CACHE_CONTAINER: { readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.REMOTE_CACHE_CONTAINER); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(JDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseLegacyJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { // We don't know the path yet PathAddress address = null; PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ENTRY_TABLE: { if (address != null) { this.removeStoreOperations(address, operations); } address = cacheAddress.append((address == null) ? StringKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH); Operations.setPathAddress(operation, address); ModelNode binaryTableOperation = operations.get(operationKey.append(BinaryTableResourceDefinition.PATH)); if (binaryTableOperation != null) { // Fix address of binary table operation Operations.setPathAddress(binaryTableOperation, address.append(BinaryTableResourceDefinition.PATH)); } this.parseJDBCStoreStringTable(reader, address, operations); break; } case BUCKET_TABLE: { if (address != null) { this.removeStoreOperations(address, operations); } address = cacheAddress.append((address == null) ? BinaryKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH); Operations.setPathAddress(operation, address); ModelNode stringTableOperation = operations.get(operationKey.append(StringTableResourceDefinition.PATH)); if (stringTableOperation != null) { // Fix address of string table operation Operations.setPathAddress(stringTableOperation, address.append(StringTableResourceDefinition.PATH)); } this.parseJDBCStoreBinaryTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseBinaryKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BinaryKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BINARY_KEYED_TABLE: { this.parseJDBCStoreBinaryTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseStringKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(StringKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case STRING_KEYED_TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseMixedKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(MixedKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BINARY_KEYED_TABLE: { this.parseJDBCStoreBinaryTable(reader, address, operations); break; } case STRING_KEYED_TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseJDBCStoreAttributes(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case DATASOURCE: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE); break; } case DIALECT: { if (this.schema.since(InfinispanSchema.VERSION_2_0)) { readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DIALECT); break; } } case DATA_SOURCE: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DATA_SOURCE); break; } } default: { this.parseStoreAttribute(reader, i, operation); } } } Attribute requiredAttribute = this.schema.since(InfinispanSchema.VERSION_4_0) ? JDBCStoreResourceDefinition.Attribute.DATA_SOURCE : JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE; if (!operation.hasDefined(requiredAttribute.getName())) { throw ParseUtils.missingRequired(reader, requiredAttribute.getName()); } } private void parseJDBCStoreBinaryTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(BinaryTableResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(BinaryTableResourceDefinition.PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case PREFIX: { readAttribute(reader, i, operation, BinaryTableResourceDefinition.Attribute.PREFIX); break; } default: { this.parseJDBCStoreTableAttribute(reader, i, operation); } } } this.parseJDBCStoreTableElements(reader, operation); } private void parseJDBCStoreStringTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(StringTableResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(StringTableResourceDefinition.PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case PREFIX: { readAttribute(reader, i, operation, StringTableResourceDefinition.Attribute.PREFIX); break; } default: { this.parseJDBCStoreTableAttribute(reader, i, operation); } } } this.parseJDBCStoreTableElements(reader, operation); } private void parseJDBCStoreTableAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case FETCH_SIZE: { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.FETCH_SIZE); break; } case BATCH_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } readAttribute(reader, index, operation, TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE); break; } case CREATE_ON_START: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.CREATE_ON_START); break; } } case DROP_ON_STOP: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.DROP_ON_STOP); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseJDBCStoreTableElements(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException { while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ID_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.ID, operation.get(TableResourceDefinition.ColumnAttribute.ID.getName()).setEmptyObject()); break; } case DATA_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.DATA, operation.get(TableResourceDefinition.ColumnAttribute.DATA.getName()).setEmptyObject()); break; } case TIMESTAMP_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.TIMESTAMP, operation.get(TableResourceDefinition.ColumnAttribute.TIMESTAMP.getName()).setEmptyObject()); break; } case SEGMENT_COLUMN: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { this.parseJDBCStoreColumn(reader, ColumnAttribute.SEGMENT, operation.get(TableResourceDefinition.ColumnAttribute.SEGMENT.getName()).setEmptyObject()); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseJDBCStoreColumn(XMLExtendedStreamReader reader, ColumnAttribute columnAttribute, ModelNode column) throws XMLStreamException { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { readAttribute(reader, i, column, columnAttribute.getColumnName()); break; } case TYPE: { readAttribute(reader, i, column, columnAttribute.getColumnType()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void removeStoreOperations(PathAddress storeAddress, Map operations) { operations.remove(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH)); } private void parseStoreAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SHARED: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.SHARED); break; } case PRELOAD: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PRELOAD); break; } case PASSIVATION: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PASSIVATION); break; } case FETCH_STATE: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.FETCH_STATE); break; } case PURGE: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PURGE); break; } case SINGLETON: { readAttribute(reader, index, operation, StoreResourceDefinition.DeprecatedAttribute.SINGLETON); break; } case MAX_BATCH_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.MAX_BATCH_SIZE); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseStoreElement(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { ModelNode operation = operations.get(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH)); XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case PROPERTY: { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, StoreResourceDefinition.Attribute.PROPERTIES); break; } case WRITE_BEHIND: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseStoreWriteBehind(reader, storeAddress, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } private void parseStoreWriteBehind(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(StoreWriteBehindResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case FLUSH_LOCK_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case MODIFICATION_QUEUE_SIZE: { readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE); break; } case SHUTDOWN_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case THREAD_POOL_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private

void parseThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map operations) throws XMLStreamException { PathAddress address = parentAddress.append(pool.getPathElement()); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MIN_THREADS: { if (pool.getMinThreads() != null) { readAttribute(reader, i, operation, pool.getMinThreads()); } break; } case MAX_THREADS: { readAttribute(reader, i, operation, pool.getMaxThreads()); break; } case QUEUE_LENGTH: { if (pool.getQueueLength() != null) { readAttribute(reader, i, operation, pool.getQueueLength()); } break; } case KEEPALIVE_TIME: { readAttribute(reader, i, operation, pool.getKeepAliveTime()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private

void parseScheduledThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map operations) throws XMLStreamException { PathAddress address = parentAddress.append(pool.getPathElement()); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_THREADS: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, pool.getMinThreads()); break; } case KEEPALIVE_TIME: { readAttribute(reader, i, operation, pool.getKeepAliveTime()); break; } case MIN_THREADS: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { readAttribute(reader, i, operation, pool.getMinThreads()); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = subsystemAddress.append(RemoteCacheContainerResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { ParseUtils.requireNoNamespaceAttribute(reader, i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case CONNECTION_TIMEOUT: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT); break; } case DEFAULT_REMOTE_CLUSTER: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER); break; } case KEY_SIZE_ESTIMATE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.KEY_SIZE_ESTIMATE); break; } case MAX_RETRIES: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES); break; } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.MODULE); break; } case PROTOCOL_VERSION: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION); break; } case SOCKET_TIMEOUT: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT); break; } case TCP_NO_DELAY: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY); break; } case TCP_KEEP_ALIVE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE); break; } case VALUE_SIZE_ESTIMATE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.VALUE_SIZE_ESTIMATE); break; } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.ListAttribute.MODULES); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ASYNC_THREAD_POOL: { this.parseThreadPool(ThreadPoolResourceDefinition.CLIENT, reader, address, operations); break; } case CONNECTION_POOL: { this.parseConnectionPool(reader, address, operations); break; } case INVALIDATION_NEAR_CACHE: { this.parseInvalidationNearCache(reader, address, operations); break; } case REMOTE_CLUSTERS: { this.parseRemoteClusters(reader, address, operations); break; } case SECURITY: { this.parseRemoteCacheContainerSecurity(reader, address, operations); break; } case TRANSACTION: { if (this.schema.since(InfinispanSchema.VERSION_8_0)) { this.parseRemoteTransaction(reader, address, operations); break; } } case PROPERTY: { if (this.schema.since(InfinispanSchema.VERSION_11_0) || (this.schema.since(InfinispanSchema.VERSION_9_1) && !this.schema.since(InfinispanSchema.VERSION_10_0))) { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, RemoteCacheContainerResourceDefinition.Attribute.PROPERTIES); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseInvalidationNearCache(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(InvalidationNearCacheResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_ENTRIES: { readAttribute(reader, i, operation, InvalidationNearCacheResourceDefinition.Attribute.MAX_ENTRIES); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseConnectionPool(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(ConnectionPoolResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case EXHAUSTED_ACTION: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION); break; } case MAX_ACTIVE: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE); break; } case MAX_WAIT: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_WAIT); break; } case MIN_EVICTABLE_IDLE_TIME: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME); break; } case MIN_IDLE: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_IDLE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteClusters(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { ParseUtils.requireNoAttributes(reader); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REMOTE_CLUSTER: { this.parseRemoteCluster(reader, containerAddress, operations); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseRemoteCluster(XMLExtendedStreamReader reader, PathAddress clustersAddress, Map operations) throws XMLStreamException { String remoteCluster = require(reader, XMLAttribute.NAME); PathAddress address = clustersAddress.append(RemoteClusterResourceDefinition.pathElement(remoteCluster)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case SOCKET_BINDINGS: { readAttribute(reader, i, operation, RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteCacheContainerSecurity(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(SecurityResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SSL_CONTEXT: { readAttribute(reader, i, operation, SecurityResourceDefinition.Attribute.SSL_CONTEXT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteTransaction(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(RemoteTransactionResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MODE: { readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.MODE); break; } case TIMEOUT: { readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.TIMEOUT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private static String require(XMLExtendedStreamReader reader, XMLAttribute attribute) throws XMLStreamException { String value = reader.getAttributeValue(null, attribute.getLocalName()); if (value == null) { throw ParseUtils.missingRequired(reader, attribute.getLocalName()); } return value; } private static ModelNode readAttribute(XMLExtendedStreamReader reader, int index, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); return definition.getParser().parse(definition, reader.getAttributeValue(index), reader); } private static void readAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation, Attribute attribute) throws XMLStreamException { setAttribute(reader, reader.getAttributeValue(index), operation, attribute); } private static void setAttribute(XMLExtendedStreamReader reader, String value, ModelNode operation, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); definition.getParser().parseAndSetParameter(definition, value, operation, reader); } private static void readElement(XMLExtendedStreamReader reader, ModelNode operation, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); AttributeParser parser = definition.getParser(); if (parser.isParseAsElement()) { parser.parseElement(definition, reader, operation); } else { parser.parseAndSetParameter(definition, reader.getElementText(), operation, reader); } } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties ================================================ # subsystem resource infinispan=The configuration of the infinispan subsystem. infinispan.add=Add the infinispan subsystem. infinispan.describe=Describe the infinispan subsystem infinispan.remove=Remove the infinispan subsystem # cache container resource infinispan.cache-container=The configuration of an infinispan cache container infinispan.cache-container.default-cache=The default infinispan cache infinispan.cache-container.listener-executor=The executor used for the replication queue infinispan.cache-container.listener-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.eviction-executor=The scheduled executor used for eviction infinispan.cache-container.eviction-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.replication-queue-executor=The executor used for asynchronous cache operations infinispan.cache-container.replication-queue-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.jndi-name=The jndi name to which to bind this cache container infinispan.cache-container.jndi-name.deprecated=Deprecated. Will be ignored. infinispan.cache-container.module=The module associated with this cache container's configuration. infinispan.cache-container.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.cache-container.modules=The set of modules associated with this cache container's configuration. infinispan.cache-container.start=The cache container start mode, which can be EAGER (immediate start) or LAZY (on-demand start). infinispan.cache-container.start.deprecated=Deprecated. Future releases will only support LAZY mode. infinispan.cache-container.statistics-enabled=If enabled, statistics will be collected for this cache container infinispan.cache-container.thread-pool=Defines thread pools for this cache container infinispan.cache-container.cache=The list of caches available to this cache container infinispan.cache-container.singleton=A set of single-instance configuration elements of the cache container. infinispan.cache-container.aliases=The list of aliases for this cache container infinispan.cache-container.add-alias=Add an alias for this cache container infinispan.cache-container.add-alias.name=The name of the alias to add to this cache container infinispan.cache-container.add-alias.deprecated=Deprecated. Use list-add operation instead. infinispan.cache-container.remove-alias=Remove an alias for this cache container infinispan.cache-container.remove-alias.name=The name of the alias to remove from this cache container infinispan.cache-container.remove-alias.deprecated=Deprecated. Use list-remove operation instead. infinispan.cache-container.add=Add a cache container to the infinispan subsystem infinispan.cache-container.remove=Remove a cache container from the infinispan subsystem # cache container read-only metrics infinispan.cache-container.cache-manager-status=The status of the cache manager component. May return null if the cache manager is not started. infinispan.cache-container.cache-manager-status.deprecated=Deprecated. Always returns RUNNING. infinispan.cache-container.is-coordinator=Set to true if this node is the cluster's coordinator. May return null if the cache manager is not started. infinispan.cache-container.coordinator-address=The logical address of the cluster's coordinator. May return null if the cache manager is not started. infinispan.cache-container.local-address=The local address of the node. May return null if the cache manager is not started. infinispan.cache-container.cluster-name=The name of the cluster this node belongs to. May return null if the cache manager is not started. # cache container children infinispan.cache-container.transport=A transport child of the cache container. infinispan.cache-container.local-cache=A local cache child of the cache container. infinispan.cache-container.invalidation-cache=An invalidation cache child of the cache container. infinispan.cache-container.replicated-cache=A replicated cache child of the cache container. infinispan.cache-container.distributed-cache=A distributed cache child of the cache container. # thread-pool resources infinispan.thread-pool.deprecated=This thread pool is deprecated and will be ignored. infinispan.thread-pool.async-operations=Defines a thread pool used for asynchronous operations. infinispan.thread-pool.listener=Defines a thread pool used for asynchronous cache listener notifications. infinispan.thread-pool.persistence=Defines a thread pool used for interacting with the persistent store. infinispan.thread-pool.remote-command=Defines a thread pool used to execute remote commands. infinispan.thread-pool.state-transfer=Defines a thread pool used for for state transfer. infinispan.thread-pool.state-transfer.deprecated=Deprecated. Has no effect. infinispan.thread-pool.transport=Defines a thread pool used for asynchronous transport communication. infinispan.thread-pool.expiration=Defines a thread pool used for for evictions. infinispan.thread-pool.blocking=Defines a thread pool used for for blocking operations. infinispan.thread-pool.non-blocking=Defines a thread pool used for for non-blocking operations. infinispan.thread-pool.add=Adds a thread pool executor. infinispan.thread-pool.remove=Removes a thread pool executor. infinispan.thread-pool.min-threads=The core thread pool size which is smaller than the maximum pool size. If undefined, the core thread pool size is the same as the maximum thread pool size. infinispan.thread-pool.min-threads.deprecated=Deprecated. Has no effect. infinispan.thread-pool.max-threads=The maximum thread pool size. infinispan.thread-pool.max-threads.deprecated=Deprecated. Use min-threads instead. infinispan.thread-pool.queue-length=The queue length. infinispan.thread-pool.queue-length.deprecated=Deprecated. Has no effect. infinispan.thread-pool.keepalive-time=Used to specify the amount of milliseconds that pool threads should be kept running when idle; if not specified, threads will run until the executor is shut down. # transport resource infinispan.transport.jgroups=The description of the transport used by this cache container infinispan.transport.jgroups.add=Add the transport to the cache container infinispan.transport.jgroups.remove=Remove the transport from the cache container infinispan.transport.jgroups.channel=The channel of this cache container's transport. infinispan.transport.jgroups.cluster=The name of the group communication cluster infinispan.transport.jgroups.cluster.deprecated=Deprecated. The cluster used by the transport of this cache container is configured via the JGroups subsystem. infinispan.transport.jgroups.executor=The executor to use for the transport infinispan.transport.jgroups.executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.transport.jgroups.lock-timeout=The timeout for locks for the transport infinispan.transport.jgroups.machine=A machine identifier for the transport infinispan.transport.jgroups.rack=A rack identifier for the transport infinispan.transport.jgroups.site=A site identifier for the transport infinispan.transport.jgroups.stack=The jgroups stack to use for the transport infinispan.transport.jgroups.stack.deprecated=Deprecated. The protocol stack used by the transport of this cache container is configured via the JGroups subsystem. infinispan.transport.none=A local-only transport used by this cache-container infinispan.transport.none.add=Adds a local transport to this cache container infinispan.transport.none.remove=Removes a local transport from this cache container # (hierarchical) cache resource infinispan.cache.start=The cache start mode, which can be EAGER (immediate start) or LAZY (on-demand start). infinispan.cache.start.deprecated=Deprecated. Only LAZY mode is supported. infinispan.cache.statistics-enabled=If enabled, statistics will be collected for this cache infinispan.cache.batching=If enabled, the invocation batching API will be made available for this cache. infinispan.cache.batching.deprecated=Deprecated. Replaced by BATCH transaction mode. infinispan.cache.indexing=If enabled, entries will be indexed when they are added to the cache. Indexes will be updated as entries change or are removed. infinispan.cache.indexing.deprecated=Deprecated. Has no effect. infinispan.cache.jndi-name=The jndi-name to which to bind this cache instance. infinispan.cache.jndi-name.deprecated=Deprecated. Will be ignored. infinispan.cache.module=The module associated with this cache's configuration. infinispan.cache.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.cache.modules=The set of modules associated with this cache's configuration. infinispan.cache.indexing-properties=Properties to control indexing behaviour infinispan.cache.indexing-properties.deprecated=Deprecated. Has no effect. infinispan.cache.remove=Remove a cache from this container. # cache read-only metrics infinispan.cache.cache-status=The status of the cache component. infinispan.cache.cache-status.deprecated=Deprecated. Always returns RUNNING. infinispan.cache.average-read-time=Average time (in ms) for cache reads. Includes hits and misses. infinispan.cache.average-read-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.average-remove-time=Average time (in ms) for cache removes. infinispan.cache.average-remove-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.average-write-time=Average time (in ms) for cache writes. infinispan.cache.average-write-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.elapsed-time=Time (in secs) since cache started. infinispan.cache.elapsed-time.deprecated=Deprecated. Use time-since-start instead. infinispan.cache.hit-ratio=The hit/miss ratio for the cache (hits/hits+misses). infinispan.cache.hit-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.hits=The number of cache attribute hits. infinispan.cache.hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.misses=The number of cache attribute misses. infinispan.cache.misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.number-of-entries=The number of entries in the cache including passivated entries. infinispan.cache.number-of-entries.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.number-of-entries-in-memory=The number of entries in the cache excluding passivated entries. infinispan.cache.read-write-ratio=The read/write ratio of the cache ((hits+misses)/stores). infinispan.cache.read-write-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.remove-hits=The number of cache attribute remove hits. infinispan.cache.remove-hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.remove-misses=The number of cache attribute remove misses. infinispan.cache.remove-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.stores=The number of cache attribute put operations. infinispan.cache.stores.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.time-since-reset=Time (in secs) since cache statistics were reset. infinispan.cache.time-since-reset.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.time-since-start=Time (in secs) since cache was started. infinispan.cache.writes=The number of cache attribute put operations. infinispan.cache.invalidations=The number of cache invalidations. infinispan.cache.invalidations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.passivations=The number of cache node passivations (passivating a node from memory to a cache store). infinispan.cache.passivations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.activations=The number of cache node activations (bringing a node into memory from a cache store). infinispan.cache.activations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # infinispan.cache.async-marshalling=If enabled, this will cause marshalling of entries to be performed asynchronously. infinispan.cache.async-marshalling.deprecated=Deprecated. Asynchronous marshalling is no longer supported. infinispan.cache.mode=Sets the clustered cache mode, ASYNC for asynchronous operation, or SYNC for synchronous operation. infinispan.cache.mode.deprecated=Deprecated. This attribute will be ignored. All cache modes will be treated as SYNC. To perform asynchronous cache operations, use Infinispan's asynchronous cache API. infinispan.cache.queue-size=In ASYNC mode, this attribute can be used to trigger flushing of the queue when it reaches a specific threshold. infinispan.cache.queue-size.deprecated=Deprecated. This attribute will be ignored. infinispan.cache.queue-flush-interval=In ASYNC mode, this attribute controls how often the asynchronous thread used to flush the replication queue runs. This should be a positive integer which represents thread wakeup time in milliseconds. infinispan.cache.queue-flush-interval.deprecated=Deprecated. This attribute will be ignored. infinispan.cache.remote-timeout=In SYNC mode, the timeout (in ms) used to wait for an acknowledgment when making a remote call, after which the call is aborted and an exception is thrown. # metrics infinispan.cache.average-replication-time=The average time taken to replicate data around the cluster. infinispan.cache.average-replication-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.replication-count=The number of times data was replicated around the cluster. infinispan.cache.replication-count.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.replication-failures=The number of data replication failures. infinispan.cache.replication-failures.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.success-ratio=The data replication success ratio (successes/successes+failures). infinispan.cache.success-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # operations infinispan.cache.reset-statistics=Reset the statistics for this cache. #child resource aliases infinispan.cache.memory=Alias to the eviction configuration component infinispan.cache.eviction=Alias to the memory=object resource infinispan.cache.expiration=Alias to the expiration configuration component infinispan.cache.locking=Alias to the locking configuration component infinispan.cache.state-transfer=Alias to the state-transfer configuration component infinispan.cache.transaction=Alias to the transaction configuration component infinispan.cache.file-store=Alias to the file store configuration component infinispan.cache.remote-store=Alias to the file store configuration component infinispan.cache.binary-keyed-jdbc-store=Alias to the binary jdbc store configuration component infinispan.cache.mixed-keyed-jdbc-store=Alias to the mixed jdbc store configuration component infinispan.cache.string-keyed-jdbc-store=Alias to the string jdbc store configuration component infinispan.cache.write-behind=Alias to the write behind configuration component infinispan.cache.backup-for=Alias to the backup-for configuration component infinispan.cache.backup=Alias to the backup child of the backups configuration infinispan.cache.segments=Controls the number of hash space segments which is the granularity for key distribution in the cluster. Value must be strictly positive. infinispan.cache.consistent-hash-strategy=Defines the consistent hash strategy for the cache. infinispan.cache.consistent-hash-strategy.deprecated=Deprecated. Segment allocation is no longer customizable. infinispan.cache.evictions=The number of cache eviction operations. infinispan.local-cache=A local cache configuration infinispan.local-cache.add=Add a local cache to this cache container infinispan.local-cache.remove=Remove a local cache from this cache container infinispan.invalidation-cache=An invalidation cache infinispan.invalidation-cache.add=Add an invalidation cache to this cache container infinispan.invalidation-cache.remove=Remove an invalidation cache from this cache container infinispan.replicated-cache=A replicated cache configuration infinispan.replicated-cache.add=Add a replicated cache to this cache container infinispan.replicated-cache.remove=Remove a replicated cache from this cache container infinispan.component.partition-handling=The partition handling configuration for distributed and replicated caches. infinispan.component.partition-handling.add=Add a partition handling configuration. infinispan.component.partition-handling.remove=Remove a partition handling configuration. infinispan.component.partition-handling.enabled=If enabled, the cache will enter degraded mode upon detecting a network partition that threatens the integrity of the cache. infinispan.component.partition-handling.availability=Indicates the current availability of the cache. infinispan.component.partition-handling.availability.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.partition-handling.force-available=Forces a cache with degraded availability to become available. infinispan.component.partition-handling.force-available.deprecated=Deprecated. Use operation from corresponding runtime cache resource. infinispan.component.state-transfer=The state transfer configuration for distributed and replicated caches. infinispan.component.state-transfer.add=Add a state transfer configuration. infinispan.component.state-transfer.remove=Remove a state transfer configuration. infinispan.component.state-transfer.enabled=If enabled, this will cause the cache to ask neighboring caches for state when it starts up, so the cache starts 'warm', although it will impact startup time. infinispan.component.state-transfer.enabled.deprecated=Deprecated. Always enabled for replicated and distributed caches. infinispan.component.state-transfer.timeout=The maximum amount of time (ms) to wait for state from neighboring caches, before throwing an exception and aborting startup. If timeout is 0, state transfer is performed asynchronously, and the cache will be immediately available. infinispan.component.state-transfer.chunk-size=The maximum number of cache entries in a batch of transferred state. infinispan.distributed-cache=A distributed cache configuration. infinispan.distributed-cache.add=Add a distributed cache to this cache container infinispan.distributed-cache.remove=Remove a distributed cache from this cache container infinispan.distributed-cache.owners=Number of cluster-wide replicas for each cache entry. infinispan.distributed-cache.virtual-nodes=Deprecated. Has no effect. infinispan.distributed-cache.virtual-nodes.deprecated=Deprecated. Has no effect. infinispan.distributed-cache.l1-lifespan=Maximum lifespan of an entry placed in the L1 cache. This element configures the L1 cache behavior in 'distributed' caches instances. In any other cache modes, this element is ignored. infinispan.distributed-cache.capacity-factor=Controls the proportion of entries that will reside on the local node, compared to the other nodes in the cluster. infinispan.scattered-cache=A scattered cache configuration. infinispan.scattered-cache.add=Add a scattered cache to this cache container infinispan.scattered-cache.remove=Remove a scattered cache from this cache container infinispan.scattered-cache.bias-lifespan=When greater than zero, specifies the duration (in ms) that a cache entry will be cached on a non-owner following a write operation. infinispan.scattered-cache.invalidation-batch-size=The threshold after which batched invalidations are sent. infinispan.cache.store=A persistent store for a cache. infinispan.cache.component=A configuration component of a cache. infinispan.component.locking=The locking configuration of the cache. infinispan.component.locking.add=Adds a locking configuration element to the cache. infinispan.component.locking.remove=Removes a locking configuration element from the cache. infinispan.component.locking.isolation=Sets the cache locking isolation level. infinispan.component.locking.striping=If true, a pool of shared locks is maintained for all entries that need to be locked. Otherwise, a lock is created per entry in the cache. Lock striping helps control memory footprint but may reduce concurrency in the system. infinispan.component.locking.acquire-timeout=Maximum time to attempt a particular lock acquisition. infinispan.component.locking.concurrency-level=Concurrency level for lock containers. Adjust this value according to the number of concurrent threads interacting with Infinispan. # metrics infinispan.component.locking.current-concurrency-level=The estimated number of concurrently updating threads which this cache can support. infinispan.component.locking.current-concurrency-level.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.locking.number-of-locks-available=The number of locks available to this cache. infinispan.component.locking.number-of-locks-available.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.locking.number-of-locks-held=The number of locks currently in use by this cache. infinispan.component.locking.number-of-locks-held.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction=The cache transaction configuration. infinispan.component.transaction.deprecated=Deprecated. Transactional behavior should be defined per remote-cache. infinispan.component.transaction.add=Adds a transaction configuration element to the cache. infinispan.component.transaction.remove=Removes a transaction configuration element from the cache. infinispan.component.transaction.mode=Sets the cache transaction mode to one of NONE, NON_XA, NON_DURABLE_XA, FULL_XA. infinispan.component.transaction.stop-timeout=If there are any ongoing transactions when a cache is stopped, Infinispan waits for ongoing remote and local transactions to finish. The amount of time to wait for is defined by the cache stop timeout. infinispan.component.transaction.locking=The locking mode for this cache, one of OPTIMISTIC or PESSIMISTIC. infinispan.component.transaction.timeout=The duration (in ms) after which idle transactions are rolled back. # metrics infinispan.component.transaction.commits=The number of transaction commits. infinispan.component.transaction.commits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction.prepares=The number of transaction prepares. infinispan.component.transaction.prepares.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction.rollbacks=The number of transaction rollbacks. infinispan.component.transaction.rollbacks.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # infinispan.memory.heap=On-heap object-based memory configuration. infinispan.memory.off-heap=Off-heap memory configuration. infinispan.memory.add=Adds a memory configuration element to the cache. infinispan.memory.remove=Removes an eviction configuration element from the cache. infinispan.memory.size=Eviction threshold, as defined by the size unit. infinispan.memory.size-unit=The unit of the eviction threshold. infinispan.memory.object.size=Triggers eviction of the least recently used entries when the number of cache entries exceeds this threshold. infinispan.memory.eviction-type=Indicates whether the size attribute refers to the number of cache entries (i.e. COUNT) or the collective size of the cache entries (i.e. MEMORY). infinispan.memory.eviction-type.deprecated=Deprecated. Replaced by size-unit. infinispan.memory.capacity=Defines the capacity of the off-heap storage. infinispan.memory.capacity.deprecated=Deprecated. Will be ignored. infinispan.memory.strategy=Sets the cache eviction strategy. Available options are 'UNORDERED', 'FIFO', 'LRU', 'LIRS' and 'NONE' (to disable eviction). infinispan.memory.strategy.deprecated=Deprecated. Eviction uses LRU and is disabled via undefining the size attribute. infinispan.memory.max-entries=Maximum number of entries in a cache instance. If selected value is not a power of two the actual value will default to the least power of two larger than selected value. -1 means no limit. infinispan.memory.max-entries.deprecated=Deprecated. Use the size attribute instead. # metrics infinispan.memory.evictions=The number of cache eviction operations. infinispan.memory.evictions.deprecated=Deprecated. Use corresponding metric on parent resource. # infinispan.component.expiration=The cache expiration configuration. infinispan.component.expiration.add=Adds an expiration configuration element to the cache. infinispan.component.expiration.remove=Removes an expiration configuration element from the cache. infinispan.component.expiration.max-idle=Maximum idle time a cache entry will be maintained in the cache, in milliseconds. If the idle time is exceeded, the entry will be expired cluster-wide. -1 means the entries never expire. infinispan.component.expiration.lifespan=Maximum lifespan of a cache entry, after which the entry is expired cluster-wide, in milliseconds. -1 means the entries never expire. infinispan.component.expiration.interval=Interval (in milliseconds) between subsequent runs to purge expired entries from memory and any cache stores. If you wish to disable the periodic eviction process altogether, set wakeupInterval to -1. infinispan.store.custom=The cache store configuration. infinispan.store.custom.add=Adds a basic cache store configuration element to the cache. infinispan.store.custom.remove=Removes a cache store configuration element from the cache. infinispan.store.shared=This setting should be set to true when multiple cache instances share the same cache store (e.g., multiple nodes in a cluster using a JDBC-based CacheStore pointing to the same, shared database.) Setting this to true avoids multiple cache instances writing the same modification multiple times. If enabled, only the node where the modification originated will write to the cache store. If disabled, each individual cache reacts to a potential remote update by storing the data to the cache store. infinispan.store.preload=If true, when the cache starts, data stored in the cache store will be pre-loaded into memory. This is particularly useful when data in the cache store will be needed immediately after startup and you want to avoid cache operations being delayed as a result of loading this data lazily. Can be used to provide a 'warm-cache' on startup, however there is a performance penalty as startup time is affected by this process. infinispan.store.passivation=If true, data is only written to the cache store when it is evicted from memory, a phenomenon known as 'passivation'. Next time the data is requested, it will be 'activated' which means that data will be brought back to memory and removed from the persistent store. If false, the cache store contains a copy of the contents in memory, so writes to cache result in cache store writes. This essentially gives you a 'write-through' configuration. infinispan.store.fetch-state=If true, fetch persistent state when joining a cluster. If multiple cache stores are chained, only one of them can have this property enabled. infinispan.store.purge=If true, purges this cache store when it starts up. infinispan.store.max-batch-size=The maximum size of a batch to be inserted/deleted from the store. If the value is less than one, then no upper limit is placed on the number of operations in a batch. infinispan.store.singleton=If true, the singleton store cache store is enabled. SingletonStore is a delegating cache store used for situations when only one instance in a cluster should interact with the underlying store. infinispan.store.singleton.deprecated=Deprecated. Consider using a shared store instead, where writes are only performed by primary owners. infinispan.store.class=The custom store implementation class to use for this cache store. infinispan.store.write-behind=Child to configure a cache store as write-behind instead of write-through. infinispan.store.properties=A list of cache store properties. infinispan.store.properties.property=A cache store property with name and value. infinispan.store.property=A cache store property with name and value. infinispan.store.write=The write behavior of the cache store. # metrics infinispan.store.cache-loader-loads=The number of cache loader node loads. infinispan.store.cache-loader-loads.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.store.cache-loader-misses=The number of cache loader node misses. infinispan.store.cache-loader-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.persistence.cache-loader-loads=The number of entries loaded by this cache loader. infinispan.component.persistence.cache-loader-misses=The number of entry load misses by this cache loader. infinispan.write.behind=Configures a cache store as write-behind instead of write-through. infinispan.write.behind.add=Adds a write-behind configuration element to the store. infinispan.write.behind.remove=Removes a write-behind configuration element from the store. infinispan.write.behind.flush-lock-timeout=Timeout to acquire the lock which guards the state to be flushed to the cache store periodically. infinispan.write.behind.flush-lock-timeout.deprecated=Deprecated. This attribute is no longer used. infinispan.write.behind.modification-queue-size=Maximum number of entries in the asynchronous queue. When the queue is full, the store becomes write-through until it can accept new entries. infinispan.write.behind.shutdown-timeout=Timeout in milliseconds to stop the cache store. infinispan.write.behind.shutdown-timeout.deprecated=Deprecated. This attribute is no longer used. infinispan.write.behind.thread-pool-size=Size of the thread pool whose threads are responsible for applying the modifications to the cache store. infinispan.write.behind.thread-pool-size.deprecated=Deprecated. Uses size of non-blocking thread pool. infinispan.write.through=Configures a cache store as write-through. infinispan.write.through.add=Add a write-through configuration to the store. infinispan.write.through.remove=Remove a write-through configuration to the store. infinispan.property=A cache store property with name and value. infinispan.property.deprecated=Deprecated. Use "properties" attribute of the appropriate cache store resource. infinispan.property.add=Adds a cache store property. infinispan.property.remove=Removes a cache store property. infinispan.property.value=The value of the cache store property. infinispan.store.none=A store-less configuration. infinispan.store.none.add=Adds a store-less configuration to this cache infinispan.store.none.remove=Removes a store-less configuration from this cache infinispan.store.file=The cache file store configuration. infinispan.store.file.add=Adds a file cache store configuration element to the cache. infinispan.store.file.remove=Removes a cache file store configuration element from the cache. infinispan.store.file.relative-to=The system path to which the specified path is relative. infinispan.store.file.path=The system path under which this cache store will persist its entries. infinispan.store.jdbc=The cache JDBC store configuration. infinispan.store.jdbc.add=Adds a JDBC cache store configuration element to the cache. infinispan.store.jdbc.remove=Removes a JDBC cache store configuration element to the cache. infinispan.store.jdbc.data-source=References the data source used to connect to this store. infinispan.store.jdbc.datasource=The jndi name of the data source used to connect to this store. infinispan.store.jdbc.datasource.deprecated=Deprecated. Replaced by data-source. infinispan.store.jdbc.dialect=The dialect of this datastore. infinispan.store.jdbc.table=Defines a table used to store persistent cache data. infinispan.store.jdbc.binary-keyed-table=Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.store.jdbc.binary-keyed-table.deprecated=Deprecated. Use table=binary child resource. infinispan.store.jdbc.binary-keyed-table.table.prefix=The prefix for the database table name. infinispan.store.jdbc.binary-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.store.jdbc.binary-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.store.jdbc.binary-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.store.jdbc.binary-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.store.jdbc.binary-keyed-table.table.column.name= infinispan.store.jdbc.binary-keyed-table.table.column.type= infinispan.store.jdbc.binary-keyed-table.table.id-column=A database column to hold cache entry ids. infinispan.store.jdbc.binary-keyed-table.table.id-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.id-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.data-column=A database column to hold cache entry data. infinispan.store.jdbc.binary-keyed-table.table.data-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.data-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.segment-column=A database column to hold cache entry segment. infinispan.store.jdbc.binary-keyed-table.table.segment-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.segment-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table=Defines a table used to store persistent cache entries. infinispan.store.jdbc.string-keyed-table.deprecated=Deprecated. Use table=string child resource. infinispan.store.jdbc.string-keyed-table.table.prefix=The prefix for the database table name. infinispan.store.jdbc.string-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.store.jdbc.string-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.store.jdbc.string-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.store.jdbc.string-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.store.jdbc.string-keyed-table.table.column.name= infinispan.store.jdbc.string-keyed-table.table.column.type= infinispan.store.jdbc.string-keyed-table.table.id-column=A database column to hold cache entry ids. infinispan.store.jdbc.string-keyed-table.table.id-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.id-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.data-column=A database column to hold cache entry data. infinispan.store.jdbc.string-keyed-table.table.data-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.data-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.segment-column=A database column to hold cache entry segment. infinispan.store.jdbc.string-keyed-table.table.segment-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.segment-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.type=The type of the database column. infinispan.store.binary-jdbc.deprecated=Deprecated. Will be removed without replacement in a future release. Use store=jdbc instead. infinispan.store.mixed-jdbc.deprecated=Deprecated. Will be removed without replacement in a future release. Use store=jdbc instead. infinispan.table.binary=Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.deprecated=Deprecated. Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.add=Adds a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.remove=Removes a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.string=Defines a table used to store cache entries whose keys can be expressed as strings. infinispan.table.string.add=Adds a table used to store cache entries whose keys can be expressed as strings. infinispan.table.string.remove=Removes a table used to store cache entries whose keys can be expressed as strings. infinispan.table.prefix=The prefix for the database table name. infinispan.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.table.batch-size.deprecated=Deprecated. Use max-batch-size instead. infinispan.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.table.id-column=A database column to hold cache entry ids. infinispan.table.id-column.column.name=The name of the database column. infinispan.table.id-column.column.type=The type of the database column. infinispan.table.data-column=A database column to hold cache entry data. infinispan.table.data-column.column.name=The name of the database column. infinispan.table.data-column.column.type=The type of the database column. infinispan.table.segment-column=A database column to hold cache entry segment. infinispan.table.segment-column.column.name=The name of the database column. infinispan.table.segment-column.column.type=The type of the database column. infinispan.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.table.timestamp-column.column.name=The name of the database column. infinispan.table.timestamp-column.column.type=The type of the database column. # /subsystem=infinispan/cache-container=X/cache=Y/store=remote infinispan.store.remote=The cache remote store configuration. infinispan.store.remote.deprecated=Use HotRod store instead. infinispan.store.remote.cache=The name of the remote cache to use for this remote store. infinispan.store.remote.tcp-no-delay=A TCP_NODELAY value for remote cache communication. infinispan.store.remote.socket-timeout=A socket timeout for remote cache communication. # keycloak patch: begin infinispan.store.remote.connect-timeout=A connect timeout for remote cache communication. # keycloak patch: end infinispan.store.remote.remote-servers=A list of remote servers for this cache store. infinispan.store.remote.remote.servers.remote-server=A remote server, defined by its outbound socket binding. infinispan.store.remote.remote-servers.remote-server.outbound-socket-binding=An outbound socket binding for a remote server. infinispan.store.remote.add=Adds a remote cache store configuration element to the cache. infinispan.store.remote.remove=Removes a cache remote store configuration element from the cache. # /subsystem=infinispan/cache-container=X/cache=Y/store=hotrod infinispan.store.hotrod=HotRod-based store using Infinispan Server instance to store data. infinispan.store.hotrod.add=Adds HotRod store. infinispan.store.hotrod.remove=Removes HotRod store. infinispan.store.hotrod.cache-configuration=Name of the cache configuration template defined in Infinispan Server to create caches from. infinispan.store.hotrod.remote-cache-container=Reference to a container-managed remote-cache-container. infinispan.backup=A backup site to which to replicate this cache. infinispan.backup.add=Adds a backup site to this cache. infinispan.backup.remove=Removes a backup site from this cache. infinispan.backup.strategy=The backup strategy for this cache infinispan.backup.failure-policy=The policy to follow when connectivity to the backup site fails. infinispan.backup.enabled=Indicates whether or not this backup site is enabled. infinispan.backup.timeout=The timeout for replicating to the backup site. infinispan.backup.after-failures=Indicates the number of failures after which this backup site should go offline. infinispan.backup.min-wait=Indicates the minimum time (in milliseconds) to wait after the max number of failures is reached, after which this backup site should go offline. # cross-site backup operations infinispan.backup.site-status=Displays the current status of the backup site. infinispan.backup.bring-site-online=Re-enables a previously disabled backup site. infinispan.backup.take-site-offline=Disables backup to a remote site. infinispan.component.backup-for=A cache for which this cache acts as a backup (for use with cross site replication). infinispan.component.backup-for.deprecated=Deprecated. Backup designation must match the current cache name. infinispan.component.backup-for.add=Adds a backup designation for this cache. infinispan.component.backup-for.remove=Removes a backup designation for this cache. infinispan.component.backup-for.remote-cache=The name of the remote cache for which this cache acts as a backup. infinispan.component.backup-for.remote-cache.deprecated=This resource is deprecated. infinispan.component.backup-for.remote-site=The site of the remote cache for which this cache acts as a backup. infinispan.component.backup-for.remote-site.deprecated=This resource is deprecated. infinispan.component.backups=The remote backups for this cache. infinispan.component.backups.add=Adds remote backup support to this cache. infinispan.component.backups.remove=Removes remote backup support from this cache. infinispan.component.backups.backup=A remote backup. # /subsystem=infinispan/remote-cache-container=* infinispan.remote-cache-container=The configuration of a remote Infinispan cache container. infinispan.remote-cache-container.add=Add a remote cache container to the infinispan subsystem. infinispan.remote-cache-container.remove=Remove a cache container from the infinispan subsystem. infinispan.remote-cache-container.component=A configuration component of a remote cache container. infinispan.remote-cache-container.thread-pool=Defines thread pools for this remote cache container. infinispan.remote-cache-container.near-cache=Configures near caching. infinispan.remote-cache-container.connection-timeout=Defines the maximum socket connect timeout before giving up connecting to the server. infinispan.remote-cache-container.default-remote-cluster=Required default remote server cluster. infinispan.remote-cache-container.key-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing keys, to minimize array resizing. infinispan.remote-cache-container.max-retries=Sets the maximum number of retries for each request. A valid value should be greater or equals than 0. Zero means no retry will made in case of a network failure. infinispan.remote-cache-container.module=The module associated with this remote cache container's configuration. infinispan.remote-cache-container.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.remote-cache-container.modules=The set of modules associated with this remote cache container's configuration. infinispan.remote-cache-container.name=Uniquely identifies this remote cache container. infinispan.remote-cache-container.properties=A list of remote cache container properties. infinispan.remote-cache-container.protocol-version=This property defines the protocol version that this client should use. infinispan.remote-cache-container.socket-timeout=Enable or disable SO_TIMEOUT on socket connections to remote Hot Rod servers with the specified timeout, in milliseconds. A timeout of 0 is interpreted as an infinite timeout. infinispan.remote-cache-container.statistics-enabled=Enables statistics gathering for this remote cache. infinispan.remote-cache-container.tcp-no-delay=Enable or disable TCP_NODELAY on socket connections to remote Hot Rod servers. infinispan.remote-cache-container.tcp-keep-alive=Configures TCP Keepalive on the TCP stack. infinispan.remote-cache-container.value-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing values, to minimize array resizing. infinispan.remote-cache-container.active-connections=The number of active connections to the Infinispan server. infinispan.remote-cache-container.connections=The total number of connections to the Infinispan server. infinispan.remote-cache-container.idle-connections=The number of idle connections to the Infinispan server. infinispan.remote-cache-container.remote-cache=A remote cache runtime resource infinispan.remote-cache.average-read-time=The average read time, in milliseconds, for this remote cache. infinispan.remote-cache.average-remove-time=The average remove time, in milliseconds, for this remote cache. infinispan.remote-cache.average-write-time=The average write time, in milliseconds, to this remote cache. infinispan.remote-cache.near-cache-hits=The number of near-cache hits for this remote cache. infinispan.remote-cache.near-cache-invalidations=The number of near-cache invalidations for this remote cache. infinispan.remote-cache.near-cache-misses=The number of near-cache misses for this remote cache. infinispan.remote-cache.near-cache-size=The number of entries in the near-cache for this remote cache. infinispan.remote-cache.hits=The number of hits to this remote cache, excluding hits from the near-cache. infinispan.remote-cache.misses=The number of misses to this remote cache. infinispan.remote-cache.removes=The number of removes to this remote cache. infinispan.remote-cache.writes=The number of writes to this remote cache. infinispan.remote-cache.reset-statistics=Resets the statistics for this remote cache. infinispan.remote-cache.time-since-reset=The number of seconds since statistics were reset on this remote cache. # /subsystem=infinispan/remote-cache-container=X/thread-pool=async infinispan.thread-pool.async=Defines a thread pool used for asynchronous operations. infinispan.thread-pool.async.add=Adds thread pool configuration used for asynchronous operations. infinispan.thread-pool.async.remove=Removes thread pool configuration used for asynchronous operations. # /subsystem=infinispan/remote-cache-container=*/component=connection-pool infinispan.component.connection-pool=Configuration of the connection pool. infinispan.component.connection-pool.add=Adds configuration of the connection pool. infinispan.component.connection-pool.remove=Removes configuration of the connection pool. infinispan.component.connection-pool.exhausted-action=Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted. infinispan.component.connection-pool.max-active=Controls the maximum number of connections per server that are allocated (checked out to client threads, or idle in the pool) at one time. When non-positive, there is no limit to the number of connections per server. When maxActive is reached, the connection pool for that server is said to be exhausted. Value -1 means no limit. infinispan.component.connection-pool.max-wait=The amount of time in milliseconds to wait for a connection to become available when the exhausted action is ExhaustedAction.WAIT, after which a java.util.NoSuchElementException will be thrown. If a negative value is supplied, the pool will block indefinitely. infinispan.component.connection-pool.min-evictable-idle-time=Specifies the minimum amount of time that an connection may sit idle in the pool before it is eligible for eviction due to idle time. When non-positive, no connection will be dropped from the pool due to idle time alone. This setting has no effect unless timeBetweenEvictionRunsMillis > 0. infinispan.component.connection-pool.min-idle=Sets a target value for the minimum number of idle connections (per server) that should always be available. If this parameter is set to a positive number and timeBetweenEvictionRunsMillis > 0, each time the idle connection eviction thread runs, it will try to create enough idle instances so that there will be minIdle idle instances available for each server. # /subsystem=infinispan/remote-cache-container=*/near-cache=invalidation infinispan.near-cache.invalidation=Configures using near cache in invalidated mode. When entries are updated or removed server-side, invalidation messages will be sent to clients to remove them from the near cache. infinispan.near-cache.invalidation.add=Adds a near cache in invalidated mode. infinispan.near-cache.invalidation.remove=Removes near cache in invalidated mode. infinispan.near-cache.invalidation.deprecated=Deprecated. Near cache is enabled per remote cache. infinispan.near-cache.invalidation.max-entries=Defines the maximum number of elements to keep in the near cache. # /subsystem=infinispan/remote-cache-container=*/near-cache=none infinispan.near-cache.none=Disables near cache. infinispan.near-cache.none.add=Adds configuration that disables near cache. infinispan.near-cache.none.remove=Removes configuration that disables near cache. infinispan.near-cache.none.deprecated=Deprecated. Near cache is disabled per remote cache. # /subsystem=infinispan/remote-cache-container=*/component=remote-clusters/remote-cluster=* infinispan.remote-cluster=Configuration of a remote cluster. infinispan.remote-cluster.add=Adds a remote cluster configuration requiring socket-bindings configuration. infinispan.remote-cluster.remove=Removes this remote cluster configuration. infinispan.remote-cluster.socket-bindings=List of outbound-socket-bindings of Hot Rod servers to connect to. infinispan.remote-cluster.switch-cluster=Switch the cluster to which this HotRod client should communicate. Primary used to failback to the local site in the event of a site failover. # /subsystem=infinispan/remote-cache-container=*/component=security infinispan.component.security=Security configuration. infinispan.component.security.add=Adds security configuration. infinispan.component.security.remove=Removes security configuration. infinispan.component.security.ssl-context=Reference to the Elytron-managed SSLContext to be used for connecting to the remote cluster. ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreResourceDefinition.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import java.util.List; import java.util.concurrent.TimeUnit; import org.jboss.as.clustering.controller.CapabilityReference; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.clustering.controller.ResourceServiceConfigurator; import org.jboss.as.clustering.controller.SimpleResourceDescriptorConfigurator; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.ModelVersion; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.controller.client.helpers.MeasurementUnit; import org.jboss.as.controller.registry.AttributeAccess; import org.jboss.as.controller.transform.TransformationContext; import org.jboss.as.controller.transform.description.AttributeConverter; import org.jboss.as.controller.transform.description.ResourceTransformationDescriptionBuilder; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; /** * Resource description for the addressable resource and its alias * * /subsystem=infinispan/cache-container=X/cache=Y/store=remote * /subsystem=infinispan/cache-container=X/cache=Y/remote-store=REMOTE_STORE * * @author Richard Achmatowicz (c) 2011 Red Hat Inc. * @deprecated Use {@link org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition} instead. */ @Deprecated public class RemoteStoreResourceDefinition extends StoreResourceDefinition { static final PathElement LEGACY_PATH = PathElement.pathElement("remote-store", "REMOTE_STORE"); static final PathElement PATH = pathElement("remote"); enum Attribute implements org.jboss.as.clustering.controller.Attribute { CACHE("cache", ModelType.STRING, null), SOCKET_TIMEOUT("socket-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))), // keycloak patch: begin CONNECT_TIMEOUT("connect-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))), // keycloak patch: end TCP_NO_DELAY("tcp-no-delay", ModelType.BOOLEAN, ModelNode.TRUE), SOCKET_BINDINGS("remote-servers") ; private final AttributeDefinition definition; Attribute(String name, ModelType type, ModelNode defaultValue) { this.definition = new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(defaultValue == null) .setDefaultValue(defaultValue) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .setMeasurementUnit((type == ModelType.LONG) ? MeasurementUnit.MILLISECONDS : null) .build(); } Attribute(String name) { this.definition = new StringListAttributeDefinition.Builder(name) .setCapabilityReference(new CapabilityReference(Capability.PERSISTENCE, CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING)) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .setMinSize(1) .build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } } static void buildTransformation(ModelVersion version, ResourceTransformationDescriptionBuilder parent) { ResourceTransformationDescriptionBuilder builder = InfinispanModel.VERSION_4_0_0.requiresTransformation(version) ? parent.addChildRedirection(PATH, LEGACY_PATH) : parent.addChildResource(PATH); if (InfinispanModel.VERSION_4_0_0.requiresTransformation(version)) { builder.getAttributeBuilder() .setValueConverter(new AttributeConverter.DefaultAttributeConverter() { @Override protected void convertAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) { if (attributeValue.isDefined()) { List remoteServers = attributeValue.clone().asList(); ModelNode legacyListObject = new ModelNode(); for (ModelNode server : remoteServers) { ModelNode legacyListItem = new ModelNode(); legacyListItem.get("outbound-socket-binding").set(server); legacyListObject.add(legacyListItem); } attributeValue.set(legacyListObject); } } }, Attribute.SOCKET_BINDINGS.getDefinition()); } StoreResourceDefinition.buildTransformation(version, builder, PATH); } RemoteStoreResourceDefinition() { super(PATH, LEGACY_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(PATH, WILDCARD_PATH), new SimpleResourceDescriptorConfigurator<>(Attribute.class)); this.setDeprecated(InfinispanModel.VERSION_7_0_0.getVersion()); } @Override public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) { return new RemoteStoreServiceConfigurator(address); } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreServiceConfigurator.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2015, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CACHE; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CONNECT_TIMEOUT; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.network.OutboundSocketBinding; import org.jboss.dmr.ModelNode; import org.jboss.msc.service.ServiceBuilder; import org.wildfly.clustering.service.Dependency; import org.wildfly.clustering.service.ServiceConfigurator; import org.wildfly.clustering.service.ServiceSupplierDependency; import org.wildfly.clustering.service.SupplierDependency; /** * @author Paul Ferraro */ @Deprecated public class RemoteStoreServiceConfigurator extends StoreServiceConfigurator { private volatile List> bindings; private volatile String remoteCacheName; private volatile long socketTimeout; // keycloak patch: begin private volatile long connectTimeout; // keycloak patch: end private volatile boolean tcpNoDelay; public RemoteStoreServiceConfigurator(PathAddress address) { super(address, RemoteStoreConfigurationBuilder.class); } @Override public ServiceBuilder register(ServiceBuilder builder) { for (Dependency dependency : this.bindings) { dependency.register(builder); } return super.register(builder); } @Override public ServiceConfigurator configure(OperationContext context, ModelNode model) throws OperationFailedException { this.remoteCacheName = CACHE.resolveModelAttribute(context, model).asString(); this.socketTimeout = SOCKET_TIMEOUT.resolveModelAttribute(context, model).asLong(); this.connectTimeout = CONNECT_TIMEOUT.resolveModelAttribute(context, model).asLong(); this.tcpNoDelay = TCP_NO_DELAY.resolveModelAttribute(context, model).asBoolean(); List bindings = StringListAttributeDefinition.unwrapValue(context, SOCKET_BINDINGS.resolveModelAttribute(context, model)); this.bindings = new ArrayList<>(bindings.size()); for (String binding : bindings) { this.bindings.add(new ServiceSupplierDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, binding))); } return super.configure(context, model); } @Override public void accept(RemoteStoreConfigurationBuilder builder) { builder.segmented(false) .remoteCacheName(this.remoteCacheName) .socketTimeout(this.socketTimeout) // keycloak patch: begin .connectionTimeout(this.connectTimeout) // keycloak patch: end .tcpNoDelay(this.tcpNoDelay) ; for (Supplier bindingDependency : this.bindings) { OutboundSocketBinding binding = bindingDependency.get(); builder.addServer().host(binding.getUnresolvedDestinationAddress()).port(binding.getDestinationPort()); } } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/XMLAttribute.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2011, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import org.jboss.as.clustering.controller.Attribute; import org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.descriptions.ModelDescriptionConstants; /** * Enumerates the attributes used in the Infinispan subsystem schema. * @author Paul Ferraro * @author Richard Achmatowicz (c) 2011 RedHat Inc. * @author Tristan Tarrant */ public enum XMLAttribute { // must be first UNKNOWN((String) null), ACQUIRE_TIMEOUT(LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT), @Deprecated CAPACITY(OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY), ALIASES(CacheContainerResourceDefinition.ListAttribute.ALIASES), @Deprecated ASYNC_MARSHALLING(ClusteredCacheResourceDefinition.DeprecatedAttribute.ASYNC_MARSHALLING), BACKUP_FAILURE_POLICY(BackupResourceDefinition.Attribute.FAILURE_POLICY), @Deprecated BATCH_SIZE(TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE), @Deprecated BATCHING(CacheResourceDefinition.DeprecatedAttribute.BATCHING), BIAS_LIFESPAN(ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN), @Deprecated CACHE(RemoteStoreResourceDefinition.Attribute.CACHE), CAPACITY_FACTOR(DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR), CHANNEL(JGroupsTransportResourceDefinition.Attribute.CHANNEL), CHUNK_SIZE(StateTransferResourceDefinition.Attribute.CHUNK_SIZE), CLASS(CustomStoreResourceDefinition.Attribute.CLASS), @Deprecated CLUSTER(JGroupsTransportResourceDefinition.DeprecatedAttribute.CLUSTER), CONCURRENCY_LEVEL(LockingResourceDefinition.Attribute.CONCURRENCY), @Deprecated CONSISTENT_HASH_STRATEGY(SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY), CREATE_ON_START(TableResourceDefinition.Attribute.CREATE_ON_START), DATA_SOURCE(JDBCStoreResourceDefinition.Attribute.DATA_SOURCE), @Deprecated DATASOURCE(JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE), DEFAULT_CACHE(CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE), @Deprecated DEFAULT_CACHE_CONTAINER("default-cache-container"), DIALECT(JDBCStoreResourceDefinition.Attribute.DIALECT), DROP_ON_STOP(TableResourceDefinition.Attribute.DROP_ON_STOP), @Deprecated EAGER_LOCKING("eager-locking"), ENABLED(BackupResourceDefinition.Attribute.ENABLED), @Deprecated EVICTION_EXECUTOR(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION), @Deprecated EVICTION_TYPE(OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE), @Deprecated EXECUTOR(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT), FETCH_SIZE(TableResourceDefinition.Attribute.FETCH_SIZE), FETCH_STATE(StoreResourceDefinition.Attribute.FETCH_STATE), @Deprecated FLUSH_LOCK_TIMEOUT(StoreWriteBehindResourceDefinition.DeprecatedAttribute.FLUSH_LOCK_TIMEOUT), @Deprecated FLUSH_TIMEOUT("flush-timeout"), @Deprecated INDEXING(CacheResourceDefinition.DeprecatedAttribute.INDEXING), @Deprecated INDEX("index"), INTERVAL(ExpirationResourceDefinition.Attribute.INTERVAL), INVALIDATION_BATCH_SIZE(ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE), ISOLATION(LockingResourceDefinition.Attribute.ISOLATION), @Deprecated JNDI_NAME(CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME), KEEPALIVE_TIME(ThreadPoolResourceDefinition.values()[0].getKeepAliveTime()), L1_LIFESPAN(DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN), LIFESPAN(ExpirationResourceDefinition.Attribute.LIFESPAN), @Deprecated LISTENER_EXECUTOR(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER), LOCK_TIMEOUT(JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT), LOCKING(TransactionResourceDefinition.Attribute.LOCKING), MACHINE("machine"), MAX("max"), MAX_BATCH_SIZE(StoreResourceDefinition.Attribute.MAX_BATCH_SIZE), MAX_ENTRIES(HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES), MAX_IDLE(ExpirationResourceDefinition.Attribute.MAX_IDLE), MAX_THREADS(ThreadPoolResourceDefinition.values()[0].getMaxThreads()), MIN_THREADS(ThreadPoolResourceDefinition.values()[0].getMinThreads()), MODE(TransactionResourceDefinition.Attribute.MODE), MODIFICATION_QUEUE_SIZE(StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE), @Deprecated MODULE(CacheContainerResourceDefinition.DeprecatedAttribute.MODULE), MODULES(CacheContainerResourceDefinition.ListAttribute.MODULES), NAME(ModelDescriptionConstants.NAME), OUTBOUND_SOCKET_BINDING("outbound-socket-binding"), OWNERS(DistributedCacheResourceDefinition.Attribute.OWNERS), PASSIVATION(StoreResourceDefinition.Attribute.PASSIVATION), PATH(FileStoreResourceDefinition.Attribute.RELATIVE_PATH), PREFIX(StringTableResourceDefinition.Attribute.PREFIX), PRELOAD(StoreResourceDefinition.Attribute.PRELOAD), PURGE(StoreResourceDefinition.Attribute.PURGE), @Deprecated QUEUE_FLUSH_INTERVAL(ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL), @Deprecated QUEUE_SIZE(ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE), QUEUE_LENGTH(ThreadPoolResourceDefinition.values()[0].getQueueLength()), RACK("rack"), RELATIVE_TO(FileStoreResourceDefinition.Attribute.RELATIVE_TO), @Deprecated REMOTE_CACHE(BackupForResourceDefinition.Attribute.CACHE), @Deprecated REMOTE_SERVERS(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS), @Deprecated REMOTE_SITE(BackupForResourceDefinition.Attribute.SITE), REMOTE_TIMEOUT(ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT), @Deprecated REPLICATION_QUEUE_EXECUTOR(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE), SEGMENTS(SegmentedCacheResourceDefinition.Attribute.SEGMENTS), SHARED(StoreResourceDefinition.Attribute.SHARED), @Deprecated SHUTDOWN_TIMEOUT(StoreWriteBehindResourceDefinition.DeprecatedAttribute.SHUTDOWN_TIMEOUT), @Deprecated SINGLETON(StoreResourceDefinition.DeprecatedAttribute.SINGLETON), SITE("site"), SIZE(MemoryResourceDefinition.Attribute.SIZE), SIZE_UNIT(MemoryResourceDefinition.SharedAttribute.SIZE_UNIT), @Deprecated STACK(JGroupsTransportResourceDefinition.DeprecatedAttribute.STACK), @Deprecated START(CacheContainerResourceDefinition.DeprecatedAttribute.START), STATISTICS_ENABLED(CacheResourceDefinition.Attribute.STATISTICS_ENABLED), STOP_TIMEOUT(TransactionResourceDefinition.Attribute.STOP_TIMEOUT), STRATEGY(HeapMemoryResourceDefinition.DeprecatedAttribute.STRATEGY), STRIPING(LockingResourceDefinition.Attribute.STRIPING), TAKE_OFFLINE_AFTER_FAILURES(BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES), TAKE_OFFLINE_MIN_WAIT(BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT), @Deprecated THREAD_POOL_SIZE(StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE), TIMEOUT(StateTransferResourceDefinition.Attribute.TIMEOUT), TYPE(TableResourceDefinition.ColumnAttribute.ID.getColumnType()), @Deprecated VIRTUAL_NODES("virtual-nodes"), // hotrod store CACHE_CONFIGURATION(HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION), // remote-cache-container REMOTE_CACHE_CONTAINER(RemoteCacheContainerResourceDefinition.WILDCARD_PATH), CONNECTION_TIMEOUT(RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT), DEFAULT_REMOTE_CLUSTER(RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER), KEY_SIZE_ESTIMATE(RemoteCacheContainerResourceDefinition.Attribute.KEY_SIZE_ESTIMATE), MAX_RETRIES(RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES), PROTOCOL_VERSION(RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION), SOCKET_TIMEOUT(RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT), // keycloak patch: begin CONNECT_TIMEOUT(RemoteCacheContainerResourceDefinition.Attribute.CONNECT_TIMEOUT), // keycloak patch: end TCP_NO_DELAY(RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY), TCP_KEEP_ALIVE(RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE), VALUE_SIZE_ESTIMATE(RemoteCacheContainerResourceDefinition.Attribute.VALUE_SIZE_ESTIMATE), // remote-cache-container -> connection-pool EXHAUSTED_ACTION(ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION), MAX_ACTIVE(ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE), MAX_WAIT(ConnectionPoolResourceDefinition.Attribute.MAX_WAIT), MIN_EVICTABLE_IDLE_TIME(ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME), MIN_IDLE(ConnectionPoolResourceDefinition.Attribute.MIN_IDLE), // remote-cache-container -> remote-clusters SOCKET_BINDINGS(RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS), // remote-cache-container -> security SSL_CONTEXT(SecurityResourceDefinition.Attribute.SSL_CONTEXT), ; private final String name; XMLAttribute(Attribute attribute) { this(attribute.getDefinition().getXmlName()); } XMLAttribute(PathElement wildcardPath) { this(wildcardPath.getKey()); } XMLAttribute(String name) { this.name = name; } /** * Get the local name of this element. * * @return the local name */ public String getLocalName() { return this.name; } private static final Map attributes; static { final Map map = new HashMap<>(); for (XMLAttribute attribute : EnumSet.allOf(XMLAttribute.class)) { final String name = attribute.getLocalName(); if (name != null) { assert !map.containsKey(name) : attribute; map.put(name, attribute); } } attributes = map; } public static XMLAttribute forName(String localName) { final XMLAttribute attribute = attributes.get(localName); return attribute == null ? UNKNOWN : attribute; } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch/src/main/java/org/jboss/as/clustering/infinispan/subsystem/remote/RemoteCacheContainerResourceDefinition.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2016, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem.remote; import java.util.EnumSet; import java.util.function.UnaryOperator; import org.infinispan.client.hotrod.ProtocolVersion; import org.jboss.as.clustering.controller.CapabilityProvider; import org.jboss.as.clustering.controller.CapabilityReference; import org.jboss.as.clustering.controller.ChildResourceDefinition; import org.jboss.as.clustering.controller.ListAttributeTranslation; import org.jboss.as.clustering.controller.ManagementResourceRegistration; import org.jboss.as.clustering.controller.MetricHandler; import org.jboss.as.clustering.controller.PropertiesAttributeDefinition; import org.jboss.as.clustering.controller.ResourceDescriptor; import org.jboss.as.clustering.controller.ResourceServiceConfigurator; import org.jboss.as.clustering.controller.ResourceServiceConfiguratorFactory; import org.jboss.as.clustering.controller.ResourceServiceHandler; import org.jboss.as.clustering.controller.ServiceValueExecutorRegistry; import org.jboss.as.clustering.controller.SimpleResourceRegistration; import org.jboss.as.clustering.controller.UnaryRequirementCapability; import org.jboss.as.clustering.controller.transform.DiscardSingletonListAttributeChecker; import org.jboss.as.clustering.controller.transform.RejectNonSingletonListAttributeChecker; import org.jboss.as.clustering.controller.transform.SingletonListAttributeConverter; import org.jboss.as.clustering.controller.validation.EnumValidator; import org.jboss.as.clustering.controller.validation.ModuleIdentifierValidatorBuilder; import org.jboss.as.clustering.infinispan.subsystem.InfinispanExtension; import org.jboss.as.clustering.infinispan.subsystem.InfinispanModel; import org.jboss.as.clustering.infinispan.subsystem.ThreadPoolResourceDefinition; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.ModelVersion; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.controller.descriptions.ModelDescriptionConstants; import org.jboss.as.controller.registry.AttributeAccess; import org.jboss.as.controller.transform.description.AttributeConverter; import org.jboss.as.controller.transform.description.DiscardAttributeChecker; import org.jboss.as.controller.transform.description.DiscardAttributeChecker.DiscardAttributeValueChecker; import org.jboss.as.controller.transform.description.RejectAttributeChecker; import org.jboss.as.controller.transform.description.ResourceTransformationDescriptionBuilder; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; import org.wildfly.clustering.infinispan.client.InfinispanClientRequirement; import org.wildfly.clustering.infinispan.client.RemoteCacheContainer; import org.wildfly.clustering.service.UnaryRequirement; /** * /subsystem=infinispan/remote-cache-container=X * * @author Radoslav Husar */ public class RemoteCacheContainerResourceDefinition extends ChildResourceDefinition implements ResourceServiceConfiguratorFactory { public static final PathElement WILDCARD_PATH = pathElement(PathElement.WILDCARD_VALUE); public static PathElement pathElement(String containerName) { return PathElement.pathElement("remote-cache-container", containerName); } public enum Capability implements CapabilityProvider { CONTAINER(InfinispanClientRequirement.REMOTE_CONTAINER), CONFIGURATION(InfinispanClientRequirement.REMOTE_CONTAINER_CONFIGURATION), ; private final org.jboss.as.clustering.controller.Capability capability; Capability(UnaryRequirement requirement) { this.capability = new UnaryRequirementCapability(requirement); } @Override public org.jboss.as.clustering.controller.Capability getCapability() { return this.capability; } } public enum Attribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator { CONNECTION_TIMEOUT("connection-timeout", ModelType.INT, new ModelNode(60000)), DEFAULT_REMOTE_CLUSTER("default-remote-cluster", ModelType.STRING, null) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setAllowExpression(false).setCapabilityReference(new CapabilityReference(Capability.CONFIGURATION, RemoteClusterResourceDefinition.Requirement.REMOTE_CLUSTER, WILDCARD_PATH)); } }, KEY_SIZE_ESTIMATE("key-size-estimate", ModelType.INT, new ModelNode(64)), MAX_RETRIES("max-retries", ModelType.INT, new ModelNode(10)), PROPERTIES("properties"), PROTOCOL_VERSION("protocol-version", ModelType.STRING, new ModelNode(ProtocolVersion.PROTOCOL_VERSION_30.toString())) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setValidator(new EnumValidator<>(ProtocolVersion.class, EnumSet.complementOf(EnumSet.of(ProtocolVersion.PROTOCOL_VERSION_AUTO)))); } }, SOCKET_TIMEOUT("socket-timeout", ModelType.INT, new ModelNode(60000)), // keycloak patch: begin CONNECT_TIMEOUT("connect-timeout", ModelType.INT, new ModelNode(60000)), // keycloak patch: end STATISTICS_ENABLED(ModelDescriptionConstants.STATISTICS_ENABLED, ModelType.BOOLEAN, ModelNode.FALSE), TCP_NO_DELAY("tcp-no-delay", ModelType.BOOLEAN, ModelNode.TRUE), TCP_KEEP_ALIVE("tcp-keep-alive", ModelType.BOOLEAN, ModelNode.FALSE), VALUE_SIZE_ESTIMATE("value-size-estimate", ModelType.INT, new ModelNode(512)), ; private final AttributeDefinition definition; Attribute(String name) { this.definition = new PropertiesAttributeDefinition.Builder(name) .setAllowExpression(true) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .build(); } Attribute(String name, ModelType type, ModelNode defaultValue) { this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(defaultValue == null) .setDefaultValue(defaultValue) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) ).build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder; } } public enum ListAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator { MODULES("modules") { @Override public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) { return builder.setElementValidator(new ModuleIdentifierValidatorBuilder().configure(builder).build()); } }, ; private final AttributeDefinition definition; ListAttribute(String name) { this.definition = this.apply(new StringListAttributeDefinition.Builder(name) .setAllowExpression(true) .setRequired(false) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) ).build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } @Override public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) { return builder; } } public enum DeprecatedAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator { MODULE("module", ModelType.STRING, InfinispanModel.VERSION_14_0_0) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setFlags(AttributeAccess.Flag.ALIAS); } }, ; private final AttributeDefinition definition; DeprecatedAttribute(String name, ModelType type, InfinispanModel deprecation) { this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(false) .setDeprecated(deprecation.getVersion()) .setFlags(AttributeAccess.Flag.RESTART_NONE) ).build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder; } } @SuppressWarnings("deprecation") public static void buildTransformation(ModelVersion version, ResourceTransformationDescriptionBuilder parent) { if (InfinispanModel.VERSION_7_0_0.requiresTransformation(version)) { parent.rejectChildResource(RemoteCacheContainerResourceDefinition.WILDCARD_PATH); } else { ResourceTransformationDescriptionBuilder builder = parent.addChildResource(RemoteCacheContainerResourceDefinition.WILDCARD_PATH); if (InfinispanModel.VERSION_14_0_0.requiresTransformation(version)) { builder.getAttributeBuilder() .setValueConverter(new SingletonListAttributeConverter(ListAttribute.MODULES), DeprecatedAttribute.MODULE.getDefinition()) .setDiscard(DiscardSingletonListAttributeChecker.INSTANCE, ListAttribute.MODULES.getDefinition()) .addRejectCheck(RejectNonSingletonListAttributeChecker.INSTANCE, ListAttribute.MODULES.getDefinition()) .end(); } if (InfinispanModel.VERSION_13_0_0.requiresTransformation(version) || (InfinispanModel.VERSION_11_1_0.requiresTransformation(version) && !InfinispanModel.VERSION_12_0_0.requiresTransformation(version))) { builder.getAttributeBuilder() .setDiscard(DiscardAttributeChecker.UNDEFINED, Attribute.PROPERTIES.getDefinition()) .addRejectCheck(RejectAttributeChecker.DEFINED, Attribute.PROPERTIES.getDefinition()) .end(); } if (InfinispanModel.VERSION_12_0_0.requiresTransformation(version)) { builder.getAttributeBuilder().setValueConverter(AttributeConverter.DEFAULT_VALUE, Attribute.PROTOCOL_VERSION.getName()); } if (InfinispanModel.VERSION_11_0_0.requiresTransformation(version)) { builder.getAttributeBuilder() .setDiscard(new DiscardAttributeValueChecker(false, true, ModelNode.FALSE), Attribute.STATISTICS_ENABLED.getDefinition()) .addRejectCheck(RejectAttributeChecker.DEFINED, Attribute.STATISTICS_ENABLED.getDefinition()) .end(); } RemoteCacheContainerMetric.buildTransformation(version, builder); ConnectionPoolResourceDefinition.buildTransformation(version, builder); SecurityResourceDefinition.buildTransformation(version, builder); RemoteTransactionResourceDefinition.buildTransformation(version, builder); NoNearCacheResourceDefinition.buildTransformation(version, builder); InvalidationNearCacheResourceDefinition.buildTransformation(version, builder); RemoteClusterResourceDefinition.buildTransformation(version, builder); ThreadPoolResourceDefinition.CLIENT.buildTransformation(builder, version); RemoteCacheResourceDefinition.buildTransformation(version, builder); } } public RemoteCacheContainerResourceDefinition() { super(WILDCARD_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(WILDCARD_PATH)); } @SuppressWarnings("deprecation") @Override public ManagementResourceRegistration register(ManagementResourceRegistration parentRegistration) { ManagementResourceRegistration registration = parentRegistration.registerSubModel(this); ResourceDescriptor descriptor = new ResourceDescriptor(this.getResourceDescriptionResolver()) .addAttributes(Attribute.class) .addAttributes(ListAttribute.class) .addIgnoredAttributes(EnumSet.complementOf(EnumSet.of(DeprecatedAttribute.MODULE))) .addAttributeTranslation(DeprecatedAttribute.MODULE, new ListAttributeTranslation(ListAttribute.MODULES)) .addCapabilities(Capability.class) .addRequiredChildren(ConnectionPoolResourceDefinition.PATH, ThreadPoolResourceDefinition.CLIENT.getPathElement(), SecurityResourceDefinition.PATH, RemoteTransactionResourceDefinition.PATH) .addRequiredSingletonChildren(NoNearCacheResourceDefinition.PATH) .setResourceTransformation(RemoteCacheContainerResource::new) ; ServiceValueExecutorRegistry executors = new ServiceValueExecutorRegistry<>(); ResourceServiceHandler handler = new RemoteCacheContainerServiceHandler(this, executors); new SimpleResourceRegistration(descriptor, handler).register(registration); new ConnectionPoolResourceDefinition().register(registration); new RemoteClusterResourceDefinition(this, executors).register(registration); new SecurityResourceDefinition().register(registration); new RemoteTransactionResourceDefinition().register(registration); new InvalidationNearCacheResourceDefinition().register(registration); new NoNearCacheResourceDefinition().register(registration); ThreadPoolResourceDefinition.CLIENT.register(registration); if (registration.isRuntimeOnlyRegistrationValid()) { new MetricHandler<>(new RemoteCacheContainerMetricExecutor(executors), RemoteCacheContainerMetric.class).register(registration); new RemoteCacheResourceDefinition(executors).register(registration); } return registration; } @Override public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) { return new RemoteCacheContainerConfigurationServiceConfigurator(address); } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/pom.xml ================================================ 4.0.0 org.example wildfly-clustering-infinispan-extension-patch-25.0.x 1.0-SNAPSHOT UTF-8 11 11 25.0.1.Final 1.1.14.Final 12.1.7.Final 4.4.1.Final 1.0 4.1.68.Final 3.0.13 1.8 org.wildfly wildfly-clustering-infinispan-extension ${version.wildfly} org.wildfly wildfly-clustering-ee-infinispan ${version.wildfly} org.wildfly wildfly-clustering-jgroups-extension ${version.wildfly} org.wildfly wildfly-clustering-infinispan-client ${version.wildfly} org.wildfly wildfly-clustering-infinispan-marshalling ${version.wildfly} org.wildfly wildfly-clustering-infinispan-spi ${version.wildfly} org.wildfly wildfly-clustering-marshalling-jboss ${version.wildfly} org.wildfly wildfly-clustering-spi ${version.wildfly} org.wildfly wildfly-transactions ${version.wildfly} org.wildfly.transaction wildfly-transaction-client ${version.org.wildfly.transaction.client} org.infinispan infinispan-cachestore-jdbc ${version.org.infinispan} org.infinispan infinispan-cachestore-remote ${version.org.infinispan} org.infinispan.protostream protostream ${version.org.infinispan.protostream} net.jcip jcip-annotations ${version.net.jcip} io.netty netty-all ${version.io.netty} io.reactivex.rxjava3 rxjava ${version.io.reactivex.rxjava3} org.kohsuke.metainf-services metainf-services provided ${version.org.kohsuke.metainf-services} wildfly-clustering-infinispan-extension-patch src/main/java **/*.properties org.apache.maven.plugins maven-shade-plugin 3.2.4 package shade org.wildfly:wildfly-clustering-infinispan-extension org/jboss/as/** **/*.properties schema/* subsystem-templates/* META-INF/services/* org.wildfly:wildfly-clustering-infinispan-extension org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/InfinispanSubsystemXMLReader.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import static org.jboss.as.clustering.infinispan.InfinispanLogger.ROOT_LOGGER; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import org.jboss.as.clustering.controller.Attribute; import org.jboss.as.clustering.controller.Operations; import org.jboss.as.clustering.controller.ResourceDefinitionProvider; import org.jboss.as.clustering.infinispan.subsystem.TableResourceDefinition.ColumnAttribute; import org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.InvalidationNearCacheResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteTransactionResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition; import org.jboss.as.clustering.jgroups.subsystem.ChannelResourceDefinition; import org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemResourceDefinition; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.AttributeParser; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.operations.common.Util; import org.jboss.as.controller.parsing.Element; import org.jboss.as.controller.parsing.ParseUtils; import org.jboss.dmr.ModelNode; import org.jboss.staxmapper.XMLElementReader; import org.jboss.staxmapper.XMLExtendedStreamReader; /** * XML reader for the Infinispan subsystem. * * @author Paul Ferraro */ @SuppressWarnings({ "deprecation", "static-method" }) public class InfinispanSubsystemXMLReader implements XMLElementReader> { private final InfinispanSchema schema; InfinispanSubsystemXMLReader(InfinispanSchema schema) { this.schema = schema; } @Override public void readElement(XMLExtendedStreamReader reader, List result) throws XMLStreamException { Map operations = new LinkedHashMap<>(); PathAddress address = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case CACHE_CONTAINER: { this.parseContainer(reader, address, operations); break; } case REMOTE_CACHE_CONTAINER: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseRemoteContainer(reader, address, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } result.addAll(operations.values()); } private void parseContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = subsystemAddress.append(CacheContainerResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { ParseUtils.requireNoNamespaceAttribute(reader, i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case DEFAULT_CACHE: { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE); break; } case JNDI_NAME: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME); break; } case LISTENER_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.LISTENER); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER.getName()); break; } case EVICTION_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.EVICTION); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION.getName()); break; } case REPLICATION_QUEUE_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE.getName()); break; } case START: { if (this.schema.since(InfinispanSchema.VERSION_1_1) && !this.schema.since(InfinispanSchema.VERSION_3_0)) { ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); } else { throw ParseUtils.unexpectedAttribute(reader, i); } break; } case ALIASES: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.ALIASES); break; } } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } if (this.schema.since(InfinispanSchema.VERSION_1_3)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.MODULE); break; } } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_1_5)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.MODULES); break; } } case MARSHALLER: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.MARSHALLER); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } if (!this.schema.since(InfinispanSchema.VERSION_1_5)) { operation.get(CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true); } List aliases = new LinkedList<>(); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ALIAS: { if (InfinispanSchema.VERSION_1_0.since(this.schema)) { aliases.add(reader.getElementText()); break; } throw ParseUtils.unexpectedElement(reader); } case TRANSPORT: { this.parseTransport(reader, address, operations); break; } case LOCAL_CACHE: { this.parseLocalCache(reader, address, operations); break; } case INVALIDATION_CACHE: { this.parseInvalidationCache(reader, address, operations); break; } case REPLICATED_CACHE: { this.parseReplicatedCache(reader, address, operations); break; } case DISTRIBUTED_CACHE: { this.parseDistributedCache(reader, address, operations); break; } case EXPIRATION_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseScheduledThreadPool(ScheduledThreadPoolResourceDefinition.EXPIRATION, reader, address, operations); break; } } case ASYNC_OPERATIONS_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.ASYNC_OPERATIONS, reader, address, operations); break; } } case LISTENER_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.LISTENER, reader, address, operations); break; } } case PERSISTENCE_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { if (this.schema.since(InfinispanSchema.VERSION_7_0) && !this.schema.since(InfinispanSchema.VERSION_10_0)) { this.parseScheduledThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations); } else { this.parseThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations); } break; } } case REMOTE_COMMAND_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.REMOTE_COMMAND, reader, address, operations); break; } } case STATE_TRANSFER_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.STATE_TRANSFER, reader, address, operations); break; } } case TRANSPORT_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.TRANSPORT, reader, address, operations); break; } } case SCATTERED_CACHE: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseScatteredCache(reader, address, operations); break; } } case BLOCKING_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.BLOCKING, reader, address, operations); break; } } case NON_BLOCKING_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.NON_BLOCKING, reader, address, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } if (!aliases.isEmpty()) { // Adapt aliases parsed from legacy schema into format expected by the current attribute parser setAttribute(reader, String.join(" ", aliases), operation, CacheContainerResourceDefinition.ListAttribute.ALIASES); } } private void parseTransport(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(JGroupsTransportResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(containerAddress.append(TransportResourceDefinition.WILDCARD_PATH), operation); String stack = null; String cluster = null; for (int i = 0; i < reader.getAttributeCount(); i++) { String value = reader.getAttributeValue(i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STACK: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } stack = value; break; } case EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT); ROOT_LOGGER.executorIgnored(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT.getName()); break; } case LOCK_TIMEOUT: { readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT); break; } case SITE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.SITE.getLocalName()); break; } case RACK: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.RACK.getLocalName()); break; } case MACHINE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.MACHINE.getLocalName()); break; } case CLUSTER: { if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_3_0)) { cluster = value; break; } throw ParseUtils.unexpectedAttribute(reader, i); } case CHANNEL: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } if (!this.schema.since(InfinispanSchema.VERSION_3_0)) { // We need to create a corresponding channel add operation String channel = (cluster != null) ? cluster : ("ee-" + containerAddress.getLastElement().getValue()); setAttribute(reader, channel, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL); PathAddress channelAddress = PathAddress.pathAddress(JGroupsSubsystemResourceDefinition.PATH, ChannelResourceDefinition.pathElement(channel)); ModelNode channelOperation = Util.createAddOperation(channelAddress); if (stack != null) { setAttribute(reader, stack, channelOperation, ChannelResourceDefinition.Attribute.STACK); } operations.put(channelAddress, channelOperation); } ParseUtils.requireNoContent(reader); } private void parseLocalCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(LocalCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseCacheElement(reader, address, operations); } } private void parseReplicatedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(ReplicatedCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseClusteredCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseSharedStateCacheElement(reader, address, operations); } } private void parseScatteredCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(ScatteredCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case BIAS_LIFESPAN: { readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN); break; } case INVALIDATION_BATCH_SIZE: { readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE); break; } default: { this.parseSegmentedCacheAttribute(reader, i, address, operations); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseSharedStateCacheElement(reader, address, operations); } } private void parseDistributedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(DistributedCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case OWNERS: { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.OWNERS); break; } case L1_LIFESPAN: { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN); break; } case VIRTUAL_NODES: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { throw ParseUtils.unexpectedAttribute(reader, i); } // AS7-5753: convert any non-expression virtual nodes value to a segments value, String virtualNodes = readAttribute(reader, i, SegmentedCacheResourceDefinition.Attribute.SEGMENTS).asString(); String segments = SegmentsAndVirtualNodeConverter.virtualNodesToSegments(virtualNodes); setAttribute(reader, segments, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS); break; } case CAPACITY_FACTOR: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR); break; } } default: { this.parseSegmentedCacheAttribute(reader, i, address, operations); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { this.parseSharedStateCacheElement(reader, address, operations); } else { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REHASHING: { this.parseStateTransfer(reader, address, operations); break; } default: { this.parseCacheElement(reader, address, operations); } } } } } private void parseInvalidationCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(InvalidationCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseClusteredCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseCacheElement(reader, address, operations); } } private void parseCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case NAME: { // Already read break; } case START: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case BATCHING: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } PathAddress transactionAddress = address.append(TransactionResourceDefinition.PATH); ModelNode transactionOperation = Util.createAddOperation(transactionAddress); transactionOperation.get(TransactionResourceDefinition.Attribute.MODE.getName()).set(new ModelNode(TransactionMode.BATCH.name())); operations.put(transactionAddress, transactionOperation); break; } case INDEXING: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { throw ParseUtils.unexpectedAttribute(reader, index); } readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING); break; } case JNDI_NAME: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.JNDI_NAME); break; } } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } if (this.schema.since(InfinispanSchema.VERSION_1_3)) { readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.MODULE); break; } } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_1_5)) { readAttribute(reader, index, operation, CacheResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, index, operation, CacheResourceDefinition.ListAttribute.MODULES); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } if (!this.schema.since(InfinispanSchema.VERSION_1_5)) { // We need to explicitly enable statistics (to reproduce old behavior), since the new attribute defaults to false. operation.get(CacheResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true); } } private void parseSegmentedCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SEGMENTS: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS); break; } } case CONSISTENT_HASH_STRATEGY: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY); break; } } default: { this.parseClusteredCacheAttribute(reader, index, address, operations); } } } private void parseClusteredCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case MODE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } break; } case QUEUE_SIZE: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE); break; } case QUEUE_FLUSH_INTERVAL: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL); break; } case REMOTE_TIMEOUT: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT); break; } case ASYNC_MARSHALLING: { if (!this.schema.since(InfinispanSchema.VERSION_1_2) && this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { this.parseCacheAttribute(reader, index, address, operations); } } } private void parseCacheElement(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case EVICTION: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } this.parseEviction(reader, cacheAddress, operations); break; } case EXPIRATION: { this.parseExpiration(reader, cacheAddress, operations); break; } case LOCKING: { this.parseLocking(reader, cacheAddress, operations); break; } case TRANSACTION: { this.parseTransaction(reader, cacheAddress, operations); break; } case STORE: { this.parseCustomStore(reader, cacheAddress, operations); break; } case FILE_STORE: { this.parseFileStore(reader, cacheAddress, operations); break; } case REMOTE_STORE: { this.parseRemoteStore(reader, cacheAddress, operations); break; } case HOTROD_STORE: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseHotRodStore(reader, cacheAddress, operations); break; } } case JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseJDBCStore(reader, cacheAddress, operations); } else { this.parseLegacyJDBCStore(reader, cacheAddress, operations); } break; } case STRING_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseStringKeyedJDBCStore(reader, cacheAddress, operations); break; } } case BINARY_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseBinaryKeyedJDBCStore(reader, cacheAddress, operations); break; } } case MIXED_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseMixedKeyedJDBCStore(reader, cacheAddress, operations); break; } } case INDEXING: { if (this.schema.since(InfinispanSchema.VERSION_1_4) && !this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseIndexing(reader, cacheAddress, operations); break; } } case OBJECT_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseHeapMemory(reader, cacheAddress, operations); break; } } case BINARY_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseBinaryMemory(reader, cacheAddress, operations); break; } } case OFF_HEAP_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseOffHeapMemory(reader, cacheAddress, operations); break; } } case HEAP_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseHeapMemory(reader, cacheAddress, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } private void parseSharedStateCacheElement(XMLExtendedStreamReader reader, PathAddress address, Map operations) throws XMLStreamException { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case STATE_TRANSFER: { this.parseStateTransfer(reader, address, operations); break; } case BACKUPS: { if (this.schema.since(InfinispanSchema.VERSION_2_0)) { this.parseBackups(reader, address, operations); break; } } case BACKUP_FOR: { if (this.schema.since(InfinispanSchema.VERSION_2_0) && !this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseBackupFor(reader, address, operations); break; } throw ParseUtils.unexpectedElement(reader); } case PARTITION_HANDLING: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parsePartitionHandling(reader, address, operations); break; } } default: { this.parseCacheElement(reader, address, operations); } } } private void parsePartitionHandling(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(PartitionHandlingResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ENABLED: { readAttribute(reader, i, operation, PartitionHandlingResourceDefinition.Attribute.ENABLED); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseStateTransfer(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(StateTransferResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case TIMEOUT: { readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.TIMEOUT); break; } case FLUSH_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case CHUNK_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.CHUNK_SIZE); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseBackups(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BackupsResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BACKUP: { this.parseBackup(reader, address, operations); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseBackup(XMLExtendedStreamReader reader, PathAddress backupsAddress, Map operations) throws XMLStreamException { String site = require(reader, XMLAttribute.SITE); PathAddress address = backupsAddress.append(BackupResourceDefinition.pathElement(site)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SITE: { // Already parsed break; } case STRATEGY: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.STRATEGY); break; } case BACKUP_FAILURE_POLICY: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.FAILURE_POLICY); break; } case TIMEOUT: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.TIMEOUT); break; } case ENABLED: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.ENABLED); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case TAKE_OFFLINE: { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case TAKE_OFFLINE_AFTER_FAILURES: { readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES); break; } case TAKE_OFFLINE_MIN_WAIT: { readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseBackupFor(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BackupForResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case REMOTE_CACHE: { readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.CACHE); break; } case REMOTE_SITE: { readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.SITE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseLocking(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(LockingResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ISOLATION: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ISOLATION); break; } case STRIPING: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.STRIPING); break; } case ACQUIRE_TIMEOUT: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT); break; } case CONCURRENCY_LEVEL: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.CONCURRENCY); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseTransaction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(TransactionResourceDefinition.PATH); ModelNode operation = operations.get(address); if (operation == null) { operation = Util.createAddOperation(address); operations.put(address, operation); } for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STOP_TIMEOUT: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.STOP_TIMEOUT); break; } case MODE: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.MODE); break; } case LOCKING: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.LOCKING); break; } case EAGER_LOCKING: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case COMPLETE_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.COMPLETE_TIMEOUT); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseEviction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STRATEGY: { ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case MAX_ENTRIES: { readAttribute(reader, i, operation, HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES); break; } case INTERVAL: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseExpiration(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(ExpirationResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_IDLE: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.MAX_IDLE); break; } case LIFESPAN: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.LIFESPAN); break; } case INTERVAL: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.INTERVAL); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseIndexing(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { ModelNode operation = operations.get(cacheAddress); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case INDEX: { readAttribute(reader, i, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { Element element = Element.forName(reader.getLocalName()); switch (element) { case PROPERTY: { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING_PROPERTIES); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SIZE_UNIT: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { readAttribute(reader, i, operation, HeapMemoryResourceDefinition.Attribute.SIZE_UNIT); break; } } default: { this.parseMemoryAttribute(reader, i, operation); } } } ParseUtils.requireNoContent(reader); } private void parseBinaryMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.BINARY_PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseBinaryMemoryAttribute(reader, i, operation); } ParseUtils.requireNoContent(reader); } private void parseOffHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CAPACITY: { readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY); break; } case SIZE_UNIT: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.Attribute.SIZE_UNIT); break; } } default: { this.parseBinaryMemoryAttribute(reader, i, operation); } } } ParseUtils.requireNoContent(reader); } private void parseBinaryMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case EVICTION_TYPE: { readAttribute(reader, index, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE); break; } default: { this.parseMemoryAttribute(reader, index, operation); } } } private void parseMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SIZE: { readAttribute(reader, index, operation, MemoryResourceDefinition.Attribute.SIZE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseCustomStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(CustomStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CLASS: { readAttribute(reader, i, operation, CustomStoreResourceDefinition.Attribute.CLASS); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } if (!operation.hasDefined(CustomStoreResourceDefinition.Attribute.CLASS.getName())) { throw ParseUtils.missingRequired(reader, EnumSet.of(XMLAttribute.CLASS)); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseFileStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(FileStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case RELATIVE_TO: { readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_TO); break; } case PATH: { readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_PATH); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseRemoteStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(RemoteStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CACHE: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CACHE); break; } case SOCKET_TIMEOUT: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT); break; } // keycloak patch: begin case CONNECTION_TIMEOUT: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT); break; } // keycloak patch: end case TCP_NO_DELAY: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY); break; } case REMOTE_SERVERS: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS); break; } } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REMOTE_SERVER: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedElement(reader); } for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case OUTBOUND_SOCKET_BINDING: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); break; } default: { this.parseStoreElement(reader, address, operations); } } } if (!operation.hasDefined(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS.getName())) { throw ParseUtils.missingRequired(reader, Collections.singleton(XMLAttribute.REMOTE_SERVERS.getLocalName())); } } private void parseHotRodStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HotRodStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CACHE_CONFIGURATION: { readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION); break; } case REMOTE_CACHE_CONTAINER: { readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.REMOTE_CACHE_CONTAINER); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(JDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseLegacyJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { // We don't know the path yet PathAddress address = null; PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ENTRY_TABLE: { if (address != null) { this.removeStoreOperations(address, operations); } address = cacheAddress.append((address == null) ? StringKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH); Operations.setPathAddress(operation, address); ModelNode binaryTableOperation = operations.get(operationKey.append(BinaryTableResourceDefinition.PATH)); if (binaryTableOperation != null) { // Fix address of binary table operation Operations.setPathAddress(binaryTableOperation, address.append(BinaryTableResourceDefinition.PATH)); } this.parseJDBCStoreStringTable(reader, address, operations); break; } case BUCKET_TABLE: { if (address != null) { this.removeStoreOperations(address, operations); } address = cacheAddress.append((address == null) ? BinaryKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH); Operations.setPathAddress(operation, address); ModelNode stringTableOperation = operations.get(operationKey.append(StringTableResourceDefinition.PATH)); if (stringTableOperation != null) { // Fix address of string table operation Operations.setPathAddress(stringTableOperation, address.append(StringTableResourceDefinition.PATH)); } this.parseJDBCStoreBinaryTable(reader, address, operations); break; } default: { if (address == null) { throw ParseUtils.missingOneOf(reader, EnumSet.of(XMLElement.ENTRY_TABLE, XMLElement.BUCKET_TABLE)); } this.parseStoreElement(reader, address, operations); } } } } private void parseBinaryKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BinaryKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BINARY_KEYED_TABLE: { this.parseJDBCStoreBinaryTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseStringKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(StringKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case STRING_KEYED_TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseMixedKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(MixedKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BINARY_KEYED_TABLE: { this.parseJDBCStoreBinaryTable(reader, address, operations); break; } case STRING_KEYED_TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseJDBCStoreAttributes(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case DATASOURCE: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE); break; } case DIALECT: { if (this.schema.since(InfinispanSchema.VERSION_2_0)) { readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DIALECT); break; } } case DATA_SOURCE: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DATA_SOURCE); break; } } default: { this.parseStoreAttribute(reader, i, operation); } } } Attribute requiredAttribute = this.schema.since(InfinispanSchema.VERSION_4_0) ? JDBCStoreResourceDefinition.Attribute.DATA_SOURCE : JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE; if (!operation.hasDefined(requiredAttribute.getName())) { throw ParseUtils.missingRequired(reader, requiredAttribute.getName()); } } private void parseJDBCStoreBinaryTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(BinaryTableResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(BinaryTableResourceDefinition.PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case PREFIX: { readAttribute(reader, i, operation, BinaryTableResourceDefinition.Attribute.PREFIX); break; } default: { this.parseJDBCStoreTableAttribute(reader, i, operation); } } } this.parseJDBCStoreTableElements(reader, operation); } private void parseJDBCStoreStringTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(StringTableResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(StringTableResourceDefinition.PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case PREFIX: { readAttribute(reader, i, operation, StringTableResourceDefinition.Attribute.PREFIX); break; } default: { this.parseJDBCStoreTableAttribute(reader, i, operation); } } } this.parseJDBCStoreTableElements(reader, operation); } private void parseJDBCStoreTableAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case FETCH_SIZE: { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.FETCH_SIZE); break; } case BATCH_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } readAttribute(reader, index, operation, TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE); break; } case CREATE_ON_START: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.CREATE_ON_START); break; } } case DROP_ON_STOP: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.DROP_ON_STOP); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseJDBCStoreTableElements(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException { while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ID_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.ID, operation.get(TableResourceDefinition.ColumnAttribute.ID.getName()).setEmptyObject()); break; } case DATA_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.DATA, operation.get(TableResourceDefinition.ColumnAttribute.DATA.getName()).setEmptyObject()); break; } case TIMESTAMP_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.TIMESTAMP, operation.get(TableResourceDefinition.ColumnAttribute.TIMESTAMP.getName()).setEmptyObject()); break; } case SEGMENT_COLUMN: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { this.parseJDBCStoreColumn(reader, ColumnAttribute.SEGMENT, operation.get(TableResourceDefinition.ColumnAttribute.SEGMENT.getName()).setEmptyObject()); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseJDBCStoreColumn(XMLExtendedStreamReader reader, ColumnAttribute columnAttribute, ModelNode column) throws XMLStreamException { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { readAttribute(reader, i, column, columnAttribute.getColumnName()); break; } case TYPE: { readAttribute(reader, i, column, columnAttribute.getColumnType()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void removeStoreOperations(PathAddress storeAddress, Map operations) { operations.remove(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH)); } private void parseStoreAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SHARED: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.SHARED); break; } case PRELOAD: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PRELOAD); break; } case PASSIVATION: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PASSIVATION); break; } case FETCH_STATE: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.FETCH_STATE); break; } case PURGE: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PURGE); break; } case SINGLETON: { readAttribute(reader, index, operation, StoreResourceDefinition.DeprecatedAttribute.SINGLETON); break; } case MAX_BATCH_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.MAX_BATCH_SIZE); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseStoreElement(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { ModelNode operation = operations.get(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH)); XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case PROPERTY: { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, StoreResourceDefinition.Attribute.PROPERTIES); break; } case WRITE_BEHIND: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseStoreWriteBehind(reader, storeAddress, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } private void parseStoreWriteBehind(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(StoreWriteBehindResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case FLUSH_LOCK_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case MODIFICATION_QUEUE_SIZE: { readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE); break; } case SHUTDOWN_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case THREAD_POOL_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private

void parseThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map operations) throws XMLStreamException { PathAddress address = parentAddress.append(pool.getPathElement()); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MIN_THREADS: { if (pool.getMinThreads() != null) { readAttribute(reader, i, operation, pool.getMinThreads()); } break; } case MAX_THREADS: { readAttribute(reader, i, operation, pool.getMaxThreads()); break; } case QUEUE_LENGTH: { if (pool.getQueueLength() != null) { readAttribute(reader, i, operation, pool.getQueueLength()); } break; } case KEEPALIVE_TIME: { readAttribute(reader, i, operation, pool.getKeepAliveTime()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private

void parseScheduledThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map operations) throws XMLStreamException { PathAddress address = parentAddress.append(pool.getPathElement()); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_THREADS: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, pool.getMinThreads()); break; } case KEEPALIVE_TIME: { readAttribute(reader, i, operation, pool.getKeepAliveTime()); break; } case MIN_THREADS: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { readAttribute(reader, i, operation, pool.getMinThreads()); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = subsystemAddress.append(RemoteCacheContainerResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { ParseUtils.requireNoNamespaceAttribute(reader, i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case CONNECTION_TIMEOUT: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT); break; } case DEFAULT_REMOTE_CLUSTER: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER); break; } case KEY_SIZE_ESTIMATE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.KEY_SIZE_ESTIMATE); break; } case MAX_RETRIES: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES); break; } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.MODULE); break; } case PROTOCOL_VERSION: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION); break; } case SOCKET_TIMEOUT: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT); break; } case TCP_NO_DELAY: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY); break; } case TCP_KEEP_ALIVE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE); break; } case VALUE_SIZE_ESTIMATE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.VALUE_SIZE_ESTIMATE); break; } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.ListAttribute.MODULES); break; } } case MARSHALLER: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MARSHALLER); break; } } case TRANSACTION_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TRANSACTION_TIMEOUT); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ASYNC_THREAD_POOL: { this.parseThreadPool(ThreadPoolResourceDefinition.CLIENT, reader, address, operations); break; } case CONNECTION_POOL: { this.parseConnectionPool(reader, address, operations); break; } case INVALIDATION_NEAR_CACHE: { this.parseInvalidationNearCache(reader, address, operations); break; } case REMOTE_CLUSTERS: { this.parseRemoteClusters(reader, address, operations); break; } case SECURITY: { this.parseRemoteCacheContainerSecurity(reader, address, operations); break; } case TRANSACTION: { if (this.schema.since(InfinispanSchema.VERSION_8_0)) { this.parseRemoteTransaction(reader, address, operations); break; } } case PROPERTY: { if (this.schema.since(InfinispanSchema.VERSION_11_0) || (this.schema.since(InfinispanSchema.VERSION_9_1) && !this.schema.since(InfinispanSchema.VERSION_10_0))) { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, RemoteCacheContainerResourceDefinition.Attribute.PROPERTIES); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseInvalidationNearCache(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(InvalidationNearCacheResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_ENTRIES: { readAttribute(reader, i, operation, InvalidationNearCacheResourceDefinition.Attribute.MAX_ENTRIES); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseConnectionPool(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(ConnectionPoolResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case EXHAUSTED_ACTION: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION); break; } case MAX_ACTIVE: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE); break; } case MAX_WAIT: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_WAIT); break; } case MIN_EVICTABLE_IDLE_TIME: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME); break; } case MIN_IDLE: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_IDLE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteClusters(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { ParseUtils.requireNoAttributes(reader); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REMOTE_CLUSTER: { this.parseRemoteCluster(reader, containerAddress, operations); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseRemoteCluster(XMLExtendedStreamReader reader, PathAddress clustersAddress, Map operations) throws XMLStreamException { String remoteCluster = require(reader, XMLAttribute.NAME); PathAddress address = clustersAddress.append(RemoteClusterResourceDefinition.pathElement(remoteCluster)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case SOCKET_BINDINGS: { readAttribute(reader, i, operation, RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteCacheContainerSecurity(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(SecurityResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SSL_CONTEXT: { readAttribute(reader, i, operation, SecurityResourceDefinition.Attribute.SSL_CONTEXT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteTransaction(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(RemoteTransactionResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MODE: { readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.MODE); break; } case TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.TIMEOUT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private static String require(XMLExtendedStreamReader reader, XMLAttribute attribute) throws XMLStreamException { String value = reader.getAttributeValue(null, attribute.getLocalName()); if (value == null) { throw ParseUtils.missingRequired(reader, attribute.getLocalName()); } return value; } private static ModelNode readAttribute(XMLExtendedStreamReader reader, int index, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); return definition.getParser().parse(definition, reader.getAttributeValue(index), reader); } private static void readAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation, Attribute attribute) throws XMLStreamException { setAttribute(reader, reader.getAttributeValue(index), operation, attribute); } private static void setAttribute(XMLExtendedStreamReader reader, String value, ModelNode operation, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); definition.getParser().parseAndSetParameter(definition, value, operation, reader); } private static void readElement(XMLExtendedStreamReader reader, ModelNode operation, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); AttributeParser parser = definition.getParser(); if (parser.isParseAsElement()) { parser.parseElement(definition, reader, operation); } else { parser.parseAndSetParameter(definition, reader.getElementText(), operation, reader); } } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties ================================================ # subsystem resource infinispan=The configuration of the infinispan subsystem. infinispan.add=Add the infinispan subsystem. infinispan.describe=Describe the infinispan subsystem infinispan.remove=Remove the infinispan subsystem # cache container resource infinispan.cache-container=The configuration of an infinispan cache container infinispan.cache-container.default-cache=The default infinispan cache infinispan.cache-container.listener-executor=The executor used for the replication queue infinispan.cache-container.listener-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.eviction-executor=The scheduled executor used for eviction infinispan.cache-container.eviction-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.replication-queue-executor=The executor used for asynchronous cache operations infinispan.cache-container.replication-queue-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.jndi-name=The jndi name to which to bind this cache container infinispan.cache-container.jndi-name.deprecated=Deprecated. Will be ignored. infinispan.cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries. infinispan.cache-container.module=The module associated with this cache container's configuration. infinispan.cache-container.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.cache-container.modules=The set of modules associated with this cache container's configuration. infinispan.cache-container.start=The cache container start mode, which can be EAGER (immediate start) or LAZY (on-demand start). infinispan.cache-container.start.deprecated=Deprecated. Future releases will only support LAZY mode. infinispan.cache-container.statistics-enabled=If enabled, statistics will be collected for this cache container infinispan.cache-container.thread-pool=Defines thread pools for this cache container infinispan.cache-container.cache=The list of caches available to this cache container infinispan.cache-container.singleton=A set of single-instance configuration elements of the cache container. infinispan.cache-container.aliases=The list of aliases for this cache container infinispan.cache-container.add-alias=Add an alias for this cache container infinispan.cache-container.add-alias.name=The name of the alias to add to this cache container infinispan.cache-container.add-alias.deprecated=Deprecated. Use list-add operation instead. infinispan.cache-container.remove-alias=Remove an alias for this cache container infinispan.cache-container.remove-alias.name=The name of the alias to remove from this cache container infinispan.cache-container.remove-alias.deprecated=Deprecated. Use list-remove operation instead. infinispan.cache-container.add=Add a cache container to the infinispan subsystem infinispan.cache-container.remove=Remove a cache container from the infinispan subsystem # cache container read-only metrics infinispan.cache-container.cache-manager-status=The status of the cache manager component. May return null if the cache manager is not started. infinispan.cache-container.cache-manager-status.deprecated=Deprecated. Always returns RUNNING. infinispan.cache-container.is-coordinator=Set to true if this node is the cluster's coordinator. May return null if the cache manager is not started. infinispan.cache-container.coordinator-address=The logical address of the cluster's coordinator. May return null if the cache manager is not started. infinispan.cache-container.local-address=The local address of the node. May return null if the cache manager is not started. infinispan.cache-container.cluster-name=The name of the cluster this node belongs to. May return null if the cache manager is not started. # cache container children infinispan.cache-container.transport=A transport child of the cache container. infinispan.cache-container.local-cache=A local cache child of the cache container. infinispan.cache-container.invalidation-cache=An invalidation cache child of the cache container. infinispan.cache-container.replicated-cache=A replicated cache child of the cache container. infinispan.cache-container.distributed-cache=A distributed cache child of the cache container. # thread-pool resources infinispan.thread-pool.deprecated=This thread pool is deprecated and will be ignored. infinispan.thread-pool.async-operations=Defines a thread pool used for asynchronous operations. infinispan.thread-pool.listener=Defines a thread pool used for asynchronous cache listener notifications. infinispan.thread-pool.persistence=Defines a thread pool used for interacting with the persistent store. infinispan.thread-pool.remote-command=Defines a thread pool used to execute remote commands. infinispan.thread-pool.state-transfer=Defines a thread pool used for for state transfer. infinispan.thread-pool.state-transfer.deprecated=Deprecated. Has no effect. infinispan.thread-pool.transport=Defines a thread pool used for asynchronous transport communication. infinispan.thread-pool.expiration=Defines a thread pool used for for evictions. infinispan.thread-pool.blocking=Defines a thread pool used for for blocking operations. infinispan.thread-pool.non-blocking=Defines a thread pool used for for non-blocking operations. infinispan.thread-pool.add=Adds a thread pool executor. infinispan.thread-pool.remove=Removes a thread pool executor. infinispan.thread-pool.min-threads=The core thread pool size which is smaller than the maximum pool size. If undefined, the core thread pool size is the same as the maximum thread pool size. infinispan.thread-pool.min-threads.deprecated=Deprecated. Has no effect. infinispan.thread-pool.max-threads=The maximum thread pool size. infinispan.thread-pool.max-threads.deprecated=Deprecated. Use min-threads instead. infinispan.thread-pool.queue-length=The queue length. infinispan.thread-pool.queue-length.deprecated=Deprecated. Has no effect. infinispan.thread-pool.keepalive-time=Used to specify the amount of milliseconds that pool threads should be kept running when idle; if not specified, threads will run until the executor is shut down. # transport resource infinispan.transport.jgroups=The description of the transport used by this cache container infinispan.transport.jgroups.add=Add the transport to the cache container infinispan.transport.jgroups.remove=Remove the transport from the cache container infinispan.transport.jgroups.channel=The channel of this cache container's transport. infinispan.transport.jgroups.cluster=The name of the group communication cluster infinispan.transport.jgroups.cluster.deprecated=Deprecated. The cluster used by the transport of this cache container is configured via the JGroups subsystem. infinispan.transport.jgroups.executor=The executor to use for the transport infinispan.transport.jgroups.executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.transport.jgroups.lock-timeout=The timeout for locks for the transport infinispan.transport.jgroups.machine=A machine identifier for the transport infinispan.transport.jgroups.rack=A rack identifier for the transport infinispan.transport.jgroups.site=A site identifier for the transport infinispan.transport.jgroups.stack=The jgroups stack to use for the transport infinispan.transport.jgroups.stack.deprecated=Deprecated. The protocol stack used by the transport of this cache container is configured via the JGroups subsystem. infinispan.transport.none=A local-only transport used by this cache-container infinispan.transport.none.add=Adds a local transport to this cache container infinispan.transport.none.remove=Removes a local transport from this cache container # (hierarchical) cache resource infinispan.cache.start=The cache start mode, which can be EAGER (immediate start) or LAZY (on-demand start). infinispan.cache.start.deprecated=Deprecated. Only LAZY mode is supported. infinispan.cache.statistics-enabled=If enabled, statistics will be collected for this cache infinispan.cache.batching=If enabled, the invocation batching API will be made available for this cache. infinispan.cache.batching.deprecated=Deprecated. Replaced by BATCH transaction mode. infinispan.cache.indexing=If enabled, entries will be indexed when they are added to the cache. Indexes will be updated as entries change or are removed. infinispan.cache.indexing.deprecated=Deprecated. Has no effect. infinispan.cache.jndi-name=The jndi-name to which to bind this cache instance. infinispan.cache.jndi-name.deprecated=Deprecated. Will be ignored. infinispan.cache.module=The module associated with this cache's configuration. infinispan.cache.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.cache.modules=The set of modules associated with this cache's configuration. infinispan.cache.indexing-properties=Properties to control indexing behaviour infinispan.cache.indexing-properties.deprecated=Deprecated. Has no effect. infinispan.cache.remove=Remove a cache from this container. # cache read-only metrics infinispan.cache.cache-status=The status of the cache component. infinispan.cache.cache-status.deprecated=Deprecated. Always returns RUNNING. infinispan.cache.average-read-time=Average time (in ms) for cache reads. Includes hits and misses. infinispan.cache.average-read-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.average-remove-time=Average time (in ms) for cache removes. infinispan.cache.average-remove-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.average-write-time=Average time (in ms) for cache writes. infinispan.cache.average-write-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.elapsed-time=Time (in secs) since cache started. infinispan.cache.elapsed-time.deprecated=Deprecated. Use time-since-start instead. infinispan.cache.hit-ratio=The hit/miss ratio for the cache (hits/hits+misses). infinispan.cache.hit-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.hits=The number of cache attribute hits. infinispan.cache.hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.misses=The number of cache attribute misses. infinispan.cache.misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.number-of-entries=The number of entries in the cache including passivated entries. infinispan.cache.number-of-entries.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.number-of-entries-in-memory=The number of entries in the cache excluding passivated entries. infinispan.cache.read-write-ratio=The read/write ratio of the cache ((hits+misses)/stores). infinispan.cache.read-write-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.remove-hits=The number of cache attribute remove hits. infinispan.cache.remove-hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.remove-misses=The number of cache attribute remove misses. infinispan.cache.remove-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.stores=The number of cache attribute put operations. infinispan.cache.stores.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.time-since-reset=Time (in secs) since cache statistics were reset. infinispan.cache.time-since-reset.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.time-since-start=Time (in secs) since cache was started. infinispan.cache.writes=The number of cache attribute put operations. infinispan.cache.invalidations=The number of cache invalidations. infinispan.cache.invalidations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.passivations=The number of cache node passivations (passivating a node from memory to a cache store). infinispan.cache.passivations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.activations=The number of cache node activations (bringing a node into memory from a cache store). infinispan.cache.activations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # infinispan.cache.async-marshalling=If enabled, this will cause marshalling of entries to be performed asynchronously. infinispan.cache.async-marshalling.deprecated=Deprecated. Asynchronous marshalling is no longer supported. infinispan.cache.mode=Sets the clustered cache mode, ASYNC for asynchronous operation, or SYNC for synchronous operation. infinispan.cache.mode.deprecated=Deprecated. This attribute will be ignored. All cache modes will be treated as SYNC. To perform asynchronous cache operations, use Infinispan's asynchronous cache API. infinispan.cache.queue-size=In ASYNC mode, this attribute can be used to trigger flushing of the queue when it reaches a specific threshold. infinispan.cache.queue-size.deprecated=Deprecated. This attribute will be ignored. infinispan.cache.queue-flush-interval=In ASYNC mode, this attribute controls how often the asynchronous thread used to flush the replication queue runs. This should be a positive integer which represents thread wakeup time in milliseconds. infinispan.cache.queue-flush-interval.deprecated=Deprecated. This attribute will be ignored. infinispan.cache.remote-timeout=In SYNC mode, the timeout (in ms) used to wait for an acknowledgment when making a remote call, after which the call is aborted and an exception is thrown. # metrics infinispan.cache.average-replication-time=The average time taken to replicate data around the cluster. infinispan.cache.average-replication-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.replication-count=The number of times data was replicated around the cluster. infinispan.cache.replication-count.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.replication-failures=The number of data replication failures. infinispan.cache.replication-failures.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.success-ratio=The data replication success ratio (successes/successes+failures). infinispan.cache.success-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # operations infinispan.cache.reset-statistics=Reset the statistics for this cache. #child resource aliases infinispan.cache.memory=Alias to the eviction configuration component infinispan.cache.eviction=Alias to the memory=object resource infinispan.cache.expiration=Alias to the expiration configuration component infinispan.cache.locking=Alias to the locking configuration component infinispan.cache.state-transfer=Alias to the state-transfer configuration component infinispan.cache.transaction=Alias to the transaction configuration component infinispan.cache.file-store=Alias to the file store configuration component infinispan.cache.remote-store=Alias to the file store configuration component infinispan.cache.binary-keyed-jdbc-store=Alias to the binary jdbc store configuration component infinispan.cache.mixed-keyed-jdbc-store=Alias to the mixed jdbc store configuration component infinispan.cache.string-keyed-jdbc-store=Alias to the string jdbc store configuration component infinispan.cache.write-behind=Alias to the write behind configuration component infinispan.cache.backup-for=Alias to the backup-for configuration component infinispan.cache.backup=Alias to the backup child of the backups configuration infinispan.cache.segments=Controls the number of hash space segments which is the granularity for key distribution in the cluster. Value must be strictly positive. infinispan.cache.consistent-hash-strategy=Defines the consistent hash strategy for the cache. infinispan.cache.consistent-hash-strategy.deprecated=Deprecated. Segment allocation is no longer customizable. infinispan.cache.evictions=The number of cache eviction operations. infinispan.local-cache=A local cache configuration infinispan.local-cache.add=Add a local cache to this cache container infinispan.local-cache.remove=Remove a local cache from this cache container infinispan.invalidation-cache=An invalidation cache infinispan.invalidation-cache.add=Add an invalidation cache to this cache container infinispan.invalidation-cache.remove=Remove an invalidation cache from this cache container infinispan.replicated-cache=A replicated cache configuration infinispan.replicated-cache.add=Add a replicated cache to this cache container infinispan.replicated-cache.remove=Remove a replicated cache from this cache container infinispan.component.partition-handling=The partition handling configuration for distributed and replicated caches. infinispan.component.partition-handling.add=Add a partition handling configuration. infinispan.component.partition-handling.remove=Remove a partition handling configuration. infinispan.component.partition-handling.enabled=If enabled, the cache will enter degraded mode upon detecting a network partition that threatens the integrity of the cache. infinispan.component.partition-handling.availability=Indicates the current availability of the cache. infinispan.component.partition-handling.availability.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.partition-handling.force-available=Forces a cache with degraded availability to become available. infinispan.component.partition-handling.force-available.deprecated=Deprecated. Use operation from corresponding runtime cache resource. infinispan.component.state-transfer=The state transfer configuration for distributed and replicated caches. infinispan.component.state-transfer.add=Add a state transfer configuration. infinispan.component.state-transfer.remove=Remove a state transfer configuration. infinispan.component.state-transfer.enabled=If enabled, this will cause the cache to ask neighboring caches for state when it starts up, so the cache starts 'warm', although it will impact startup time. infinispan.component.state-transfer.enabled.deprecated=Deprecated. Always enabled for replicated and distributed caches. infinispan.component.state-transfer.timeout=The maximum amount of time (ms) to wait for state from neighboring caches, before throwing an exception and aborting startup. If timeout is 0, state transfer is performed asynchronously, and the cache will be immediately available. infinispan.component.state-transfer.chunk-size=The maximum number of cache entries in a batch of transferred state. infinispan.distributed-cache=A distributed cache configuration. infinispan.distributed-cache.add=Add a distributed cache to this cache container infinispan.distributed-cache.remove=Remove a distributed cache from this cache container infinispan.distributed-cache.owners=Number of cluster-wide replicas for each cache entry. infinispan.distributed-cache.virtual-nodes=Deprecated. Has no effect. infinispan.distributed-cache.virtual-nodes.deprecated=Deprecated. Has no effect. infinispan.distributed-cache.l1-lifespan=Maximum lifespan of an entry placed in the L1 cache. This element configures the L1 cache behavior in 'distributed' caches instances. In any other cache modes, this element is ignored. infinispan.distributed-cache.capacity-factor=Controls the proportion of entries that will reside on the local node, compared to the other nodes in the cluster. infinispan.scattered-cache=A scattered cache configuration. infinispan.scattered-cache.add=Add a scattered cache to this cache container infinispan.scattered-cache.remove=Remove a scattered cache from this cache container infinispan.scattered-cache.bias-lifespan=When greater than zero, specifies the duration (in ms) that a cache entry will be cached on a non-owner following a write operation. infinispan.scattered-cache.invalidation-batch-size=The threshold after which batched invalidations are sent. infinispan.cache.store=A persistent store for a cache. infinispan.cache.component=A configuration component of a cache. infinispan.component.locking=The locking configuration of the cache. infinispan.component.locking.add=Adds a locking configuration element to the cache. infinispan.component.locking.remove=Removes a locking configuration element from the cache. infinispan.component.locking.isolation=Sets the cache locking isolation level. infinispan.component.locking.striping=If true, a pool of shared locks is maintained for all entries that need to be locked. Otherwise, a lock is created per entry in the cache. Lock striping helps control memory footprint but may reduce concurrency in the system. infinispan.component.locking.acquire-timeout=Maximum time to attempt a particular lock acquisition. infinispan.component.locking.concurrency-level=Concurrency level for lock containers. Adjust this value according to the number of concurrent threads interacting with Infinispan. # metrics infinispan.component.locking.current-concurrency-level=The estimated number of concurrently updating threads which this cache can support. infinispan.component.locking.current-concurrency-level.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.locking.number-of-locks-available=The number of locks available to this cache. infinispan.component.locking.number-of-locks-available.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.locking.number-of-locks-held=The number of locks currently in use by this cache. infinispan.component.locking.number-of-locks-held.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction=The cache transaction configuration. infinispan.component.transaction.deprecated=Deprecated. Transactional behavior should be defined per remote-cache. infinispan.component.transaction.add=Adds a transaction configuration element to the cache. infinispan.component.transaction.complete-timeout=The duration (in ms) after which idle transactions are removed. infinispan.component.transaction.remove=Removes a transaction configuration element from the cache. infinispan.component.transaction.mode=Sets the cache transaction mode to one of NONE, NON_XA, NON_DURABLE_XA, FULL_XA. infinispan.component.transaction.stop-timeout=If there are any ongoing transactions when a cache is stopped, Infinispan waits for ongoing remote and local transactions to finish. The amount of time to wait for is defined by the cache stop timeout. infinispan.component.transaction.locking=The locking mode for this cache, one of OPTIMISTIC or PESSIMISTIC. infinispan.component.transaction.timeout=The duration (in ms) after which idle transactions are rolled back. # metrics infinispan.component.transaction.commits=The number of transaction commits. infinispan.component.transaction.commits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction.prepares=The number of transaction prepares. infinispan.component.transaction.prepares.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction.rollbacks=The number of transaction rollbacks. infinispan.component.transaction.rollbacks.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # infinispan.memory.heap=On-heap object-based memory configuration. infinispan.memory.off-heap=Off-heap memory configuration. infinispan.memory.add=Adds a memory configuration element to the cache. infinispan.memory.remove=Removes an eviction configuration element from the cache. infinispan.memory.size=Eviction threshold, as defined by the size unit. infinispan.memory.size-unit=The unit of the eviction threshold. infinispan.memory.object.size=Triggers eviction of the least recently used entries when the number of cache entries exceeds this threshold. infinispan.memory.eviction-type=Indicates whether the size attribute refers to the number of cache entries (i.e. COUNT) or the collective size of the cache entries (i.e. MEMORY). infinispan.memory.eviction-type.deprecated=Deprecated. Replaced by size-unit. infinispan.memory.capacity=Defines the capacity of the off-heap storage. infinispan.memory.capacity.deprecated=Deprecated. Will be ignored. infinispan.memory.strategy=Sets the cache eviction strategy. Available options are 'UNORDERED', 'FIFO', 'LRU', 'LIRS' and 'NONE' (to disable eviction). infinispan.memory.strategy.deprecated=Deprecated. Eviction uses LRU and is disabled via undefining the size attribute. infinispan.memory.max-entries=Maximum number of entries in a cache instance. If selected value is not a power of two the actual value will default to the least power of two larger than selected value. -1 means no limit. infinispan.memory.max-entries.deprecated=Deprecated. Use the size attribute instead. # metrics infinispan.memory.evictions=The number of cache eviction operations. infinispan.memory.evictions.deprecated=Deprecated. Use corresponding metric on parent resource. # infinispan.component.expiration=The cache expiration configuration. infinispan.component.expiration.add=Adds an expiration configuration element to the cache. infinispan.component.expiration.remove=Removes an expiration configuration element from the cache. infinispan.component.expiration.max-idle=Maximum idle time a cache entry will be maintained in the cache, in milliseconds. If the idle time is exceeded, the entry will be expired cluster-wide. -1 means the entries never expire. infinispan.component.expiration.lifespan=Maximum lifespan of a cache entry, after which the entry is expired cluster-wide, in milliseconds. -1 means the entries never expire. infinispan.component.expiration.interval=Interval (in milliseconds) between subsequent runs to purge expired entries from memory and any cache stores. If you wish to disable the periodic eviction process altogether, set wakeupInterval to -1. infinispan.store.custom=The cache store configuration. infinispan.store.custom.add=Adds a basic cache store configuration element to the cache. infinispan.store.custom.remove=Removes a cache store configuration element from the cache. infinispan.store.shared=This setting should be set to true when multiple cache instances share the same cache store (e.g., multiple nodes in a cluster using a JDBC-based CacheStore pointing to the same, shared database.) Setting this to true avoids multiple cache instances writing the same modification multiple times. If enabled, only the node where the modification originated will write to the cache store. If disabled, each individual cache reacts to a potential remote update by storing the data to the cache store. infinispan.store.preload=If true, when the cache starts, data stored in the cache store will be pre-loaded into memory. This is particularly useful when data in the cache store will be needed immediately after startup and you want to avoid cache operations being delayed as a result of loading this data lazily. Can be used to provide a 'warm-cache' on startup, however there is a performance penalty as startup time is affected by this process. infinispan.store.passivation=If true, data is only written to the cache store when it is evicted from memory, a phenomenon known as 'passivation'. Next time the data is requested, it will be 'activated' which means that data will be brought back to memory and removed from the persistent store. If false, the cache store contains a copy of the contents in memory, so writes to cache result in cache store writes. This essentially gives you a 'write-through' configuration. infinispan.store.fetch-state=If true, fetch persistent state when joining a cluster. If multiple cache stores are chained, only one of them can have this property enabled. infinispan.store.purge=If true, purges this cache store when it starts up. infinispan.store.max-batch-size=The maximum size of a batch to be inserted/deleted from the store. If the value is less than one, then no upper limit is placed on the number of operations in a batch. infinispan.store.singleton=If true, the singleton store cache store is enabled. SingletonStore is a delegating cache store used for situations when only one instance in a cluster should interact with the underlying store. infinispan.store.singleton.deprecated=Deprecated. Consider using a shared store instead, where writes are only performed by primary owners. infinispan.store.class=The custom store implementation class to use for this cache store. infinispan.store.write-behind=Child to configure a cache store as write-behind instead of write-through. infinispan.store.properties=A list of cache store properties. infinispan.store.properties.property=A cache store property with name and value. infinispan.store.property=A cache store property with name and value. infinispan.store.write=The write behavior of the cache store. # metrics infinispan.store.cache-loader-loads=The number of cache loader node loads. infinispan.store.cache-loader-loads.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.store.cache-loader-misses=The number of cache loader node misses. infinispan.store.cache-loader-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.persistence.cache-loader-loads=The number of entries loaded by this cache loader. infinispan.component.persistence.cache-loader-misses=The number of entry load misses by this cache loader. infinispan.write.behind=Configures a cache store as write-behind instead of write-through. infinispan.write.behind.add=Adds a write-behind configuration element to the store. infinispan.write.behind.remove=Removes a write-behind configuration element from the store. infinispan.write.behind.flush-lock-timeout=Timeout to acquire the lock which guards the state to be flushed to the cache store periodically. infinispan.write.behind.flush-lock-timeout.deprecated=Deprecated. This attribute is no longer used. infinispan.write.behind.modification-queue-size=Maximum number of entries in the asynchronous queue. When the queue is full, the store becomes write-through until it can accept new entries. infinispan.write.behind.shutdown-timeout=Timeout in milliseconds to stop the cache store. infinispan.write.behind.shutdown-timeout.deprecated=Deprecated. This attribute is no longer used. infinispan.write.behind.thread-pool-size=Size of the thread pool whose threads are responsible for applying the modifications to the cache store. infinispan.write.behind.thread-pool-size.deprecated=Deprecated. Uses size of non-blocking thread pool. infinispan.write.through=Configures a cache store as write-through. infinispan.write.through.add=Add a write-through configuration to the store. infinispan.write.through.remove=Remove a write-through configuration to the store. infinispan.property=A cache store property with name and value. infinispan.property.deprecated=Deprecated. Use "properties" attribute of the appropriate cache store resource. infinispan.property.add=Adds a cache store property. infinispan.property.remove=Removes a cache store property. infinispan.property.value=The value of the cache store property. infinispan.store.none=A store-less configuration. infinispan.store.none.add=Adds a store-less configuration to this cache infinispan.store.none.remove=Removes a store-less configuration from this cache infinispan.store.file=The cache file store configuration. infinispan.store.file.add=Adds a file cache store configuration element to the cache. infinispan.store.file.remove=Removes a cache file store configuration element from the cache. infinispan.store.file.relative-to=The system path to which the specified path is relative. infinispan.store.file.path=The system path under which this cache store will persist its entries. infinispan.store.jdbc=The cache JDBC store configuration. infinispan.store.jdbc.add=Adds a JDBC cache store configuration element to the cache. infinispan.store.jdbc.remove=Removes a JDBC cache store configuration element to the cache. infinispan.store.jdbc.data-source=References the data source used to connect to this store. infinispan.store.jdbc.datasource=The jndi name of the data source used to connect to this store. infinispan.store.jdbc.datasource.deprecated=Deprecated. Replaced by data-source. infinispan.store.jdbc.dialect=The dialect of this datastore. infinispan.store.jdbc.table=Defines a table used to store persistent cache data. infinispan.store.jdbc.binary-keyed-table=Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.store.jdbc.binary-keyed-table.deprecated=Deprecated. Use table=binary child resource. infinispan.store.jdbc.binary-keyed-table.table.prefix=The prefix for the database table name. infinispan.store.jdbc.binary-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.store.jdbc.binary-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.store.jdbc.binary-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.store.jdbc.binary-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.store.jdbc.binary-keyed-table.table.column.name= infinispan.store.jdbc.binary-keyed-table.table.column.type= infinispan.store.jdbc.binary-keyed-table.table.id-column=A database column to hold cache entry ids. infinispan.store.jdbc.binary-keyed-table.table.id-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.id-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.data-column=A database column to hold cache entry data. infinispan.store.jdbc.binary-keyed-table.table.data-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.data-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.segment-column=A database column to hold cache entry segment. infinispan.store.jdbc.binary-keyed-table.table.segment-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.segment-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table=Defines a table used to store persistent cache entries. infinispan.store.jdbc.string-keyed-table.deprecated=Deprecated. Use table=string child resource. infinispan.store.jdbc.string-keyed-table.table.prefix=The prefix for the database table name. infinispan.store.jdbc.string-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.store.jdbc.string-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.store.jdbc.string-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.store.jdbc.string-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.store.jdbc.string-keyed-table.table.column.name= infinispan.store.jdbc.string-keyed-table.table.column.type= infinispan.store.jdbc.string-keyed-table.table.id-column=A database column to hold cache entry ids. infinispan.store.jdbc.string-keyed-table.table.id-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.id-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.data-column=A database column to hold cache entry data. infinispan.store.jdbc.string-keyed-table.table.data-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.data-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.segment-column=A database column to hold cache entry segment. infinispan.store.jdbc.string-keyed-table.table.segment-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.segment-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.type=The type of the database column. infinispan.store.binary-jdbc.deprecated=Deprecated. Will be removed without replacement in a future release. Use store=jdbc instead. infinispan.store.mixed-jdbc.deprecated=Deprecated. Will be removed without replacement in a future release. Use store=jdbc instead. infinispan.table.binary=Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.deprecated=Deprecated. Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.add=Adds a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.remove=Removes a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.string=Defines a table used to store cache entries whose keys can be expressed as strings. infinispan.table.string.add=Adds a table used to store cache entries whose keys can be expressed as strings. infinispan.table.string.remove=Removes a table used to store cache entries whose keys can be expressed as strings. infinispan.table.prefix=The prefix for the database table name. infinispan.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.table.batch-size.deprecated=Deprecated. Use max-batch-size instead. infinispan.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.table.id-column=A database column to hold cache entry ids. infinispan.table.id-column.column.name=The name of the database column. infinispan.table.id-column.column.type=The type of the database column. infinispan.table.data-column=A database column to hold cache entry data. infinispan.table.data-column.column.name=The name of the database column. infinispan.table.data-column.column.type=The type of the database column. infinispan.table.segment-column=A database column to hold cache entry segment. infinispan.table.segment-column.column.name=The name of the database column. infinispan.table.segment-column.column.type=The type of the database column. infinispan.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.table.timestamp-column.column.name=The name of the database column. infinispan.table.timestamp-column.column.type=The type of the database column. # /subsystem=infinispan/cache-container=X/cache=Y/store=remote infinispan.store.remote=The cache remote store configuration. infinispan.store.remote.deprecated=Use HotRod store instead. infinispan.store.remote.cache=The name of the remote cache to use for this remote store. infinispan.store.remote.tcp-no-delay=A TCP_NODELAY value for remote cache communication. infinispan.store.remote.socket-timeout=A socket timeout for remote cache communication. # keycloak patch: begin infinispan.store.remote.connection-timeout=A connect timeout for remote cache communication. # keycloak patch: end infinispan.store.remote.remote-servers=A list of remote servers for this cache store. infinispan.store.remote.remote.servers.remote-server=A remote server, defined by its outbound socket binding. infinispan.store.remote.remote-servers.remote-server.outbound-socket-binding=An outbound socket binding for a remote server. infinispan.store.remote.add=Adds a remote cache store configuration element to the cache. infinispan.store.remote.remove=Removes a cache remote store configuration element from the cache. # /subsystem=infinispan/cache-container=X/cache=Y/store=hotrod infinispan.store.hotrod=HotRod-based store using Infinispan Server instance to store data. infinispan.store.hotrod.add=Adds HotRod store. infinispan.store.hotrod.remove=Removes HotRod store. infinispan.store.hotrod.cache-configuration=Name of the cache configuration template defined in Infinispan Server to create caches from. infinispan.store.hotrod.remote-cache-container=Reference to a container-managed remote-cache-container. infinispan.backup=A backup site to which to replicate this cache. infinispan.backup.add=Adds a backup site to this cache. infinispan.backup.remove=Removes a backup site from this cache. infinispan.backup.strategy=The backup strategy for this cache infinispan.backup.failure-policy=The policy to follow when connectivity to the backup site fails. infinispan.backup.enabled=Indicates whether or not this backup site is enabled. infinispan.backup.timeout=The timeout for replicating to the backup site. infinispan.backup.after-failures=Indicates the number of failures after which this backup site should go offline. infinispan.backup.min-wait=Indicates the minimum time (in milliseconds) to wait after the max number of failures is reached, after which this backup site should go offline. # cross-site backup operations infinispan.backup.site-status=Displays the current status of the backup site. infinispan.backup.bring-site-online=Re-enables a previously disabled backup site. infinispan.backup.take-site-offline=Disables backup to a remote site. infinispan.component.backup-for=A cache for which this cache acts as a backup (for use with cross site replication). infinispan.component.backup-for.deprecated=Deprecated. Backup designation must match the current cache name. infinispan.component.backup-for.add=Adds a backup designation for this cache. infinispan.component.backup-for.remove=Removes a backup designation for this cache. infinispan.component.backup-for.remote-cache=The name of the remote cache for which this cache acts as a backup. infinispan.component.backup-for.remote-cache.deprecated=This resource is deprecated. infinispan.component.backup-for.remote-site=The site of the remote cache for which this cache acts as a backup. infinispan.component.backup-for.remote-site.deprecated=This resource is deprecated. infinispan.component.backups=The remote backups for this cache. infinispan.component.backups.add=Adds remote backup support to this cache. infinispan.component.backups.remove=Removes remote backup support from this cache. infinispan.component.backups.backup=A remote backup. # /subsystem=infinispan/remote-cache-container=* infinispan.remote-cache-container=The configuration of a remote Infinispan cache container. infinispan.remote-cache-container.add=Add a remote cache container to the infinispan subsystem. infinispan.remote-cache-container.remove=Remove a cache container from the infinispan subsystem. infinispan.remote-cache-container.component=A configuration component of a remote cache container. infinispan.remote-cache-container.thread-pool=Defines thread pools for this remote cache container. infinispan.remote-cache-container.near-cache=Configures near caching. infinispan.remote-cache-container.connection-timeout=Defines the maximum socket connect timeout before giving up connecting to the server. infinispan.remote-cache-container.default-remote-cluster=Required default remote server cluster. infinispan.remote-cache-container.key-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing keys, to minimize array resizing. infinispan.remote-cache-container.key-size-estimate.deprecated=Deprecated. This attribute will be ignored. infinispan.remote-cache-container.max-retries=Sets the maximum number of retries for each request. A valid value should be greater or equals than 0. Zero means no retry will made in case of a network failure. infinispan.remote-cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries. infinispan.remote-cache-container.module=The module associated with this remote cache container's configuration. infinispan.remote-cache-container.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.remote-cache-container.modules=The set of modules associated with this remote cache container's configuration. infinispan.remote-cache-container.name=Uniquely identifies this remote cache container. infinispan.remote-cache-container.properties=A list of remote cache container properties. infinispan.remote-cache-container.protocol-version=This property defines the protocol version that this client should use. infinispan.remote-cache-container.socket-timeout=Enable or disable SO_TIMEOUT on socket connections to remote Hot Rod servers with the specified timeout, in milliseconds. A timeout of 0 is interpreted as an infinite timeout. infinispan.remote-cache-container.statistics-enabled=Enables statistics gathering for this remote cache. infinispan.remote-cache-container.tcp-no-delay=Enable or disable TCP_NODELAY on socket connections to remote Hot Rod servers. infinispan.remote-cache-container.tcp-keep-alive=Configures TCP Keepalive on the TCP stack. infinispan.remote-cache-container.value-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing values, to minimize array resizing. infinispan.remote-cache-container.value-size-estimate.deprecated=Deprecated. This attribute will be ignored. infinispan.remote-cache-container.active-connections=The number of active connections to the Infinispan server. infinispan.remote-cache-container.connections=The total number of connections to the Infinispan server. infinispan.remote-cache-container.idle-connections=The number of idle connections to the Infinispan server. infinispan.remote-cache-container.transaction-timeout=The duration (in ms) after which idle transactions are rolled back. infinispan.remote-cache-container.remote-cache=A remote cache runtime resource infinispan.remote-cache.average-read-time=The average read time, in milliseconds, for this remote cache. infinispan.remote-cache.average-remove-time=The average remove time, in milliseconds, for this remote cache. infinispan.remote-cache.average-write-time=The average write time, in milliseconds, to this remote cache. infinispan.remote-cache.near-cache-hits=The number of near-cache hits for this remote cache. infinispan.remote-cache.near-cache-invalidations=The number of near-cache invalidations for this remote cache. infinispan.remote-cache.near-cache-misses=The number of near-cache misses for this remote cache. infinispan.remote-cache.near-cache-size=The number of entries in the near-cache for this remote cache. infinispan.remote-cache.hits=The number of hits to this remote cache, excluding hits from the near-cache. infinispan.remote-cache.misses=The number of misses to this remote cache. infinispan.remote-cache.removes=The number of removes to this remote cache. infinispan.remote-cache.writes=The number of writes to this remote cache. infinispan.remote-cache.reset-statistics=Resets the statistics for this remote cache. infinispan.remote-cache.time-since-reset=The number of seconds since statistics were reset on this remote cache. # /subsystem=infinispan/remote-cache-container=X/thread-pool=async infinispan.thread-pool.async=Defines a thread pool used for asynchronous operations. infinispan.thread-pool.async.add=Adds thread pool configuration used for asynchronous operations. infinispan.thread-pool.async.remove=Removes thread pool configuration used for asynchronous operations. # /subsystem=infinispan/remote-cache-container=*/component=connection-pool infinispan.component.connection-pool=Configuration of the connection pool. infinispan.component.connection-pool.add=Adds configuration of the connection pool. infinispan.component.connection-pool.remove=Removes configuration of the connection pool. infinispan.component.connection-pool.exhausted-action=Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted. infinispan.component.connection-pool.max-active=Controls the maximum number of connections per server that are allocated (checked out to client threads, or idle in the pool) at one time. When non-positive, there is no limit to the number of connections per server. When maxActive is reached, the connection pool for that server is said to be exhausted. Value -1 means no limit. infinispan.component.connection-pool.max-wait=The amount of time in milliseconds to wait for a connection to become available when the exhausted action is ExhaustedAction.WAIT, after which a java.util.NoSuchElementException will be thrown. If a negative value is supplied, the pool will block indefinitely. infinispan.component.connection-pool.min-evictable-idle-time=Specifies the minimum amount of time that an connection may sit idle in the pool before it is eligible for eviction due to idle time. When non-positive, no connection will be dropped from the pool due to idle time alone. This setting has no effect unless timeBetweenEvictionRunsMillis > 0. infinispan.component.connection-pool.min-idle=Sets a target value for the minimum number of idle connections (per server) that should always be available. If this parameter is set to a positive number and timeBetweenEvictionRunsMillis > 0, each time the idle connection eviction thread runs, it will try to create enough idle instances so that there will be minIdle idle instances available for each server. # /subsystem=infinispan/remote-cache-container=*/near-cache=invalidation infinispan.near-cache.invalidation=Configures using near cache in invalidated mode. When entries are updated or removed server-side, invalidation messages will be sent to clients to remove them from the near cache. infinispan.near-cache.invalidation.add=Adds a near cache in invalidated mode. infinispan.near-cache.invalidation.remove=Removes near cache in invalidated mode. infinispan.near-cache.invalidation.deprecated=Deprecated. Near cache is enabled per remote cache. infinispan.near-cache.invalidation.max-entries=Defines the maximum number of elements to keep in the near cache. # /subsystem=infinispan/remote-cache-container=*/near-cache=none infinispan.near-cache.none=Disables near cache. infinispan.near-cache.none.add=Adds configuration that disables near cache. infinispan.near-cache.none.remove=Removes configuration that disables near cache. infinispan.near-cache.none.deprecated=Deprecated. Near cache is disabled per remote cache. # /subsystem=infinispan/remote-cache-container=*/component=remote-clusters/remote-cluster=* infinispan.remote-cluster=Configuration of a remote cluster. infinispan.remote-cluster.add=Adds a remote cluster configuration requiring socket-bindings configuration. infinispan.remote-cluster.remove=Removes this remote cluster configuration. infinispan.remote-cluster.socket-bindings=List of outbound-socket-bindings of Hot Rod servers to connect to. infinispan.remote-cluster.switch-cluster=Switch the cluster to which this HotRod client should communicate. Primary used to failback to the local site in the event of a site failover. # /subsystem=infinispan/remote-cache-container=*/component=security infinispan.component.security=Security configuration. infinispan.component.security.add=Adds security configuration. infinispan.component.security.remove=Removes security configuration. infinispan.component.security.ssl-context=Reference to the Elytron-managed SSLContext to be used for connecting to the remote cluster. ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreResourceDefinition.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import java.util.concurrent.TimeUnit; import org.jboss.as.clustering.controller.CapabilityReference; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.clustering.controller.ResourceServiceConfigurator; import org.jboss.as.clustering.controller.SimpleResourceDescriptorConfigurator; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.controller.client.helpers.MeasurementUnit; import org.jboss.as.controller.registry.AttributeAccess; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; /** * Resource description for the addressable resource and its alias * * /subsystem=infinispan/cache-container=X/cache=Y/store=remote * /subsystem=infinispan/cache-container=X/cache=Y/remote-store=REMOTE_STORE * * @author Richard Achmatowicz (c) 2011 Red Hat Inc. * @deprecated Use {@link org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition} instead. */ @Deprecated public class RemoteStoreResourceDefinition extends StoreResourceDefinition { static final PathElement LEGACY_PATH = PathElement.pathElement("remote-store", "REMOTE_STORE"); static final PathElement PATH = pathElement("remote"); enum Attribute implements org.jboss.as.clustering.controller.Attribute { CACHE("cache", ModelType.STRING, null), SOCKET_TIMEOUT("socket-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))), // keycloak patch: begin CONNECTION_TIMEOUT("connection-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))), // keycloak patch: end TCP_NO_DELAY("tcp-no-delay", ModelType.BOOLEAN, ModelNode.TRUE), SOCKET_BINDINGS("remote-servers") ; private final AttributeDefinition definition; Attribute(String name, ModelType type, ModelNode defaultValue) { this.definition = new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(defaultValue == null) .setDefaultValue(defaultValue) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .setMeasurementUnit((type == ModelType.LONG) ? MeasurementUnit.MILLISECONDS : null) .build(); } Attribute(String name) { this.definition = new StringListAttributeDefinition.Builder(name) .setCapabilityReference(new CapabilityReference(Capability.PERSISTENCE, CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING)) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .setMinSize(1) .build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } } RemoteStoreResourceDefinition() { super(PATH, LEGACY_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(PATH, WILDCARD_PATH), new SimpleResourceDescriptorConfigurator<>(Attribute.class)); this.setDeprecated(InfinispanModel.VERSION_7_0_0.getVersion()); } @Override public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) { return new RemoteStoreServiceConfigurator(address); } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreServiceConfigurator.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2015, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CACHE; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.network.OutboundSocketBinding; import org.jboss.dmr.ModelNode; import org.jboss.msc.service.ServiceBuilder; import org.wildfly.clustering.service.Dependency; import org.wildfly.clustering.service.ServiceConfigurator; import org.wildfly.clustering.service.ServiceSupplierDependency; import org.wildfly.clustering.service.SupplierDependency; /** * @author Paul Ferraro */ @Deprecated public class RemoteStoreServiceConfigurator extends StoreServiceConfigurator { private volatile List> bindings; private volatile String remoteCacheName; private volatile long socketTimeout; // keycloak patch: begin private volatile long connectionTimeout; // keycloak patch: end private volatile boolean tcpNoDelay; public RemoteStoreServiceConfigurator(PathAddress address) { super(address, RemoteStoreConfigurationBuilder.class); } @Override public ServiceBuilder register(ServiceBuilder builder) { for (Dependency dependency : this.bindings) { dependency.register(builder); } return super.register(builder); } @Override public ServiceConfigurator configure(OperationContext context, ModelNode model) throws OperationFailedException { this.remoteCacheName = CACHE.resolveModelAttribute(context, model).asString(); this.socketTimeout = SOCKET_TIMEOUT.resolveModelAttribute(context, model).asLong(); this.connectionTimeout = CONNECTION_TIMEOUT.resolveModelAttribute(context, model).asLong(); this.tcpNoDelay = TCP_NO_DELAY.resolveModelAttribute(context, model).asBoolean(); List bindings = StringListAttributeDefinition.unwrapValue(context, SOCKET_BINDINGS.resolveModelAttribute(context, model)); this.bindings = new ArrayList<>(bindings.size()); for (String binding : bindings) { this.bindings.add(new ServiceSupplierDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, binding))); } return super.configure(context, model); } @Override public void accept(RemoteStoreConfigurationBuilder builder) { builder.segmented(false) .remoteCacheName(this.remoteCacheName) .socketTimeout(this.socketTimeout) .connectionTimeout(this.connectionTimeout) .tcpNoDelay(this.tcpNoDelay) ; for (Supplier bindingDependency : this.bindings) { OutboundSocketBinding binding = bindingDependency.get(); builder.addServer().host(binding.getUnresolvedDestinationAddress()).port(binding.getDestinationPort()); } } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-25.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/remote/RemoteCacheContainerResourceDefinition.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2016, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem.remote; import java.util.EnumSet; import java.util.concurrent.TimeUnit; import java.util.function.UnaryOperator; import org.infinispan.client.hotrod.ProtocolVersion; import org.jboss.as.clustering.controller.CapabilityProvider; import org.jboss.as.clustering.controller.CapabilityReference; import org.jboss.as.clustering.controller.ChildResourceDefinition; import org.jboss.as.clustering.controller.ListAttributeTranslation; import org.jboss.as.clustering.controller.ManagementResourceRegistration; import org.jboss.as.clustering.controller.MetricHandler; import org.jboss.as.clustering.controller.PropertiesAttributeDefinition; import org.jboss.as.clustering.controller.ResourceDescriptor; import org.jboss.as.clustering.controller.ResourceServiceConfigurator; import org.jboss.as.clustering.controller.ResourceServiceConfiguratorFactory; import org.jboss.as.clustering.controller.ResourceServiceHandler; import org.jboss.as.clustering.controller.ServiceValueExecutorRegistry; import org.jboss.as.clustering.controller.SimpleResourceRegistration; import org.jboss.as.clustering.controller.UnaryRequirementCapability; import org.jboss.as.clustering.controller.validation.EnumValidator; import org.jboss.as.clustering.controller.validation.ModuleIdentifierValidatorBuilder; import org.jboss.as.clustering.infinispan.InfinispanLogger; import org.jboss.as.clustering.infinispan.subsystem.InfinispanExtension; import org.jboss.as.clustering.infinispan.subsystem.InfinispanModel; import org.jboss.as.clustering.infinispan.subsystem.ThreadPoolResourceDefinition; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.controller.client.helpers.MeasurementUnit; import org.jboss.as.controller.descriptions.ModelDescriptionConstants; import org.jboss.as.controller.registry.AttributeAccess; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; import org.wildfly.clustering.infinispan.client.InfinispanClientRequirement; import org.wildfly.clustering.infinispan.client.RemoteCacheContainer; import org.wildfly.clustering.infinispan.client.marshaller.HotRodMarshallerFactory; import org.wildfly.clustering.service.UnaryRequirement; /** * /subsystem=infinispan/remote-cache-container=X * * @author Radoslav Husar */ public class RemoteCacheContainerResourceDefinition extends ChildResourceDefinition implements ResourceServiceConfiguratorFactory { public static final PathElement WILDCARD_PATH = pathElement(PathElement.WILDCARD_VALUE); public static PathElement pathElement(String containerName) { return PathElement.pathElement("remote-cache-container", containerName); } public enum Capability implements CapabilityProvider { CONTAINER(InfinispanClientRequirement.REMOTE_CONTAINER), CONFIGURATION(InfinispanClientRequirement.REMOTE_CONTAINER_CONFIGURATION), ; private final org.jboss.as.clustering.controller.Capability capability; Capability(UnaryRequirement requirement) { this.capability = new UnaryRequirementCapability(requirement); } @Override public org.jboss.as.clustering.controller.Capability getCapability() { return this.capability; } } public enum Attribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator { CONNECTION_TIMEOUT("connection-timeout", ModelType.INT, new ModelNode(60000)), DEFAULT_REMOTE_CLUSTER("default-remote-cluster", ModelType.STRING, null) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setAllowExpression(false).setCapabilityReference(new CapabilityReference(Capability.CONFIGURATION, RemoteClusterResourceDefinition.Requirement.REMOTE_CLUSTER, WILDCARD_PATH)); } }, MARSHALLER("marshaller", ModelType.STRING, new ModelNode(HotRodMarshallerFactory.LEGACY.name())) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setValidator(new EnumValidator(HotRodMarshallerFactory.class) { @Override public void validateParameter(String parameterName, ModelNode value) throws OperationFailedException { super.validateParameter(parameterName, value); if (!value.isDefined() || value.equals(MARSHALLER.getDefinition().getDefaultValue())) { InfinispanLogger.ROOT_LOGGER.marshallerEnumValueDeprecated(parameterName, HotRodMarshallerFactory.LEGACY, EnumSet.complementOf(EnumSet.of(HotRodMarshallerFactory.LEGACY))); } } }); } }, MAX_RETRIES("max-retries", ModelType.INT, new ModelNode(10)), PROPERTIES("properties"), PROTOCOL_VERSION("protocol-version", ModelType.STRING, new ModelNode(ProtocolVersion.PROTOCOL_VERSION_31.toString())) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setValidator(new org.jboss.as.controller.operations.validation.EnumValidator<>(ProtocolVersion.class, EnumSet.complementOf(EnumSet.of(ProtocolVersion.PROTOCOL_VERSION_AUTO)))); } }, SOCKET_TIMEOUT("socket-timeout", ModelType.INT, new ModelNode(60000)), STATISTICS_ENABLED(ModelDescriptionConstants.STATISTICS_ENABLED, ModelType.BOOLEAN, ModelNode.FALSE), TCP_NO_DELAY("tcp-no-delay", ModelType.BOOLEAN, ModelNode.TRUE), TCP_KEEP_ALIVE("tcp-keep-alive", ModelType.BOOLEAN, ModelNode.FALSE), TRANSACTION_TIMEOUT("transaction-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setMeasurementUnit(MeasurementUnit.MILLISECONDS); } }, ; private final AttributeDefinition definition; Attribute(String name) { this.definition = new PropertiesAttributeDefinition.Builder(name) .setAllowExpression(true) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .build(); } Attribute(String name, ModelType type, ModelNode defaultValue) { this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(defaultValue == null) .setDefaultValue(defaultValue) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) ).build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder; } } public enum ListAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator { MODULES("modules") { @Override public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) { return builder.setElementValidator(new ModuleIdentifierValidatorBuilder().configure(builder).build()); } }, ; private final AttributeDefinition definition; ListAttribute(String name) { this.definition = this.apply(new StringListAttributeDefinition.Builder(name) .setAllowExpression(true) .setRequired(false) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) ).build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } @Override public StringListAttributeDefinition.Builder apply(StringListAttributeDefinition.Builder builder) { return builder; } } public enum DeprecatedAttribute implements org.jboss.as.clustering.controller.Attribute, UnaryOperator { KEY_SIZE_ESTIMATE("key-size-estimate", ModelType.INT, InfinispanModel.VERSION_15_0_0) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setDefaultValue(new ModelNode(64)); } }, MODULE("module", ModelType.STRING, InfinispanModel.VERSION_14_0_0) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setFlags(AttributeAccess.Flag.ALIAS); } }, VALUE_SIZE_ESTIMATE("value-size-estimate", ModelType.INT, InfinispanModel.VERSION_15_0_0) { @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder.setDefaultValue(new ModelNode(512)); } } ; private final AttributeDefinition definition; DeprecatedAttribute(String name, ModelType type, InfinispanModel deprecation) { this.definition = this.apply(new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(false) .setDeprecated(deprecation.getVersion()) .setFlags(AttributeAccess.Flag.RESTART_NONE) ).build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } @Override public SimpleAttributeDefinitionBuilder apply(SimpleAttributeDefinitionBuilder builder) { return builder; } } public RemoteCacheContainerResourceDefinition() { super(WILDCARD_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(WILDCARD_PATH)); } @SuppressWarnings("deprecation") @Override public ManagementResourceRegistration register(ManagementResourceRegistration parentRegistration) { ManagementResourceRegistration registration = parentRegistration.registerSubModel(this); ResourceDescriptor descriptor = new ResourceDescriptor(this.getResourceDescriptionResolver()) .addAttributes(Attribute.class) .addAttributes(ListAttribute.class) .addIgnoredAttributes(EnumSet.complementOf(EnumSet.of(DeprecatedAttribute.MODULE))) .addAttributeTranslation(DeprecatedAttribute.MODULE, new ListAttributeTranslation(ListAttribute.MODULES)) .addCapabilities(Capability.class) .addRequiredChildren(ConnectionPoolResourceDefinition.PATH, ThreadPoolResourceDefinition.CLIENT.getPathElement(), SecurityResourceDefinition.PATH, RemoteTransactionResourceDefinition.PATH) .addRequiredSingletonChildren(NoNearCacheResourceDefinition.PATH) .setResourceTransformation(RemoteCacheContainerResource::new) ; ServiceValueExecutorRegistry executors = new ServiceValueExecutorRegistry<>(); ResourceServiceHandler handler = new RemoteCacheContainerServiceHandler(this, executors); new SimpleResourceRegistration(descriptor, handler).register(registration); new ConnectionPoolResourceDefinition().register(registration); new RemoteClusterResourceDefinition(this, executors).register(registration); new SecurityResourceDefinition().register(registration); new RemoteTransactionResourceDefinition().register(registration); new InvalidationNearCacheResourceDefinition().register(registration); new NoNearCacheResourceDefinition().register(registration); ThreadPoolResourceDefinition.CLIENT.register(registration); if (registration.isRuntimeOnlyRegistrationValid()) { new MetricHandler<>(new RemoteCacheContainerMetricExecutor(executors), RemoteCacheContainerMetric.class).register(registration); new RemoteCacheResourceDefinition(executors).register(registration); } return registration; } @Override public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) { return new RemoteCacheContainerConfigurationServiceConfigurator(address); } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/pom.xml ================================================ 4.0.0 org.example wildfly-clustering-infinispan-extension-patch-26.0.x 1.0-SNAPSHOT UTF-8 11 11 26.0.1.Final 2.0.0.Final 12.1.7.Final 4.4.1.Final 1.0 4.1.68.Final 3.0.13 1.8 org.wildfly wildfly-clustering-infinispan-extension ${version.wildfly} org.wildfly wildfly-clustering-ee-infinispan ${version.wildfly} org.wildfly wildfly-clustering-jgroups-extension ${version.wildfly} org.wildfly wildfly-clustering-infinispan-client ${version.wildfly} org.wildfly wildfly-clustering-infinispan-marshalling ${version.wildfly} org.wildfly wildfly-clustering-infinispan-spi ${version.wildfly} org.wildfly wildfly-clustering-marshalling-jboss ${version.wildfly} org.wildfly wildfly-clustering-spi ${version.wildfly} org.wildfly wildfly-transactions ${version.wildfly} org.wildfly.transaction wildfly-transaction-client ${version.org.wildfly.transaction.client} org.infinispan infinispan-cachestore-jdbc ${version.org.infinispan} org.infinispan infinispan-cachestore-remote ${version.org.infinispan} org.infinispan.protostream protostream ${version.org.infinispan.protostream} net.jcip jcip-annotations ${version.net.jcip} io.netty netty-all ${version.io.netty} io.reactivex.rxjava3 rxjava ${version.io.reactivex.rxjava3} org.kohsuke.metainf-services metainf-services provided ${version.org.kohsuke.metainf-services} wildfly-clustering-infinispan-extension-patch src/main/java **/*.properties org.apache.maven.plugins maven-shade-plugin 3.2.4 package shade org.wildfly:wildfly-clustering-infinispan-extension org/jboss/as/** **/*.properties schema/* subsystem-templates/* META-INF/services/* org.wildfly:wildfly-clustering-infinispan-extension org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/InfinispanSubsystemXMLReader.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import static org.jboss.as.clustering.infinispan.InfinispanLogger.ROOT_LOGGER; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import org.jboss.as.clustering.controller.Attribute; import org.jboss.as.clustering.controller.Operations; import org.jboss.as.clustering.controller.ResourceDefinitionProvider; import org.jboss.as.clustering.infinispan.subsystem.TableResourceDefinition.ColumnAttribute; import org.jboss.as.clustering.infinispan.subsystem.remote.ConnectionPoolResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.InvalidationNearCacheResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteCacheContainerResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteClusterResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.RemoteTransactionResourceDefinition; import org.jboss.as.clustering.infinispan.subsystem.remote.SecurityResourceDefinition; import org.jboss.as.clustering.jgroups.subsystem.ChannelResourceDefinition; import org.jboss.as.clustering.jgroups.subsystem.JGroupsSubsystemResourceDefinition; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.AttributeParser; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.operations.common.Util; import org.jboss.as.controller.parsing.Element; import org.jboss.as.controller.parsing.ParseUtils; import org.jboss.dmr.ModelNode; import org.jboss.staxmapper.XMLElementReader; import org.jboss.staxmapper.XMLExtendedStreamReader; /** * XML reader for the Infinispan subsystem. * * @author Paul Ferraro */ @SuppressWarnings({ "deprecation", "static-method" }) public class InfinispanSubsystemXMLReader implements XMLElementReader> { private final InfinispanSchema schema; InfinispanSubsystemXMLReader(InfinispanSchema schema) { this.schema = schema; } @Override public void readElement(XMLExtendedStreamReader reader, List result) throws XMLStreamException { Map operations = new LinkedHashMap<>(); PathAddress address = PathAddress.pathAddress(InfinispanSubsystemResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case CACHE_CONTAINER: { this.parseContainer(reader, address, operations); break; } case REMOTE_CACHE_CONTAINER: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseRemoteContainer(reader, address, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } result.addAll(operations.values()); } private void parseContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = subsystemAddress.append(CacheContainerResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { ParseUtils.requireNoNamespaceAttribute(reader, i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case DEFAULT_CACHE: { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.DEFAULT_CACHE); break; } case JNDI_NAME: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.JNDI_NAME); break; } case LISTENER_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.LISTENER); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.LISTENER.getName()); break; } case EVICTION_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.EVICTION); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.EVICTION.getName()); break; } case REPLICATION_QUEUE_EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE); ROOT_LOGGER.executorIgnored(CacheContainerResourceDefinition.ExecutorAttribute.REPLICATION_QUEUE.getName()); break; } case START: { if (this.schema.since(InfinispanSchema.VERSION_1_1) && !this.schema.since(InfinispanSchema.VERSION_3_0)) { ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); } else { throw ParseUtils.unexpectedAttribute(reader, i); } break; } case ALIASES: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.ALIASES); break; } } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } if (this.schema.since(InfinispanSchema.VERSION_1_3)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.DeprecatedAttribute.MODULE); break; } } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_1_5)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.ListAttribute.MODULES); break; } } case MARSHALLER: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, CacheContainerResourceDefinition.Attribute.MARSHALLER); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } if (!this.schema.since(InfinispanSchema.VERSION_1_5)) { operation.get(CacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true); } List aliases = new LinkedList<>(); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ALIAS: { if (InfinispanSchema.VERSION_1_0.since(this.schema)) { aliases.add(reader.getElementText()); break; } throw ParseUtils.unexpectedElement(reader); } case TRANSPORT: { this.parseTransport(reader, address, operations); break; } case LOCAL_CACHE: { this.parseLocalCache(reader, address, operations); break; } case INVALIDATION_CACHE: { this.parseInvalidationCache(reader, address, operations); break; } case REPLICATED_CACHE: { this.parseReplicatedCache(reader, address, operations); break; } case DISTRIBUTED_CACHE: { this.parseDistributedCache(reader, address, operations); break; } case EXPIRATION_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseScheduledThreadPool(ScheduledThreadPoolResourceDefinition.EXPIRATION, reader, address, operations); break; } } case ASYNC_OPERATIONS_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.ASYNC_OPERATIONS, reader, address, operations); break; } } case LISTENER_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.LISTENER, reader, address, operations); break; } } case PERSISTENCE_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { if (this.schema.since(InfinispanSchema.VERSION_7_0) && !this.schema.since(InfinispanSchema.VERSION_10_0)) { this.parseScheduledThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations); } else { this.parseThreadPool(ThreadPoolResourceDefinition.PERSISTENCE, reader, address, operations); } break; } } case REMOTE_COMMAND_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.REMOTE_COMMAND, reader, address, operations); break; } } case STATE_TRANSFER_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.STATE_TRANSFER, reader, address, operations); break; } } case TRANSPORT_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.TRANSPORT, reader, address, operations); break; } } case SCATTERED_CACHE: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseScatteredCache(reader, address, operations); break; } } case BLOCKING_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.BLOCKING, reader, address, operations); break; } } case NON_BLOCKING_THREAD_POOL: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseThreadPool(ThreadPoolResourceDefinition.NON_BLOCKING, reader, address, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } if (!aliases.isEmpty()) { // Adapt aliases parsed from legacy schema into format expected by the current attribute parser setAttribute(reader, String.join(" ", aliases), operation, CacheContainerResourceDefinition.ListAttribute.ALIASES); } } private void parseTransport(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(JGroupsTransportResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(containerAddress.append(TransportResourceDefinition.WILDCARD_PATH), operation); String stack = null; String cluster = null; for (int i = 0; i < reader.getAttributeCount(); i++) { String value = reader.getAttributeValue(i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STACK: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } stack = value; break; } case EXECUTOR: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT); ROOT_LOGGER.executorIgnored(JGroupsTransportResourceDefinition.ExecutorAttribute.TRANSPORT.getName()); break; } case LOCK_TIMEOUT: { readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.LOCK_TIMEOUT); break; } case SITE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.SITE.getLocalName()); break; } case RACK: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.RACK.getLocalName()); break; } case MACHINE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.topologyAttributeDeprecated(XMLAttribute.MACHINE.getLocalName()); break; } case CLUSTER: { if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_3_0)) { cluster = value; break; } throw ParseUtils.unexpectedAttribute(reader, i); } case CHANNEL: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, i, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } if (!this.schema.since(InfinispanSchema.VERSION_3_0)) { // We need to create a corresponding channel add operation String channel = (cluster != null) ? cluster : ("ee-" + containerAddress.getLastElement().getValue()); setAttribute(reader, channel, operation, JGroupsTransportResourceDefinition.Attribute.CHANNEL); PathAddress channelAddress = PathAddress.pathAddress(JGroupsSubsystemResourceDefinition.PATH, ChannelResourceDefinition.pathElement(channel)); ModelNode channelOperation = Util.createAddOperation(channelAddress); if (stack != null) { setAttribute(reader, stack, channelOperation, ChannelResourceDefinition.Attribute.STACK); } operations.put(channelAddress, channelOperation); } ParseUtils.requireNoContent(reader); } private void parseLocalCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(LocalCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseCacheElement(reader, address, operations); } } private void parseReplicatedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(ReplicatedCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseClusteredCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseSharedStateCacheElement(reader, address, operations); } } private void parseScatteredCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(ScatteredCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case BIAS_LIFESPAN: { readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.BIAS_LIFESPAN); break; } case INVALIDATION_BATCH_SIZE: { readAttribute(reader, i, operation, ScatteredCacheResourceDefinition.Attribute.INVALIDATION_BATCH_SIZE); break; } default: { this.parseSegmentedCacheAttribute(reader, i, address, operations); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseSharedStateCacheElement(reader, address, operations); } } private void parseDistributedCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(DistributedCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case OWNERS: { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.OWNERS); break; } case L1_LIFESPAN: { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.L1_LIFESPAN); break; } case VIRTUAL_NODES: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { throw ParseUtils.unexpectedAttribute(reader, i); } // AS7-5753: convert any non-expression virtual nodes value to a segments value, String virtualNodes = readAttribute(reader, i, SegmentedCacheResourceDefinition.Attribute.SEGMENTS).asString(); String segments = SegmentsAndVirtualNodeConverter.virtualNodesToSegments(virtualNodes); setAttribute(reader, segments, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS); break; } case CAPACITY_FACTOR: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, i, operation, DistributedCacheResourceDefinition.Attribute.CAPACITY_FACTOR); break; } } default: { this.parseSegmentedCacheAttribute(reader, i, address, operations); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { this.parseSharedStateCacheElement(reader, address, operations); } else { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REHASHING: { this.parseStateTransfer(reader, address, operations); break; } default: { this.parseCacheElement(reader, address, operations); } } } } } private void parseInvalidationCache(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = containerAddress.append(InvalidationCacheResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseClusteredCacheAttribute(reader, i, address, operations); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseCacheElement(reader, address, operations); } } private void parseCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case NAME: { // Already read break; } case START: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case BATCHING: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } PathAddress transactionAddress = address.append(TransactionResourceDefinition.PATH); ModelNode transactionOperation = Util.createAddOperation(transactionAddress); transactionOperation.get(TransactionResourceDefinition.Attribute.MODE.getName()).set(new ModelNode(TransactionMode.BATCH.name())); operations.put(transactionAddress, transactionOperation); break; } case INDEXING: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { throw ParseUtils.unexpectedAttribute(reader, index); } readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING); break; } case JNDI_NAME: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.JNDI_NAME); break; } } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } if (this.schema.since(InfinispanSchema.VERSION_1_3)) { readAttribute(reader, index, operation, CacheResourceDefinition.DeprecatedAttribute.MODULE); break; } } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_1_5)) { readAttribute(reader, index, operation, CacheResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, index, operation, CacheResourceDefinition.ListAttribute.MODULES); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } if (!this.schema.since(InfinispanSchema.VERSION_1_5)) { // We need to explicitly enable statistics (to reproduce old behavior), since the new attribute defaults to false. operation.get(CacheResourceDefinition.Attribute.STATISTICS_ENABLED.getName()).set(true); } } private void parseSegmentedCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SEGMENTS: { if (this.schema.since(InfinispanSchema.VERSION_1_4)) { readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.Attribute.SEGMENTS); break; } } case CONSISTENT_HASH_STRATEGY: { if (this.schema.since(InfinispanSchema.VERSION_3_0)) { readAttribute(reader, index, operation, SegmentedCacheResourceDefinition.DeprecatedAttribute.CONSISTENT_HASH_STRATEGY); break; } } default: { this.parseClusteredCacheAttribute(reader, index, address, operations); } } } private void parseClusteredCacheAttribute(XMLExtendedStreamReader reader, int index, PathAddress address, Map operations) throws XMLStreamException { ModelNode operation = operations.get(address); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case MODE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } break; } case QUEUE_SIZE: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_SIZE); break; } case QUEUE_FLUSH_INTERVAL: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.DeprecatedAttribute.QUEUE_FLUSH_INTERVAL); break; } case REMOTE_TIMEOUT: { readAttribute(reader, index, operation, ClusteredCacheResourceDefinition.Attribute.REMOTE_TIMEOUT); break; } case ASYNC_MARSHALLING: { if (!this.schema.since(InfinispanSchema.VERSION_1_2) && this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { this.parseCacheAttribute(reader, index, address, operations); } } } private void parseCacheElement(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case EVICTION: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } this.parseEviction(reader, cacheAddress, operations); break; } case EXPIRATION: { this.parseExpiration(reader, cacheAddress, operations); break; } case LOCKING: { this.parseLocking(reader, cacheAddress, operations); break; } case TRANSACTION: { this.parseTransaction(reader, cacheAddress, operations); break; } case STORE: { this.parseCustomStore(reader, cacheAddress, operations); break; } case FILE_STORE: { this.parseFileStore(reader, cacheAddress, operations); break; } case REMOTE_STORE: { this.parseRemoteStore(reader, cacheAddress, operations); break; } case HOTROD_STORE: { if (this.schema.since(InfinispanSchema.VERSION_6_0)) { this.parseHotRodStore(reader, cacheAddress, operations); break; } } case JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2) && !this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseJDBCStore(reader, cacheAddress, operations); } else { this.parseLegacyJDBCStore(reader, cacheAddress, operations); } break; } case STRING_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseStringKeyedJDBCStore(reader, cacheAddress, operations); break; } } case BINARY_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseBinaryKeyedJDBCStore(reader, cacheAddress, operations); break; } } case MIXED_KEYED_JDBC_STORE: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseMixedKeyedJDBCStore(reader, cacheAddress, operations); break; } } case INDEXING: { if (this.schema.since(InfinispanSchema.VERSION_1_4) && !this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parseIndexing(reader, cacheAddress, operations); break; } } case OBJECT_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseHeapMemory(reader, cacheAddress, operations); break; } } case BINARY_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedElement(reader); } if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseBinaryMemory(reader, cacheAddress, operations); break; } } case OFF_HEAP_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseOffHeapMemory(reader, cacheAddress, operations); break; } } case HEAP_MEMORY: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { this.parseHeapMemory(reader, cacheAddress, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } private void parseSharedStateCacheElement(XMLExtendedStreamReader reader, PathAddress address, Map operations) throws XMLStreamException { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case STATE_TRANSFER: { this.parseStateTransfer(reader, address, operations); break; } case BACKUPS: { if (this.schema.since(InfinispanSchema.VERSION_2_0)) { this.parseBackups(reader, address, operations); break; } } case BACKUP_FOR: { if (this.schema.since(InfinispanSchema.VERSION_2_0) && !this.schema.since(InfinispanSchema.VERSION_5_0)) { this.parseBackupFor(reader, address, operations); break; } throw ParseUtils.unexpectedElement(reader); } case PARTITION_HANDLING: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { this.parsePartitionHandling(reader, address, operations); break; } } default: { this.parseCacheElement(reader, address, operations); } } } private void parsePartitionHandling(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(PartitionHandlingResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ENABLED: { readAttribute(reader, i, operation, PartitionHandlingResourceDefinition.Attribute.ENABLED); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseStateTransfer(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(StateTransferResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case TIMEOUT: { readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.TIMEOUT); break; } case FLUSH_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case CHUNK_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { readAttribute(reader, i, operation, StateTransferResourceDefinition.Attribute.CHUNK_SIZE); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseBackups(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BackupsResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BACKUP: { this.parseBackup(reader, address, operations); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseBackup(XMLExtendedStreamReader reader, PathAddress backupsAddress, Map operations) throws XMLStreamException { String site = require(reader, XMLAttribute.SITE); PathAddress address = backupsAddress.append(BackupResourceDefinition.pathElement(site)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SITE: { // Already parsed break; } case STRATEGY: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.STRATEGY); break; } case BACKUP_FAILURE_POLICY: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.FAILURE_POLICY); break; } case TIMEOUT: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.TIMEOUT); break; } case ENABLED: { readAttribute(reader, i, operation, BackupResourceDefinition.Attribute.ENABLED); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case TAKE_OFFLINE: { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case TAKE_OFFLINE_AFTER_FAILURES: { readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.AFTER_FAILURES); break; } case TAKE_OFFLINE_MIN_WAIT: { readAttribute(reader, i, operation, BackupResourceDefinition.TakeOfflineAttribute.MIN_WAIT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseBackupFor(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BackupForResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case REMOTE_CACHE: { readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.CACHE); break; } case REMOTE_SITE: { readAttribute(reader, i, operation, BackupForResourceDefinition.Attribute.SITE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseLocking(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(LockingResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case ISOLATION: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ISOLATION); break; } case STRIPING: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.STRIPING); break; } case ACQUIRE_TIMEOUT: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.ACQUIRE_TIMEOUT); break; } case CONCURRENCY_LEVEL: { readAttribute(reader, i, operation, LockingResourceDefinition.Attribute.CONCURRENCY); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseTransaction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(TransactionResourceDefinition.PATH); ModelNode operation = operations.get(address); if (operation == null) { operation = Util.createAddOperation(address); operations.put(address, operation); } for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STOP_TIMEOUT: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.STOP_TIMEOUT); break; } case MODE: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.MODE); break; } case LOCKING: { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.LOCKING); break; } case EAGER_LOCKING: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case COMPLETE_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, TransactionResourceDefinition.Attribute.COMPLETE_TIMEOUT); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseEviction(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case STRATEGY: { ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case MAX_ENTRIES: { readAttribute(reader, i, operation, HeapMemoryResourceDefinition.DeprecatedAttribute.MAX_ENTRIES); break; } case INTERVAL: { if (this.schema.since(InfinispanSchema.VERSION_1_1)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseExpiration(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(ExpirationResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_IDLE: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.MAX_IDLE); break; } case LIFESPAN: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.LIFESPAN); break; } case INTERVAL: { readAttribute(reader, i, operation, ExpirationResourceDefinition.Attribute.INTERVAL); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseIndexing(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { ModelNode operation = operations.get(cacheAddress); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case INDEX: { readAttribute(reader, i, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { Element element = Element.forName(reader.getLocalName()); switch (element) { case PROPERTY: { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, CacheResourceDefinition.DeprecatedAttribute.INDEXING_PROPERTIES); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SIZE_UNIT: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { readAttribute(reader, i, operation, HeapMemoryResourceDefinition.Attribute.SIZE_UNIT); break; } } default: { this.parseMemoryAttribute(reader, i, operation); } } } ParseUtils.requireNoContent(reader); } private void parseBinaryMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.BINARY_PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { this.parseBinaryMemoryAttribute(reader, i, operation); } ParseUtils.requireNoContent(reader); } private void parseOffHeapMemory(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(OffHeapMemoryResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CAPACITY: { readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.CAPACITY); break; } case SIZE_UNIT: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { readAttribute(reader, i, operation, OffHeapMemoryResourceDefinition.Attribute.SIZE_UNIT); break; } } default: { this.parseBinaryMemoryAttribute(reader, i, operation); } } } ParseUtils.requireNoContent(reader); } private void parseBinaryMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case EVICTION_TYPE: { readAttribute(reader, index, operation, OffHeapMemoryResourceDefinition.DeprecatedAttribute.EVICTION_TYPE); break; } default: { this.parseMemoryAttribute(reader, index, operation); } } } private void parseMemoryAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SIZE: { readAttribute(reader, index, operation, MemoryResourceDefinition.Attribute.SIZE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseCustomStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(CustomStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CLASS: { readAttribute(reader, i, operation, CustomStoreResourceDefinition.Attribute.CLASS); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } if (!operation.hasDefined(CustomStoreResourceDefinition.Attribute.CLASS.getName())) { throw ParseUtils.missingRequired(reader, EnumSet.of(XMLAttribute.CLASS)); } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseFileStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(FileStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case RELATIVE_TO: { readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_TO); break; } case PATH: { readAttribute(reader, i, operation, FileStoreResourceDefinition.Attribute.RELATIVE_PATH); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseRemoteStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(RemoteStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CACHE: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CACHE); break; } case SOCKET_TIMEOUT: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT); break; } // keycloak patch: begin case CONNECTION_TIMEOUT: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT); break; } // keycloak patch: end case TCP_NO_DELAY: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY); break; } case REMOTE_SERVERS: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS); break; } } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REMOTE_SERVER: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedElement(reader); } for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case OUTBOUND_SOCKET_BINDING: { readAttribute(reader, i, operation, RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); break; } default: { this.parseStoreElement(reader, address, operations); } } } if (!operation.hasDefined(RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS.getName())) { throw ParseUtils.missingRequired(reader, Collections.singleton(XMLAttribute.REMOTE_SERVERS.getLocalName())); } } private void parseHotRodStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(HotRodStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case CACHE_CONFIGURATION: { readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.CACHE_CONFIGURATION); break; } case REMOTE_CACHE_CONTAINER: { readAttribute(reader, i, operation, HotRodStoreResourceDefinition.Attribute.REMOTE_CACHE_CONTAINER); break; } default: { this.parseStoreAttribute(reader, i, operation); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { this.parseStoreElement(reader, address, operations); } } private void parseJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(JDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseLegacyJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { // We don't know the path yet PathAddress address = null; PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ENTRY_TABLE: { if (address != null) { this.removeStoreOperations(address, operations); } address = cacheAddress.append((address == null) ? StringKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH); Operations.setPathAddress(operation, address); ModelNode binaryTableOperation = operations.get(operationKey.append(BinaryTableResourceDefinition.PATH)); if (binaryTableOperation != null) { // Fix address of binary table operation Operations.setPathAddress(binaryTableOperation, address.append(BinaryTableResourceDefinition.PATH)); } this.parseJDBCStoreStringTable(reader, address, operations); break; } case BUCKET_TABLE: { if (address != null) { this.removeStoreOperations(address, operations); } address = cacheAddress.append((address == null) ? BinaryKeyedJDBCStoreResourceDefinition.PATH : MixedKeyedJDBCStoreResourceDefinition.PATH); Operations.setPathAddress(operation, address); ModelNode stringTableOperation = operations.get(operationKey.append(StringTableResourceDefinition.PATH)); if (stringTableOperation != null) { // Fix address of string table operation Operations.setPathAddress(stringTableOperation, address.append(StringTableResourceDefinition.PATH)); } this.parseJDBCStoreBinaryTable(reader, address, operations); break; } default: { if (address == null) { throw ParseUtils.missingOneOf(reader, EnumSet.of(XMLElement.ENTRY_TABLE, XMLElement.BUCKET_TABLE)); } this.parseStoreElement(reader, address, operations); } } } } private void parseBinaryKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(BinaryKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BINARY_KEYED_TABLE: { this.parseJDBCStoreBinaryTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseStringKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(StringKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case STRING_KEYED_TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseMixedKeyedJDBCStore(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(MixedKeyedJDBCStoreResourceDefinition.PATH); PathAddress operationKey = cacheAddress.append(StoreResourceDefinition.WILDCARD_PATH); if (operations.containsKey(operationKey)) { throw ParseUtils.unexpectedElement(reader); } ModelNode operation = Util.createAddOperation(address); operations.put(operationKey, operation); this.parseJDBCStoreAttributes(reader, operation); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case BINARY_KEYED_TABLE: { this.parseJDBCStoreBinaryTable(reader, address, operations); break; } case STRING_KEYED_TABLE: { this.parseJDBCStoreStringTable(reader, address, operations); break; } default: { this.parseStoreElement(reader, address, operations); } } } } private void parseJDBCStoreAttributes(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case DATASOURCE: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE); break; } case DIALECT: { if (this.schema.since(InfinispanSchema.VERSION_2_0)) { readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DIALECT); break; } } case DATA_SOURCE: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { readAttribute(reader, i, operation, JDBCStoreResourceDefinition.Attribute.DATA_SOURCE); break; } } default: { this.parseStoreAttribute(reader, i, operation); } } } Attribute requiredAttribute = this.schema.since(InfinispanSchema.VERSION_4_0) ? JDBCStoreResourceDefinition.Attribute.DATA_SOURCE : JDBCStoreResourceDefinition.DeprecatedAttribute.DATASOURCE; if (!operation.hasDefined(requiredAttribute.getName())) { throw ParseUtils.missingRequired(reader, requiredAttribute.getName()); } } private void parseJDBCStoreBinaryTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(BinaryTableResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(BinaryTableResourceDefinition.PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case PREFIX: { readAttribute(reader, i, operation, BinaryTableResourceDefinition.Attribute.PREFIX); break; } default: { this.parseJDBCStoreTableAttribute(reader, i, operation); } } } this.parseJDBCStoreTableElements(reader, operation); } private void parseJDBCStoreStringTable(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(StringTableResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH).append(StringTableResourceDefinition.PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case PREFIX: { readAttribute(reader, i, operation, StringTableResourceDefinition.Attribute.PREFIX); break; } default: { this.parseJDBCStoreTableAttribute(reader, i, operation); } } } this.parseJDBCStoreTableElements(reader, operation); } private void parseJDBCStoreTableAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case FETCH_SIZE: { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.FETCH_SIZE); break; } case BATCH_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { throw ParseUtils.unexpectedAttribute(reader, index); } readAttribute(reader, index, operation, TableResourceDefinition.DeprecatedAttribute.BATCH_SIZE); break; } case CREATE_ON_START: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.CREATE_ON_START); break; } } case DROP_ON_STOP: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, index, operation, TableResourceDefinition.Attribute.DROP_ON_STOP); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseJDBCStoreTableElements(XMLExtendedStreamReader reader, ModelNode operation) throws XMLStreamException { while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ID_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.ID, operation.get(TableResourceDefinition.ColumnAttribute.ID.getName()).setEmptyObject()); break; } case DATA_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.DATA, operation.get(TableResourceDefinition.ColumnAttribute.DATA.getName()).setEmptyObject()); break; } case TIMESTAMP_COLUMN: { this.parseJDBCStoreColumn(reader, ColumnAttribute.TIMESTAMP, operation.get(TableResourceDefinition.ColumnAttribute.TIMESTAMP.getName()).setEmptyObject()); break; } case SEGMENT_COLUMN: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { this.parseJDBCStoreColumn(reader, ColumnAttribute.SEGMENT, operation.get(TableResourceDefinition.ColumnAttribute.SEGMENT.getName()).setEmptyObject()); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseJDBCStoreColumn(XMLExtendedStreamReader reader, ColumnAttribute columnAttribute, ModelNode column) throws XMLStreamException { for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { readAttribute(reader, i, column, columnAttribute.getColumnName()); break; } case TYPE: { readAttribute(reader, i, column, columnAttribute.getColumnType()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void removeStoreOperations(PathAddress storeAddress, Map operations) { operations.remove(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH)); } private void parseStoreAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation) throws XMLStreamException { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(index)); switch (attribute) { case SHARED: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.SHARED); break; } case PRELOAD: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PRELOAD); break; } case PASSIVATION: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PASSIVATION); break; } case FETCH_STATE: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.FETCH_STATE); break; } case PURGE: { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.PURGE); break; } case SINGLETON: { readAttribute(reader, index, operation, StoreResourceDefinition.DeprecatedAttribute.SINGLETON); break; } case MAX_BATCH_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_5_0)) { readAttribute(reader, index, operation, StoreResourceDefinition.Attribute.MAX_BATCH_SIZE); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, index); } } } private void parseStoreElement(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { ModelNode operation = operations.get(storeAddress.getParent().append(StoreResourceDefinition.WILDCARD_PATH)); XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case PROPERTY: { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, StoreResourceDefinition.Attribute.PROPERTIES); break; } case WRITE_BEHIND: { if (this.schema.since(InfinispanSchema.VERSION_1_2)) { this.parseStoreWriteBehind(reader, storeAddress, operations); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } private void parseStoreWriteBehind(XMLExtendedStreamReader reader, PathAddress storeAddress, Map operations) throws XMLStreamException { PathAddress address = storeAddress.append(StoreWriteBehindResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(storeAddress.append(StoreWriteResourceDefinition.WILDCARD_PATH), operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case FLUSH_LOCK_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case MODIFICATION_QUEUE_SIZE: { readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.Attribute.MODIFICATION_QUEUE_SIZE); break; } case SHUTDOWN_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_4_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } ROOT_LOGGER.attributeDeprecated(attribute.getLocalName(), reader.getLocalName()); break; } case THREAD_POOL_SIZE: { if (this.schema.since(InfinispanSchema.VERSION_11_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, StoreWriteBehindResourceDefinition.DeprecatedAttribute.THREAD_POOL_SIZE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private

void parseThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map operations) throws XMLStreamException { PathAddress address = parentAddress.append(pool.getPathElement()); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MIN_THREADS: { if (pool.getMinThreads() != null) { readAttribute(reader, i, operation, pool.getMinThreads()); } break; } case MAX_THREADS: { readAttribute(reader, i, operation, pool.getMaxThreads()); break; } case QUEUE_LENGTH: { if (pool.getQueueLength() != null) { readAttribute(reader, i, operation, pool.getQueueLength()); } break; } case KEEPALIVE_TIME: { readAttribute(reader, i, operation, pool.getKeepAliveTime()); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private

void parseScheduledThreadPool(P pool, XMLExtendedStreamReader reader, PathAddress parentAddress, Map operations) throws XMLStreamException { PathAddress address = parentAddress.append(pool.getPathElement()); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_THREADS: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, pool.getMinThreads()); break; } case KEEPALIVE_TIME: { readAttribute(reader, i, operation, pool.getKeepAliveTime()); break; } case MIN_THREADS: { if (this.schema.since(InfinispanSchema.VERSION_10_0)) { readAttribute(reader, i, operation, pool.getMinThreads()); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteContainer(XMLExtendedStreamReader reader, PathAddress subsystemAddress, Map operations) throws XMLStreamException { String name = require(reader, XMLAttribute.NAME); PathAddress address = subsystemAddress.append(RemoteCacheContainerResourceDefinition.pathElement(name)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { ParseUtils.requireNoNamespaceAttribute(reader, i); XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case CONNECTION_TIMEOUT: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.CONNECTION_TIMEOUT); break; } case DEFAULT_REMOTE_CLUSTER: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.DEFAULT_REMOTE_CLUSTER); break; } case KEY_SIZE_ESTIMATE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.KEY_SIZE_ESTIMATE); break; } case MAX_RETRIES: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MAX_RETRIES); break; } case MODULE: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.MODULE); break; } case PROTOCOL_VERSION: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.PROTOCOL_VERSION); break; } case SOCKET_TIMEOUT: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.SOCKET_TIMEOUT); break; } case TCP_NO_DELAY: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_NO_DELAY); break; } case TCP_KEEP_ALIVE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TCP_KEEP_ALIVE); break; } case VALUE_SIZE_ESTIMATE: { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.DeprecatedAttribute.VALUE_SIZE_ESTIMATE); break; } case STATISTICS_ENABLED: { if (this.schema.since(InfinispanSchema.VERSION_9_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.STATISTICS_ENABLED); break; } } case MODULES: { if (this.schema.since(InfinispanSchema.VERSION_12_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.ListAttribute.MODULES); break; } } case MARSHALLER: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.MARSHALLER); break; } } case TRANSACTION_TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { readAttribute(reader, i, operation, RemoteCacheContainerResourceDefinition.Attribute.TRANSACTION_TIMEOUT); break; } } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case ASYNC_THREAD_POOL: { this.parseThreadPool(ThreadPoolResourceDefinition.CLIENT, reader, address, operations); break; } case CONNECTION_POOL: { this.parseConnectionPool(reader, address, operations); break; } case INVALIDATION_NEAR_CACHE: { this.parseInvalidationNearCache(reader, address, operations); break; } case REMOTE_CLUSTERS: { this.parseRemoteClusters(reader, address, operations); break; } case SECURITY: { this.parseRemoteCacheContainerSecurity(reader, address, operations); break; } case TRANSACTION: { if (this.schema.since(InfinispanSchema.VERSION_8_0)) { this.parseRemoteTransaction(reader, address, operations); break; } } case PROPERTY: { if (this.schema.since(InfinispanSchema.VERSION_11_0) || (this.schema.since(InfinispanSchema.VERSION_9_1) && !this.schema.since(InfinispanSchema.VERSION_10_0))) { ParseUtils.requireSingleAttribute(reader, XMLAttribute.NAME.getLocalName()); readElement(reader, operation, RemoteCacheContainerResourceDefinition.Attribute.PROPERTIES); break; } } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseInvalidationNearCache(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(InvalidationNearCacheResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MAX_ENTRIES: { readAttribute(reader, i, operation, InvalidationNearCacheResourceDefinition.Attribute.MAX_ENTRIES); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseConnectionPool(XMLExtendedStreamReader reader, PathAddress cacheAddress, Map operations) throws XMLStreamException { PathAddress address = cacheAddress.append(ConnectionPoolResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case EXHAUSTED_ACTION: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.EXHAUSTED_ACTION); break; } case MAX_ACTIVE: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_ACTIVE); break; } case MAX_WAIT: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MAX_WAIT); break; } case MIN_EVICTABLE_IDLE_TIME: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_EVICTABLE_IDLE_TIME); break; } case MIN_IDLE: { readAttribute(reader, i, operation, ConnectionPoolResourceDefinition.Attribute.MIN_IDLE); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteClusters(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { ParseUtils.requireNoAttributes(reader); while (reader.hasNext() && (reader.nextTag() != XMLStreamConstants.END_ELEMENT)) { XMLElement element = XMLElement.forName(reader.getLocalName()); switch (element) { case REMOTE_CLUSTER: { this.parseRemoteCluster(reader, containerAddress, operations); break; } default: { throw ParseUtils.unexpectedElement(reader); } } } } private void parseRemoteCluster(XMLExtendedStreamReader reader, PathAddress clustersAddress, Map operations) throws XMLStreamException { String remoteCluster = require(reader, XMLAttribute.NAME); PathAddress address = clustersAddress.append(RemoteClusterResourceDefinition.pathElement(remoteCluster)); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case NAME: { // Already parsed break; } case SOCKET_BINDINGS: { readAttribute(reader, i, operation, RemoteClusterResourceDefinition.Attribute.SOCKET_BINDINGS); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteCacheContainerSecurity(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(SecurityResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case SSL_CONTEXT: { readAttribute(reader, i, operation, SecurityResourceDefinition.Attribute.SSL_CONTEXT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private void parseRemoteTransaction(XMLExtendedStreamReader reader, PathAddress containerAddress, Map operations) throws XMLStreamException { PathAddress address = containerAddress.append(RemoteTransactionResourceDefinition.PATH); ModelNode operation = Util.createAddOperation(address); operations.put(address, operation); for (int i = 0; i < reader.getAttributeCount(); i++) { XMLAttribute attribute = XMLAttribute.forName(reader.getAttributeLocalName(i)); switch (attribute) { case MODE: { readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.MODE); break; } case TIMEOUT: { if (this.schema.since(InfinispanSchema.VERSION_13_0)) { throw ParseUtils.unexpectedAttribute(reader, i); } readAttribute(reader, i, operation, RemoteTransactionResourceDefinition.Attribute.TIMEOUT); break; } default: { throw ParseUtils.unexpectedAttribute(reader, i); } } } ParseUtils.requireNoContent(reader); } private static String require(XMLExtendedStreamReader reader, XMLAttribute attribute) throws XMLStreamException { String value = reader.getAttributeValue(null, attribute.getLocalName()); if (value == null) { throw ParseUtils.missingRequired(reader, attribute.getLocalName()); } return value; } private static ModelNode readAttribute(XMLExtendedStreamReader reader, int index, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); return definition.getParser().parse(definition, reader.getAttributeValue(index), reader); } private static void readAttribute(XMLExtendedStreamReader reader, int index, ModelNode operation, Attribute attribute) throws XMLStreamException { setAttribute(reader, reader.getAttributeValue(index), operation, attribute); } private static void setAttribute(XMLExtendedStreamReader reader, String value, ModelNode operation, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); definition.getParser().parseAndSetParameter(definition, value, operation, reader); } private static void readElement(XMLExtendedStreamReader reader, ModelNode operation, Attribute attribute) throws XMLStreamException { AttributeDefinition definition = attribute.getDefinition(); AttributeParser parser = definition.getParser(); if (parser.isParseAsElement()) { parser.parseElement(definition, reader, operation); } else { parser.parseAndSetParameter(definition, reader.getElementText(), operation, reader); } } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/LocalDescriptions.properties ================================================ # subsystem resource infinispan=The configuration of the infinispan subsystem. infinispan.add=Add the infinispan subsystem. infinispan.describe=Describe the infinispan subsystem infinispan.remove=Remove the infinispan subsystem # cache container resource infinispan.cache-container=The configuration of an infinispan cache container infinispan.cache-container.default-cache=The default infinispan cache infinispan.cache-container.listener-executor=The executor used for the replication queue infinispan.cache-container.listener-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.eviction-executor=The scheduled executor used for eviction infinispan.cache-container.eviction-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.replication-queue-executor=The executor used for asynchronous cache operations infinispan.cache-container.replication-queue-executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.cache-container.jndi-name=The jndi name to which to bind this cache container infinispan.cache-container.jndi-name.deprecated=Deprecated. Will be ignored. infinispan.cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries. infinispan.cache-container.module=The module associated with this cache container's configuration. infinispan.cache-container.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.cache-container.modules=The set of modules associated with this cache container's configuration. infinispan.cache-container.start=The cache container start mode, which can be EAGER (immediate start) or LAZY (on-demand start). infinispan.cache-container.start.deprecated=Deprecated. Future releases will only support LAZY mode. infinispan.cache-container.statistics-enabled=If enabled, statistics will be collected for this cache container infinispan.cache-container.thread-pool=Defines thread pools for this cache container infinispan.cache-container.cache=The list of caches available to this cache container infinispan.cache-container.singleton=A set of single-instance configuration elements of the cache container. infinispan.cache-container.aliases=The list of aliases for this cache container infinispan.cache-container.add-alias=Add an alias for this cache container infinispan.cache-container.add-alias.name=The name of the alias to add to this cache container infinispan.cache-container.add-alias.deprecated=Deprecated. Use list-add operation instead. infinispan.cache-container.remove-alias=Remove an alias for this cache container infinispan.cache-container.remove-alias.name=The name of the alias to remove from this cache container infinispan.cache-container.remove-alias.deprecated=Deprecated. Use list-remove operation instead. infinispan.cache-container.add=Add a cache container to the infinispan subsystem infinispan.cache-container.remove=Remove a cache container from the infinispan subsystem # cache container read-only metrics infinispan.cache-container.cache-manager-status=The status of the cache manager component. May return null if the cache manager is not started. infinispan.cache-container.cache-manager-status.deprecated=Deprecated. Always returns RUNNING. infinispan.cache-container.is-coordinator=Set to true if this node is the cluster's coordinator. May return null if the cache manager is not started. infinispan.cache-container.coordinator-address=The logical address of the cluster's coordinator. May return null if the cache manager is not started. infinispan.cache-container.local-address=The local address of the node. May return null if the cache manager is not started. infinispan.cache-container.cluster-name=The name of the cluster this node belongs to. May return null if the cache manager is not started. # cache container children infinispan.cache-container.transport=A transport child of the cache container. infinispan.cache-container.local-cache=A local cache child of the cache container. infinispan.cache-container.invalidation-cache=An invalidation cache child of the cache container. infinispan.cache-container.replicated-cache=A replicated cache child of the cache container. infinispan.cache-container.distributed-cache=A distributed cache child of the cache container. # thread-pool resources infinispan.thread-pool.deprecated=This thread pool is deprecated and will be ignored. infinispan.thread-pool.async-operations=Defines a thread pool used for asynchronous operations. infinispan.thread-pool.listener=Defines a thread pool used for asynchronous cache listener notifications. infinispan.thread-pool.persistence=Defines a thread pool used for interacting with the persistent store. infinispan.thread-pool.remote-command=Defines a thread pool used to execute remote commands. infinispan.thread-pool.state-transfer=Defines a thread pool used for for state transfer. infinispan.thread-pool.state-transfer.deprecated=Deprecated. Has no effect. infinispan.thread-pool.transport=Defines a thread pool used for asynchronous transport communication. infinispan.thread-pool.expiration=Defines a thread pool used for for evictions. infinispan.thread-pool.blocking=Defines a thread pool used for for blocking operations. infinispan.thread-pool.non-blocking=Defines a thread pool used for for non-blocking operations. infinispan.thread-pool.add=Adds a thread pool executor. infinispan.thread-pool.remove=Removes a thread pool executor. infinispan.thread-pool.min-threads=The core thread pool size which is smaller than the maximum pool size. If undefined, the core thread pool size is the same as the maximum thread pool size. infinispan.thread-pool.min-threads.deprecated=Deprecated. Has no effect. infinispan.thread-pool.max-threads=The maximum thread pool size. infinispan.thread-pool.max-threads.deprecated=Deprecated. Use min-threads instead. infinispan.thread-pool.queue-length=The queue length. infinispan.thread-pool.queue-length.deprecated=Deprecated. Has no effect. infinispan.thread-pool.keepalive-time=Used to specify the amount of milliseconds that pool threads should be kept running when idle; if not specified, threads will run until the executor is shut down. # transport resource infinispan.transport.jgroups=The description of the transport used by this cache container infinispan.transport.jgroups.add=Add the transport to the cache container infinispan.transport.jgroups.remove=Remove the transport from the cache container infinispan.transport.jgroups.channel=The channel of this cache container's transport. infinispan.transport.jgroups.cluster=The name of the group communication cluster infinispan.transport.jgroups.cluster.deprecated=Deprecated. The cluster used by the transport of this cache container is configured via the JGroups subsystem. infinispan.transport.jgroups.executor=The executor to use for the transport infinispan.transport.jgroups.executor.deprecated=Deprecated. This will be replaced by thread pool configuration embedded in the subsystem in a future release. infinispan.transport.jgroups.lock-timeout=The timeout for locks for the transport infinispan.transport.jgroups.machine=A machine identifier for the transport infinispan.transport.jgroups.rack=A rack identifier for the transport infinispan.transport.jgroups.site=A site identifier for the transport infinispan.transport.jgroups.stack=The jgroups stack to use for the transport infinispan.transport.jgroups.stack.deprecated=Deprecated. The protocol stack used by the transport of this cache container is configured via the JGroups subsystem. infinispan.transport.none=A local-only transport used by this cache-container infinispan.transport.none.add=Adds a local transport to this cache container infinispan.transport.none.remove=Removes a local transport from this cache container # (hierarchical) cache resource infinispan.cache.start=The cache start mode, which can be EAGER (immediate start) or LAZY (on-demand start). infinispan.cache.start.deprecated=Deprecated. Only LAZY mode is supported. infinispan.cache.statistics-enabled=If enabled, statistics will be collected for this cache infinispan.cache.batching=If enabled, the invocation batching API will be made available for this cache. infinispan.cache.batching.deprecated=Deprecated. Replaced by BATCH transaction mode. infinispan.cache.indexing=If enabled, entries will be indexed when they are added to the cache. Indexes will be updated as entries change or are removed. infinispan.cache.indexing.deprecated=Deprecated. Has no effect. infinispan.cache.jndi-name=The jndi-name to which to bind this cache instance. infinispan.cache.jndi-name.deprecated=Deprecated. Will be ignored. infinispan.cache.module=The module associated with this cache's configuration. infinispan.cache.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.cache.modules=The set of modules associated with this cache's configuration. infinispan.cache.indexing-properties=Properties to control indexing behaviour infinispan.cache.indexing-properties.deprecated=Deprecated. Has no effect. infinispan.cache.remove=Remove a cache from this container. # cache read-only metrics infinispan.cache.cache-status=The status of the cache component. infinispan.cache.cache-status.deprecated=Deprecated. Always returns RUNNING. infinispan.cache.average-read-time=Average time (in ms) for cache reads. Includes hits and misses. infinispan.cache.average-read-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.average-remove-time=Average time (in ms) for cache removes. infinispan.cache.average-remove-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.average-write-time=Average time (in ms) for cache writes. infinispan.cache.average-write-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.elapsed-time=Time (in secs) since cache started. infinispan.cache.elapsed-time.deprecated=Deprecated. Use time-since-start instead. infinispan.cache.hit-ratio=The hit/miss ratio for the cache (hits/hits+misses). infinispan.cache.hit-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.hits=The number of cache attribute hits. infinispan.cache.hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.misses=The number of cache attribute misses. infinispan.cache.misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.number-of-entries=The number of entries in the cache including passivated entries. infinispan.cache.number-of-entries.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.number-of-entries-in-memory=The number of entries in the cache excluding passivated entries. infinispan.cache.read-write-ratio=The read/write ratio of the cache ((hits+misses)/stores). infinispan.cache.read-write-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.remove-hits=The number of cache attribute remove hits. infinispan.cache.remove-hits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.remove-misses=The number of cache attribute remove misses. infinispan.cache.remove-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.stores=The number of cache attribute put operations. infinispan.cache.stores.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.time-since-reset=Time (in secs) since cache statistics were reset. infinispan.cache.time-since-reset.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.time-since-start=Time (in secs) since cache was started. infinispan.cache.writes=The number of cache attribute put operations. infinispan.cache.invalidations=The number of cache invalidations. infinispan.cache.invalidations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.passivations=The number of cache node passivations (passivating a node from memory to a cache store). infinispan.cache.passivations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.activations=The number of cache node activations (bringing a node into memory from a cache store). infinispan.cache.activations.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # infinispan.cache.async-marshalling=If enabled, this will cause marshalling of entries to be performed asynchronously. infinispan.cache.async-marshalling.deprecated=Deprecated. Asynchronous marshalling is no longer supported. infinispan.cache.mode=Sets the clustered cache mode, ASYNC for asynchronous operation, or SYNC for synchronous operation. infinispan.cache.mode.deprecated=Deprecated. This attribute will be ignored. All cache modes will be treated as SYNC. To perform asynchronous cache operations, use Infinispan's asynchronous cache API. infinispan.cache.queue-size=In ASYNC mode, this attribute can be used to trigger flushing of the queue when it reaches a specific threshold. infinispan.cache.queue-size.deprecated=Deprecated. This attribute will be ignored. infinispan.cache.queue-flush-interval=In ASYNC mode, this attribute controls how often the asynchronous thread used to flush the replication queue runs. This should be a positive integer which represents thread wakeup time in milliseconds. infinispan.cache.queue-flush-interval.deprecated=Deprecated. This attribute will be ignored. infinispan.cache.remote-timeout=In SYNC mode, the timeout (in ms) used to wait for an acknowledgment when making a remote call, after which the call is aborted and an exception is thrown. # metrics infinispan.cache.average-replication-time=The average time taken to replicate data around the cluster. infinispan.cache.average-replication-time.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.replication-count=The number of times data was replicated around the cluster. infinispan.cache.replication-count.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.replication-failures=The number of data replication failures. infinispan.cache.replication-failures.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.cache.success-ratio=The data replication success ratio (successes/successes+failures). infinispan.cache.success-ratio.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # operations infinispan.cache.reset-statistics=Reset the statistics for this cache. #child resource aliases infinispan.cache.memory=Alias to the eviction configuration component infinispan.cache.eviction=Alias to the memory=object resource infinispan.cache.expiration=Alias to the expiration configuration component infinispan.cache.locking=Alias to the locking configuration component infinispan.cache.state-transfer=Alias to the state-transfer configuration component infinispan.cache.transaction=Alias to the transaction configuration component infinispan.cache.file-store=Alias to the file store configuration component infinispan.cache.remote-store=Alias to the file store configuration component infinispan.cache.binary-keyed-jdbc-store=Alias to the binary jdbc store configuration component infinispan.cache.mixed-keyed-jdbc-store=Alias to the mixed jdbc store configuration component infinispan.cache.string-keyed-jdbc-store=Alias to the string jdbc store configuration component infinispan.cache.write-behind=Alias to the write behind configuration component infinispan.cache.backup-for=Alias to the backup-for configuration component infinispan.cache.backup=Alias to the backup child of the backups configuration infinispan.cache.segments=Controls the number of hash space segments which is the granularity for key distribution in the cluster. Value must be strictly positive. infinispan.cache.consistent-hash-strategy=Defines the consistent hash strategy for the cache. infinispan.cache.consistent-hash-strategy.deprecated=Deprecated. Segment allocation is no longer customizable. infinispan.cache.evictions=The number of cache eviction operations. infinispan.local-cache=A local cache configuration infinispan.local-cache.add=Add a local cache to this cache container infinispan.local-cache.remove=Remove a local cache from this cache container infinispan.invalidation-cache=An invalidation cache infinispan.invalidation-cache.add=Add an invalidation cache to this cache container infinispan.invalidation-cache.remove=Remove an invalidation cache from this cache container infinispan.replicated-cache=A replicated cache configuration infinispan.replicated-cache.add=Add a replicated cache to this cache container infinispan.replicated-cache.remove=Remove a replicated cache from this cache container infinispan.component.partition-handling=The partition handling configuration for distributed and replicated caches. infinispan.component.partition-handling.add=Add a partition handling configuration. infinispan.component.partition-handling.remove=Remove a partition handling configuration. infinispan.component.partition-handling.enabled=If enabled, the cache will enter degraded mode upon detecting a network partition that threatens the integrity of the cache. infinispan.component.partition-handling.availability=Indicates the current availability of the cache. infinispan.component.partition-handling.availability.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.partition-handling.force-available=Forces a cache with degraded availability to become available. infinispan.component.partition-handling.force-available.deprecated=Deprecated. Use operation from corresponding runtime cache resource. infinispan.component.state-transfer=The state transfer configuration for distributed and replicated caches. infinispan.component.state-transfer.add=Add a state transfer configuration. infinispan.component.state-transfer.remove=Remove a state transfer configuration. infinispan.component.state-transfer.enabled=If enabled, this will cause the cache to ask neighboring caches for state when it starts up, so the cache starts 'warm', although it will impact startup time. infinispan.component.state-transfer.enabled.deprecated=Deprecated. Always enabled for replicated and distributed caches. infinispan.component.state-transfer.timeout=The maximum amount of time (ms) to wait for state from neighboring caches, before throwing an exception and aborting startup. If timeout is 0, state transfer is performed asynchronously, and the cache will be immediately available. infinispan.component.state-transfer.chunk-size=The maximum number of cache entries in a batch of transferred state. infinispan.distributed-cache=A distributed cache configuration. infinispan.distributed-cache.add=Add a distributed cache to this cache container infinispan.distributed-cache.remove=Remove a distributed cache from this cache container infinispan.distributed-cache.owners=Number of cluster-wide replicas for each cache entry. infinispan.distributed-cache.virtual-nodes=Deprecated. Has no effect. infinispan.distributed-cache.virtual-nodes.deprecated=Deprecated. Has no effect. infinispan.distributed-cache.l1-lifespan=Maximum lifespan of an entry placed in the L1 cache. This element configures the L1 cache behavior in 'distributed' caches instances. In any other cache modes, this element is ignored. infinispan.distributed-cache.capacity-factor=Controls the proportion of entries that will reside on the local node, compared to the other nodes in the cluster. infinispan.scattered-cache=A scattered cache configuration. infinispan.scattered-cache.add=Add a scattered cache to this cache container infinispan.scattered-cache.remove=Remove a scattered cache from this cache container infinispan.scattered-cache.bias-lifespan=When greater than zero, specifies the duration (in ms) that a cache entry will be cached on a non-owner following a write operation. infinispan.scattered-cache.invalidation-batch-size=The threshold after which batched invalidations are sent. infinispan.cache.store=A persistent store for a cache. infinispan.cache.component=A configuration component of a cache. infinispan.component.locking=The locking configuration of the cache. infinispan.component.locking.add=Adds a locking configuration element to the cache. infinispan.component.locking.remove=Removes a locking configuration element from the cache. infinispan.component.locking.isolation=Sets the cache locking isolation level. infinispan.component.locking.striping=If true, a pool of shared locks is maintained for all entries that need to be locked. Otherwise, a lock is created per entry in the cache. Lock striping helps control memory footprint but may reduce concurrency in the system. infinispan.component.locking.acquire-timeout=Maximum time to attempt a particular lock acquisition. infinispan.component.locking.concurrency-level=Concurrency level for lock containers. Adjust this value according to the number of concurrent threads interacting with Infinispan. # metrics infinispan.component.locking.current-concurrency-level=The estimated number of concurrently updating threads which this cache can support. infinispan.component.locking.current-concurrency-level.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.locking.number-of-locks-available=The number of locks available to this cache. infinispan.component.locking.number-of-locks-available.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.locking.number-of-locks-held=The number of locks currently in use by this cache. infinispan.component.locking.number-of-locks-held.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction=The cache transaction configuration. infinispan.component.transaction.deprecated=Deprecated. Transactional behavior should be defined per remote-cache. infinispan.component.transaction.add=Adds a transaction configuration element to the cache. infinispan.component.transaction.complete-timeout=The duration (in ms) after which idle transactions are removed. infinispan.component.transaction.remove=Removes a transaction configuration element from the cache. infinispan.component.transaction.mode=Sets the cache transaction mode to one of NONE, NON_XA, NON_DURABLE_XA, FULL_XA. infinispan.component.transaction.stop-timeout=If there are any ongoing transactions when a cache is stopped, Infinispan waits for ongoing remote and local transactions to finish. The amount of time to wait for is defined by the cache stop timeout. infinispan.component.transaction.locking=The locking mode for this cache, one of OPTIMISTIC or PESSIMISTIC. infinispan.component.transaction.timeout=The duration (in ms) after which idle transactions are rolled back. # metrics infinispan.component.transaction.commits=The number of transaction commits. infinispan.component.transaction.commits.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction.prepares=The number of transaction prepares. infinispan.component.transaction.prepares.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.transaction.rollbacks=The number of transaction rollbacks. infinispan.component.transaction.rollbacks.deprecated=Deprecated. Use metric from corresponding runtime cache resource. # infinispan.memory.heap=On-heap object-based memory configuration. infinispan.memory.off-heap=Off-heap memory configuration. infinispan.memory.add=Adds a memory configuration element to the cache. infinispan.memory.remove=Removes an eviction configuration element from the cache. infinispan.memory.size=Eviction threshold, as defined by the size unit. infinispan.memory.size-unit=The unit of the eviction threshold. infinispan.memory.object.size=Triggers eviction of the least recently used entries when the number of cache entries exceeds this threshold. infinispan.memory.eviction-type=Indicates whether the size attribute refers to the number of cache entries (i.e. COUNT) or the collective size of the cache entries (i.e. MEMORY). infinispan.memory.eviction-type.deprecated=Deprecated. Replaced by size-unit. infinispan.memory.capacity=Defines the capacity of the off-heap storage. infinispan.memory.capacity.deprecated=Deprecated. Will be ignored. infinispan.memory.strategy=Sets the cache eviction strategy. Available options are 'UNORDERED', 'FIFO', 'LRU', 'LIRS' and 'NONE' (to disable eviction). infinispan.memory.strategy.deprecated=Deprecated. Eviction uses LRU and is disabled via undefining the size attribute. infinispan.memory.max-entries=Maximum number of entries in a cache instance. If selected value is not a power of two the actual value will default to the least power of two larger than selected value. -1 means no limit. infinispan.memory.max-entries.deprecated=Deprecated. Use the size attribute instead. # metrics infinispan.memory.evictions=The number of cache eviction operations. infinispan.memory.evictions.deprecated=Deprecated. Use corresponding metric on parent resource. # infinispan.component.expiration=The cache expiration configuration. infinispan.component.expiration.add=Adds an expiration configuration element to the cache. infinispan.component.expiration.remove=Removes an expiration configuration element from the cache. infinispan.component.expiration.max-idle=Maximum idle time a cache entry will be maintained in the cache, in milliseconds. If the idle time is exceeded, the entry will be expired cluster-wide. -1 means the entries never expire. infinispan.component.expiration.lifespan=Maximum lifespan of a cache entry, after which the entry is expired cluster-wide, in milliseconds. -1 means the entries never expire. infinispan.component.expiration.interval=Interval (in milliseconds) between subsequent runs to purge expired entries from memory and any cache stores. If you wish to disable the periodic eviction process altogether, set wakeupInterval to -1. infinispan.store.custom=The cache store configuration. infinispan.store.custom.add=Adds a basic cache store configuration element to the cache. infinispan.store.custom.remove=Removes a cache store configuration element from the cache. infinispan.store.shared=This setting should be set to true when multiple cache instances share the same cache store (e.g., multiple nodes in a cluster using a JDBC-based CacheStore pointing to the same, shared database.) Setting this to true avoids multiple cache instances writing the same modification multiple times. If enabled, only the node where the modification originated will write to the cache store. If disabled, each individual cache reacts to a potential remote update by storing the data to the cache store. infinispan.store.preload=If true, when the cache starts, data stored in the cache store will be pre-loaded into memory. This is particularly useful when data in the cache store will be needed immediately after startup and you want to avoid cache operations being delayed as a result of loading this data lazily. Can be used to provide a 'warm-cache' on startup, however there is a performance penalty as startup time is affected by this process. infinispan.store.passivation=If true, data is only written to the cache store when it is evicted from memory, a phenomenon known as 'passivation'. Next time the data is requested, it will be 'activated' which means that data will be brought back to memory and removed from the persistent store. If false, the cache store contains a copy of the contents in memory, so writes to cache result in cache store writes. This essentially gives you a 'write-through' configuration. infinispan.store.fetch-state=If true, fetch persistent state when joining a cluster. If multiple cache stores are chained, only one of them can have this property enabled. infinispan.store.purge=If true, purges this cache store when it starts up. infinispan.store.max-batch-size=The maximum size of a batch to be inserted/deleted from the store. If the value is less than one, then no upper limit is placed on the number of operations in a batch. infinispan.store.singleton=If true, the singleton store cache store is enabled. SingletonStore is a delegating cache store used for situations when only one instance in a cluster should interact with the underlying store. infinispan.store.singleton.deprecated=Deprecated. Consider using a shared store instead, where writes are only performed by primary owners. infinispan.store.class=The custom store implementation class to use for this cache store. infinispan.store.write-behind=Child to configure a cache store as write-behind instead of write-through. infinispan.store.properties=A list of cache store properties. infinispan.store.properties.property=A cache store property with name and value. infinispan.store.property=A cache store property with name and value. infinispan.store.write=The write behavior of the cache store. # metrics infinispan.store.cache-loader-loads=The number of cache loader node loads. infinispan.store.cache-loader-loads.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.store.cache-loader-misses=The number of cache loader node misses. infinispan.store.cache-loader-misses.deprecated=Deprecated. Use metric from corresponding runtime cache resource. infinispan.component.persistence.cache-loader-loads=The number of entries loaded by this cache loader. infinispan.component.persistence.cache-loader-misses=The number of entry load misses by this cache loader. infinispan.write.behind=Configures a cache store as write-behind instead of write-through. infinispan.write.behind.add=Adds a write-behind configuration element to the store. infinispan.write.behind.remove=Removes a write-behind configuration element from the store. infinispan.write.behind.flush-lock-timeout=Timeout to acquire the lock which guards the state to be flushed to the cache store periodically. infinispan.write.behind.flush-lock-timeout.deprecated=Deprecated. This attribute is no longer used. infinispan.write.behind.modification-queue-size=Maximum number of entries in the asynchronous queue. When the queue is full, the store becomes write-through until it can accept new entries. infinispan.write.behind.shutdown-timeout=Timeout in milliseconds to stop the cache store. infinispan.write.behind.shutdown-timeout.deprecated=Deprecated. This attribute is no longer used. infinispan.write.behind.thread-pool-size=Size of the thread pool whose threads are responsible for applying the modifications to the cache store. infinispan.write.behind.thread-pool-size.deprecated=Deprecated. Uses size of non-blocking thread pool. infinispan.write.through=Configures a cache store as write-through. infinispan.write.through.add=Add a write-through configuration to the store. infinispan.write.through.remove=Remove a write-through configuration to the store. infinispan.property=A cache store property with name and value. infinispan.property.deprecated=Deprecated. Use "properties" attribute of the appropriate cache store resource. infinispan.property.add=Adds a cache store property. infinispan.property.remove=Removes a cache store property. infinispan.property.value=The value of the cache store property. infinispan.store.none=A store-less configuration. infinispan.store.none.add=Adds a store-less configuration to this cache infinispan.store.none.remove=Removes a store-less configuration from this cache infinispan.store.file=The cache file store configuration. infinispan.store.file.add=Adds a file cache store configuration element to the cache. infinispan.store.file.remove=Removes a cache file store configuration element from the cache. infinispan.store.file.relative-to=The system path to which the specified path is relative. infinispan.store.file.path=The system path under which this cache store will persist its entries. infinispan.store.jdbc=The cache JDBC store configuration. infinispan.store.jdbc.add=Adds a JDBC cache store configuration element to the cache. infinispan.store.jdbc.remove=Removes a JDBC cache store configuration element to the cache. infinispan.store.jdbc.data-source=References the data source used to connect to this store. infinispan.store.jdbc.datasource=The jndi name of the data source used to connect to this store. infinispan.store.jdbc.datasource.deprecated=Deprecated. Replaced by data-source. infinispan.store.jdbc.dialect=The dialect of this datastore. infinispan.store.jdbc.table=Defines a table used to store persistent cache data. infinispan.store.jdbc.binary-keyed-table=Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.store.jdbc.binary-keyed-table.deprecated=Deprecated. Use table=binary child resource. infinispan.store.jdbc.binary-keyed-table.table.prefix=The prefix for the database table name. infinispan.store.jdbc.binary-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.store.jdbc.binary-keyed-table.table.batch-size.deprecated=Deprecated. Use max-batch-size instead. infinispan.store.jdbc.binary-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.store.jdbc.binary-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.store.jdbc.binary-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.store.jdbc.binary-keyed-table.table.column.name= infinispan.store.jdbc.binary-keyed-table.table.column.type= infinispan.store.jdbc.binary-keyed-table.table.id-column=A database column to hold cache entry ids. infinispan.store.jdbc.binary-keyed-table.table.id-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.id-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.data-column=A database column to hold cache entry data. infinispan.store.jdbc.binary-keyed-table.table.data-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.data-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.segment-column=A database column to hold cache entry segment. infinispan.store.jdbc.binary-keyed-table.table.segment-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.segment-column.column.type=The type of the database column. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.name=The name of the database column. infinispan.store.jdbc.binary-keyed-table.table.timestamp-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table=Defines a table used to store persistent cache entries. infinispan.store.jdbc.string-keyed-table.deprecated=Deprecated. Use table=string child resource. infinispan.store.jdbc.string-keyed-table.table.prefix=The prefix for the database table name. infinispan.store.jdbc.string-keyed-table.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.store.jdbc.string-keyed-table.table.batch-size.deprecated=Deprecated. Use max-batch-size instead. infinispan.store.jdbc.string-keyed-table.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.store.jdbc.string-keyed-table.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.store.jdbc.string-keyed-table.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.store.jdbc.string-keyed-table.table.column.name= infinispan.store.jdbc.string-keyed-table.table.column.type= infinispan.store.jdbc.string-keyed-table.table.id-column=A database column to hold cache entry ids. infinispan.store.jdbc.string-keyed-table.table.id-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.id-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.data-column=A database column to hold cache entry data. infinispan.store.jdbc.string-keyed-table.table.data-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.data-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.segment-column=A database column to hold cache entry segment. infinispan.store.jdbc.string-keyed-table.table.segment-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.segment-column.column.type=The type of the database column. infinispan.store.jdbc.string-keyed-table.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.name=The name of the database column. infinispan.store.jdbc.string-keyed-table.table.timestamp-column.column.type=The type of the database column. infinispan.store.binary-jdbc.deprecated=Deprecated. Will be removed without replacement in a future release. Use store=jdbc instead. infinispan.store.mixed-jdbc.deprecated=Deprecated. Will be removed without replacement in a future release. Use store=jdbc instead. infinispan.table.binary=Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.deprecated=Deprecated. Defines a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.add=Adds a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.binary.remove=Removes a table used to store cache entries whose keys cannot be expressed as strings. infinispan.table.string=Defines a table used to store cache entries whose keys can be expressed as strings. infinispan.table.string.add=Adds a table used to store cache entries whose keys can be expressed as strings. infinispan.table.string.remove=Removes a table used to store cache entries whose keys can be expressed as strings. infinispan.table.prefix=The prefix for the database table name. infinispan.table.batch-size=For DB inserts, the batch size determines how many inserts are batched together. infinispan.table.batch-size.deprecated=Deprecated. Use max-batch-size instead. infinispan.table.fetch-size=For DB queries, the fetch size will be used to set the fetch size on ResultSets. infinispan.table.create-on-start=Indicates whether the store should create this database table when the cache starts. infinispan.table.drop-on-stop=Indicates whether the store should drop this database table when the cache stops. infinispan.table.id-column=A database column to hold cache entry ids. infinispan.table.id-column.column.name=The name of the database column. infinispan.table.id-column.column.type=The type of the database column. infinispan.table.data-column=A database column to hold cache entry data. infinispan.table.data-column.column.name=The name of the database column. infinispan.table.data-column.column.type=The type of the database column. infinispan.table.segment-column=A database column to hold cache entry segment. infinispan.table.segment-column.column.name=The name of the database column. infinispan.table.segment-column.column.type=The type of the database column. infinispan.table.timestamp-column=A database column to hold cache entry timestamps. infinispan.table.timestamp-column.column.name=The name of the database column. infinispan.table.timestamp-column.column.type=The type of the database column. # /subsystem=infinispan/cache-container=X/cache=Y/store=remote infinispan.store.remote=The cache remote store configuration. infinispan.store.remote.deprecated=Use HotRod store instead. infinispan.store.remote.cache=The name of the remote cache to use for this remote store. infinispan.store.remote.tcp-no-delay=A TCP_NODELAY value for remote cache communication. infinispan.store.remote.socket-timeout=A socket timeout for remote cache communication. # keycloak patch: begin infinispan.store.remote.connection-timeout=A connect timeout for remote cache communication. # keycloak patch: end infinispan.store.remote.remote-servers=A list of remote servers for this cache store. infinispan.store.remote.remote.servers.remote-server=A remote server, defined by its outbound socket binding. infinispan.store.remote.remote-servers.remote-server.outbound-socket-binding=An outbound socket binding for a remote server. infinispan.store.remote.add=Adds a remote cache store configuration element to the cache. infinispan.store.remote.remove=Removes a cache remote store configuration element from the cache. # /subsystem=infinispan/cache-container=X/cache=Y/store=hotrod infinispan.store.hotrod=HotRod-based store using Infinispan Server instance to store data. infinispan.store.hotrod.add=Adds HotRod store. infinispan.store.hotrod.remove=Removes HotRod store. infinispan.store.hotrod.cache-configuration=Name of the cache configuration template defined in Infinispan Server to create caches from. infinispan.store.hotrod.remote-cache-container=Reference to a container-managed remote-cache-container. infinispan.backup=A backup site to which to replicate this cache. infinispan.backup.add=Adds a backup site to this cache. infinispan.backup.remove=Removes a backup site from this cache. infinispan.backup.strategy=The backup strategy for this cache infinispan.backup.failure-policy=The policy to follow when connectivity to the backup site fails. infinispan.backup.enabled=Indicates whether or not this backup site is enabled. infinispan.backup.timeout=The timeout for replicating to the backup site. infinispan.backup.after-failures=Indicates the number of failures after which this backup site should go offline. infinispan.backup.min-wait=Indicates the minimum time (in milliseconds) to wait after the max number of failures is reached, after which this backup site should go offline. # cross-site backup operations infinispan.backup.site-status=Displays the current status of the backup site. infinispan.backup.bring-site-online=Re-enables a previously disabled backup site. infinispan.backup.take-site-offline=Disables backup to a remote site. infinispan.component.backup-for=A cache for which this cache acts as a backup (for use with cross site replication). infinispan.component.backup-for.deprecated=Deprecated. Backup designation must match the current cache name. infinispan.component.backup-for.add=Adds a backup designation for this cache. infinispan.component.backup-for.remove=Removes a backup designation for this cache. infinispan.component.backup-for.remote-cache=The name of the remote cache for which this cache acts as a backup. infinispan.component.backup-for.remote-cache.deprecated=This resource is deprecated. infinispan.component.backup-for.remote-site=The site of the remote cache for which this cache acts as a backup. infinispan.component.backup-for.remote-site.deprecated=This resource is deprecated. infinispan.component.backups=The remote backups for this cache. infinispan.component.backups.add=Adds remote backup support to this cache. infinispan.component.backups.remove=Removes remote backup support from this cache. infinispan.component.backups.backup=A remote backup. # /subsystem=infinispan/remote-cache-container=* infinispan.remote-cache-container=The configuration of a remote Infinispan cache container. infinispan.remote-cache-container.add=Add a remote cache container to the infinispan subsystem. infinispan.remote-cache-container.remove=Remove a cache container from the infinispan subsystem. infinispan.remote-cache-container.component=A configuration component of a remote cache container. infinispan.remote-cache-container.thread-pool=Defines thread pools for this remote cache container. infinispan.remote-cache-container.near-cache=Configures near caching. infinispan.remote-cache-container.connection-timeout=Defines the maximum socket connect timeout before giving up connecting to the server. infinispan.remote-cache-container.default-remote-cluster=Required default remote server cluster. infinispan.remote-cache-container.key-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing keys, to minimize array resizing. infinispan.remote-cache-container.key-size-estimate.deprecated=Deprecated. This attribute will be ignored. infinispan.remote-cache-container.max-retries=Sets the maximum number of retries for each request. A valid value should be greater or equals than 0. Zero means no retry will made in case of a network failure. infinispan.remote-cache-container.marshaller=Defines the marshalling implementation used to marshal cache entries. infinispan.remote-cache-container.module=The module associated with this remote cache container's configuration. infinispan.remote-cache-container.module.deprecated=Deprecated. Superseded by the modules attribute. infinispan.remote-cache-container.modules=The set of modules associated with this remote cache container's configuration. infinispan.remote-cache-container.name=Uniquely identifies this remote cache container. infinispan.remote-cache-container.properties=A list of remote cache container properties. infinispan.remote-cache-container.protocol-version=This property defines the protocol version that this client should use. infinispan.remote-cache-container.socket-timeout=Enable or disable SO_TIMEOUT on socket connections to remote Hot Rod servers with the specified timeout, in milliseconds. A timeout of 0 is interpreted as an infinite timeout. infinispan.remote-cache-container.statistics-enabled=Enables statistics gathering for this remote cache. infinispan.remote-cache-container.tcp-no-delay=Enable or disable TCP_NODELAY on socket connections to remote Hot Rod servers. infinispan.remote-cache-container.tcp-keep-alive=Configures TCP Keepalive on the TCP stack. infinispan.remote-cache-container.value-size-estimate=This hint allows sizing of byte buffers when serializing and deserializing values, to minimize array resizing. infinispan.remote-cache-container.value-size-estimate.deprecated=Deprecated. This attribute will be ignored. infinispan.remote-cache-container.active-connections=The number of active connections to the Infinispan server. infinispan.remote-cache-container.connections=The total number of connections to the Infinispan server. infinispan.remote-cache-container.idle-connections=The number of idle connections to the Infinispan server. infinispan.remote-cache-container.transaction-timeout=The duration (in ms) after which idle transactions are rolled back. infinispan.remote-cache-container.remote-cache=A remote cache runtime resource infinispan.remote-cache.average-read-time=The average read time, in milliseconds, for this remote cache. infinispan.remote-cache.average-remove-time=The average remove time, in milliseconds, for this remote cache. infinispan.remote-cache.average-write-time=The average write time, in milliseconds, to this remote cache. infinispan.remote-cache.near-cache-hits=The number of near-cache hits for this remote cache. infinispan.remote-cache.near-cache-invalidations=The number of near-cache invalidations for this remote cache. infinispan.remote-cache.near-cache-misses=The number of near-cache misses for this remote cache. infinispan.remote-cache.near-cache-size=The number of entries in the near-cache for this remote cache. infinispan.remote-cache.hits=The number of hits to this remote cache, excluding hits from the near-cache. infinispan.remote-cache.misses=The number of misses to this remote cache. infinispan.remote-cache.removes=The number of removes to this remote cache. infinispan.remote-cache.writes=The number of writes to this remote cache. infinispan.remote-cache.reset-statistics=Resets the statistics for this remote cache. infinispan.remote-cache.time-since-reset=The number of seconds since statistics were reset on this remote cache. # /subsystem=infinispan/remote-cache-container=X/thread-pool=async infinispan.thread-pool.async=Defines a thread pool used for asynchronous operations. infinispan.thread-pool.async.add=Adds thread pool configuration used for asynchronous operations. infinispan.thread-pool.async.remove=Removes thread pool configuration used for asynchronous operations. # /subsystem=infinispan/remote-cache-container=*/component=connection-pool infinispan.component.connection-pool=Configuration of the connection pool. infinispan.component.connection-pool.add=Adds configuration of the connection pool. infinispan.component.connection-pool.remove=Removes configuration of the connection pool. infinispan.component.connection-pool.exhausted-action=Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted. infinispan.component.connection-pool.max-active=Controls the maximum number of connections per server that are allocated (checked out to client threads, or idle in the pool) at one time. When non-positive, there is no limit to the number of connections per server. When maxActive is reached, the connection pool for that server is said to be exhausted. Value -1 means no limit. infinispan.component.connection-pool.max-wait=The amount of time in milliseconds to wait for a connection to become available when the exhausted action is ExhaustedAction.WAIT, after which a java.util.NoSuchElementException will be thrown. If a negative value is supplied, the pool will block indefinitely. infinispan.component.connection-pool.min-evictable-idle-time=Specifies the minimum amount of time that an connection may sit idle in the pool before it is eligible for eviction due to idle time. When non-positive, no connection will be dropped from the pool due to idle time alone. This setting has no effect unless timeBetweenEvictionRunsMillis > 0. infinispan.component.connection-pool.min-idle=Sets a target value for the minimum number of idle connections (per server) that should always be available. If this parameter is set to a positive number and timeBetweenEvictionRunsMillis > 0, each time the idle connection eviction thread runs, it will try to create enough idle instances so that there will be minIdle idle instances available for each server. # /subsystem=infinispan/remote-cache-container=*/near-cache=invalidation infinispan.near-cache.invalidation=Configures using near cache in invalidated mode. When entries are updated or removed server-side, invalidation messages will be sent to clients to remove them from the near cache. infinispan.near-cache.invalidation.add=Adds a near cache in invalidated mode. infinispan.near-cache.invalidation.remove=Removes near cache in invalidated mode. infinispan.near-cache.invalidation.deprecated=Deprecated. Near cache is enabled per remote cache. infinispan.near-cache.invalidation.max-entries=Defines the maximum number of elements to keep in the near cache. # /subsystem=infinispan/remote-cache-container=*/near-cache=none infinispan.near-cache.none=Disables near cache. infinispan.near-cache.none.add=Adds configuration that disables near cache. infinispan.near-cache.none.remove=Removes configuration that disables near cache. infinispan.near-cache.none.deprecated=Deprecated. Near cache is disabled per remote cache. # /subsystem=infinispan/remote-cache-container=*/component=remote-clusters/remote-cluster=* infinispan.remote-cluster=Configuration of a remote cluster. infinispan.remote-cluster.add=Adds a remote cluster configuration requiring socket-bindings configuration. infinispan.remote-cluster.remove=Removes this remote cluster configuration. infinispan.remote-cluster.socket-bindings=List of outbound-socket-bindings of Hot Rod servers to connect to. infinispan.remote-cluster.switch-cluster=Switch the cluster to which this HotRod client should communicate. Primary used to failback to the local site in the event of a site failover. # /subsystem=infinispan/remote-cache-container=*/component=security infinispan.component.security=Security configuration. infinispan.component.security.add=Adds security configuration. infinispan.component.security.remove=Removes security configuration. infinispan.component.security.ssl-context=Reference to the Elytron-managed SSLContext to be used for connecting to the remote cluster. ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreResourceDefinition.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import java.util.concurrent.TimeUnit; import org.jboss.as.clustering.controller.CapabilityReference; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.clustering.controller.ResourceServiceConfigurator; import org.jboss.as.clustering.controller.SimpleResourceDescriptorConfigurator; import org.jboss.as.controller.AttributeDefinition; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.controller.client.helpers.MeasurementUnit; import org.jboss.as.controller.registry.AttributeAccess; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; /** * Resource description for the addressable resource and its alias * * /subsystem=infinispan/cache-container=X/cache=Y/store=remote * /subsystem=infinispan/cache-container=X/cache=Y/remote-store=REMOTE_STORE * * @author Richard Achmatowicz (c) 2011 Red Hat Inc. * @deprecated Use {@link org.jboss.as.clustering.infinispan.subsystem.remote.HotRodStoreResourceDefinition} instead. */ @Deprecated public class RemoteStoreResourceDefinition extends StoreResourceDefinition { static final PathElement LEGACY_PATH = PathElement.pathElement("remote-store", "REMOTE_STORE"); static final PathElement PATH = pathElement("remote"); enum Attribute implements org.jboss.as.clustering.controller.Attribute { CACHE("cache", ModelType.STRING, null), SOCKET_TIMEOUT("socket-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))), // keycloak patch: begin CONNECTION_TIMEOUT("connection-timeout", ModelType.LONG, new ModelNode(TimeUnit.MINUTES.toMillis(1))), // keycloak patch: end TCP_NO_DELAY("tcp-no-delay", ModelType.BOOLEAN, ModelNode.TRUE), SOCKET_BINDINGS("remote-servers") ; private final AttributeDefinition definition; Attribute(String name, ModelType type, ModelNode defaultValue) { this.definition = new SimpleAttributeDefinitionBuilder(name, type) .setAllowExpression(true) .setRequired(defaultValue == null) .setDefaultValue(defaultValue) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .setMeasurementUnit((type == ModelType.LONG) ? MeasurementUnit.MILLISECONDS : null) .build(); } Attribute(String name) { this.definition = new StringListAttributeDefinition.Builder(name) .setCapabilityReference(new CapabilityReference(Capability.PERSISTENCE, CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING)) .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) .setMinSize(1) .build(); } @Override public AttributeDefinition getDefinition() { return this.definition; } } RemoteStoreResourceDefinition() { super(PATH, LEGACY_PATH, InfinispanExtension.SUBSYSTEM_RESOLVER.createChildResolver(PATH, WILDCARD_PATH), new SimpleResourceDescriptorConfigurator<>(Attribute.class)); this.setDeprecated(InfinispanModel.VERSION_7_0_0.getVersion()); } @Override public ResourceServiceConfigurator createServiceConfigurator(PathAddress address) { return new RemoteStoreServiceConfigurator(address); } } ================================================ FILE: keycloak/patches/wildfly-clustering-infinispan-extension-patch-26.0.x/src/main/java/org/jboss/as/clustering/infinispan/subsystem/RemoteStoreServiceConfigurator.java ================================================ /* * JBoss, Home of Professional Open Source. * Copyright 2015, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.clustering.infinispan.subsystem; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CACHE; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.CONNECTION_TIMEOUT; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_BINDINGS; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.SOCKET_TIMEOUT; import static org.jboss.as.clustering.infinispan.subsystem.RemoteStoreResourceDefinition.Attribute.TCP_NO_DELAY; import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; import org.infinispan.persistence.remote.configuration.RemoteStoreConfiguration; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.jboss.as.clustering.controller.CommonUnaryRequirement; import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.StringListAttributeDefinition; import org.jboss.as.network.OutboundSocketBinding; import org.jboss.dmr.ModelNode; import org.jboss.msc.service.ServiceBuilder; import org.wildfly.clustering.service.Dependency; import org.wildfly.clustering.service.ServiceConfigurator; import org.wildfly.clustering.service.ServiceSupplierDependency; import org.wildfly.clustering.service.SupplierDependency; /** * @author Paul Ferraro */ @Deprecated public class RemoteStoreServiceConfigurator extends StoreServiceConfigurator { private volatile List> bindings; private volatile String remoteCacheName; private volatile long socketTimeout; // keycloak patch: begin private volatile long connectionTimeout; // keycloak patch: end private volatile boolean tcpNoDelay; public RemoteStoreServiceConfigurator(PathAddress address) { super(address, RemoteStoreConfigurationBuilder.class); } @Override public ServiceBuilder register(ServiceBuilder builder) { for (Dependency dependency : this.bindings) { dependency.register(builder); } return super.register(builder); } @Override public ServiceConfigurator configure(OperationContext context, ModelNode model) throws OperationFailedException { this.remoteCacheName = CACHE.resolveModelAttribute(context, model).asString(); this.socketTimeout = SOCKET_TIMEOUT.resolveModelAttribute(context, model).asLong(); this.connectionTimeout = CONNECTION_TIMEOUT.resolveModelAttribute(context, model).asLong(); this.tcpNoDelay = TCP_NO_DELAY.resolveModelAttribute(context, model).asBoolean(); List bindings = StringListAttributeDefinition.unwrapValue(context, SOCKET_BINDINGS.resolveModelAttribute(context, model)); this.bindings = new ArrayList<>(bindings.size()); for (String binding : bindings) { this.bindings.add(new ServiceSupplierDependency<>(CommonUnaryRequirement.OUTBOUND_SOCKET_BINDING.getServiceName(context, binding))); } return super.configure(context, model); } @Override public void accept(RemoteStoreConfigurationBuilder builder) { builder.segmented(false) .remoteCacheName(this.remoteCacheName) .socketTimeout(this.socketTimeout) .connectionTimeout(this.connectionTimeout) .tcpNoDelay(this.tcpNoDelay) ; for (Supplier bindingDependency : this.bindings) { OutboundSocketBinding binding = bindingDependency.get(); builder.addServer().host(binding.getUnresolvedDestinationAddress()).port(binding.getDestinationPort()); } } } ================================================ FILE: keycloak/themes/acme-account.v2/account/messages/messages_de.properties ================================================ personalInfoSidebarTitle=Profil accountSecuritySidebarTitle=Sicherheit signingInSidebarTitle=Anmeldung ================================================ FILE: keycloak/themes/acme-account.v2/account/messages/messages_en.properties ================================================ personalInfoSidebarTitle=Profile accountSecuritySidebarTitle=Security ================================================ FILE: keycloak/themes/acme-account.v2/account/resources/content.json ================================================ [ { "id": "personal-info", "path": "personal-info", "icon": "pf-icon-user", "label": "personalInfoSidebarTitle", "descriptionLabel": "personalInfoIntroMessage", "modulePath": "/content/account-page/AccountPage.js", "componentName": "AccountPage" }, { "id": "security", "icon": "pf-icon-security", "label": "accountSecuritySidebarTitle", "descriptionLabel": "accountSecurityIntroMessage", "content": [ { "id": "signingin", "path": "security/signingin", "label": "signingInSidebarTitle", "modulePath": "/content/signingin-page/SigningInPage.js", "componentName": "SigningInPage" }, { "id": "linked-accounts", "path": "security/linked-accounts", "label": "linkedAccountsSidebarTitle", "modulePath": "/content/linked-accounts-page/LinkedAccountsPage.js", "componentName": "LinkedAccountsPage", "hidden": "!features.isLinkedAccountsEnabled" } ] } ] ================================================ FILE: keycloak/themes/acme-account.v2/account/resources/css/styles.css ================================================ body { --pf-global--BackgroundColor--dark-100: #F0F0F0; --pf-global--BackgroundColor--dark-200: #004b96; --pf-global--BackgroundColor--dark-300: #004b96; --pf-global--BackgroundColor--dark-400: #004b96; --pf-global--Color--100: #004b96; } .pf-c-nav__list .pf-c-nav__link { --pf-c-nav__list-link--Color: #004b96; --pf-c-nav__list-link--m-current--Color: #004b96; } .pf-c-nav__simple-list .pf-c-nav__link { --pf-c-nav__simple-list-link--Color: #004b96; --pf-c-nav__simple-list-link--m-current--Color: #004b96; } ================================================ FILE: keycloak/themes/acme-account.v2/account/theme.properties ================================================ # This theme will inherit everything from its parent unless # it is overridden in the current theme. parent=keycloak.v3 ## account console contents can be configured via the content.json file # The locales supported by this theme locales=de,en # Look at the styles.css file to see examples of using PatternFly's CSS variables # for modifying look and feel. styles=css/styles.css # This is the logo in upper left-hand corner. # It must be a relative path from the theme resources-folder logo=/public/keycloak-logo.png # This is the link followed when clicking on the logo. # It can be any valid URL, including an external site. logoUrl=https://www.keycloak.org # This is the icon for the account console. # It must be a relative path from the theme resources-folder favIcon=/public/favicon.ico ================================================ FILE: keycloak/themes/admin-custom/admin/admin-settings.ftl ================================================ Keycloak Admin Settings <#if properties.meta?has_content> <#list properties.meta?split(' ') as meta> <#if properties.stylesCommon?has_content> <#list properties.stylesCommon?split(' ') as style> <#if properties.styles?has_content> <#list properties.styles?split(' ') as style> <#if properties.scripts?has_content> <#list properties.scripts?split(' ') as script> <#if scripts??> <#list scripts as script>

Realm Settings

Realm: ${realm.displayName}

<#list realmSettings.settings as setting>
================================================ FILE: keycloak/themes/admin-custom/admin/resources/js/admin-settings.js ================================================ console.log("admin-settings.js"); ================================================ FILE: keycloak/themes/admin-custom/admin/theme.properties ================================================ parent=keycloak.v2 import=common/keycloak # Custom Styles styles= stylesCommon=node_modules/@patternfly/patternfly/patternfly.min.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css # Custom JavaScript scripts=js/admin-settings.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 ================================================ FILE: keycloak/themes/apps/login/login.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section> <#if section = "header"> ${msg("loginAccountTitle")} <#elseif section = "form">
<#-- <#if realm.password>--> <#--
--> <#--
--> <#-- --> <#-- <#if usernameEditDisabled??>--> <#-- --> <#-- <#else>--> <#-- <#-- aria-invalid="<#if messagesPerField.existsError('username','password')>true"--> <#-- />--> <#-- <#if messagesPerField.existsError('username','password')>--> <#-- --> <#-- ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}--> <#-- --> <#-- --> <#-- --> <#--
--> <#--
--> <#-- --> <#-- <#-- aria-invalid="<#if messagesPerField.existsError('username','password')>true"--> <#-- />--> <#--
--> <#--
--> <#--
--> <#-- <#if realm.rememberMe && !usernameEditDisabled??>--> <#--
--> <#-- --> <#--
--> <#-- --> <#--
--> <#--
--> <#-- <#if realm.resetPasswordAllowed>--> <#-- ${msg("doForgotPassword")}--> <#-- --> <#--
--> <#--
--> <#--
--> <#-- value="${auth.selectedCredential}"/>--> <#-- --> <#--
--> <#--
--> <#-- --> <#--
--> <#if realm.password && social.providers??>

<#--

${msg("identity-provider-login-label")}

-->
<#elseif section = "info" > <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
${msg("noAccount")} ${msg("doRegister")}
================================================ FILE: keycloak/themes/apps/login/messages/messages_de.properties ================================================ acceptTerms=Nutzungsbedingungen akzeptieren termsText=Die Allgemeinen Gesch\u00e4ftsbedingungen ({0}) k\u00f6nnen hier eingesehen werden. termsRequired=Um sich anzumelden, m\u00fcssen Sie unseren Allgemeinen Gesch\u00e4ftsbedingungen zustimmen ================================================ FILE: keycloak/themes/apps/login/messages/messages_en.properties ================================================ acceptTerms=Accept Terms termsText=The terms and conditions Terms ({0}) can be found here termsRequired=You must agree to our terms and conditions to register. ================================================ FILE: keycloak/themes/apps/login/register.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section> <#if section = "header"> ${msg("registerTitle")} <#elseif section = "form">
<#if !realm.registrationEmailAsUsername>
<#if messagesPerField.existsError('username')> ${kcSanitize(messagesPerField.get('username'))?no_esc}
<#if messagesPerField.existsError('email')> ${kcSanitize(messagesPerField.get('email'))?no_esc}
<#if messagesPerField.existsError('firstName')> ${kcSanitize(messagesPerField.get('firstName'))?no_esc}
<#if messagesPerField.existsError('lastName')> ${kcSanitize(messagesPerField.get('lastName'))?no_esc}
<#if passwordRequired??>
<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<#if messagesPerField.existsError('password-confirm')> ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
<#if acceptTermsRequired??>
${msg("termsTitle")}
${kcSanitize(msg("termsText"))?no_esc}
<#if messagesPerField.existsError('terms')> ${kcSanitize(messagesPerField.get('terms'))?no_esc}
<#if recaptchaRequired??>
================================================ FILE: keycloak/themes/apps/login/resources/css/custom-login.css ================================================ /* acme login css */ .card-pf { background-color: whitesmoke; } .login-pf { background: darkgray; } .login-pf body { background: none; } /*#kc-form-login {*/ /* display: none;*/ /*}*/ /*#kc-social-providers > h4 {*/ /* display: none;*/ /*}*/ ================================================ FILE: keycloak/themes/apps/login/resources/js/custom-login.js ================================================ // custom-login.js (function initTheme() { console.log("apps theme"); })(); ================================================ FILE: keycloak/themes/apps/login/terms.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=false; section> <#if section = "header"> ${msg("termsTitle")} <#elseif section = "form">
${kcSanitize(msg("termsText", terms_id))?no_esc}
================================================ FILE: keycloak/themes/apps/login/theme.properties ================================================ parent=keycloak import=common/keycloak # Custom Styles styles=css/login.css css/custom-login.css stylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css # Custom JavaScript scripts=js/custom-login.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 ================================================ FILE: keycloak/themes/custom/login/messages/messages_en.properties ================================================ acceptTerms=Accept Terms termsText=The terms and conditions Terms can be found here termsRequired=You must agree to our terms and conditions to register. ================================================ FILE: keycloak/themes/custom/login/register.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section> <#if section = "header"> ${msg("registerTitle")} <#elseif section = "form">
<#if !realm.registrationEmailAsUsername>
<#if messagesPerField.existsError('username')> ${kcSanitize(messagesPerField.get('username'))?no_esc}
<#if messagesPerField.existsError('email')> ${kcSanitize(messagesPerField.get('email'))?no_esc}
<#if messagesPerField.existsError('firstName')> ${kcSanitize(messagesPerField.get('firstName'))?no_esc}
<#if messagesPerField.existsError('lastName')> ${kcSanitize(messagesPerField.get('lastName'))?no_esc}
<#if passwordRequired??>
<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<#if messagesPerField.existsError('password-confirm')> ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
<#if acceptTermsRequired??>
${msg("termsTitle")}
${kcSanitize(msg("termsText"))?no_esc}
<#if messagesPerField.existsError('terms')> ${kcSanitize(messagesPerField.get('terms'))?no_esc}
<#if recaptchaRequired??>
================================================ FILE: keycloak/themes/custom/login/resources/css/custom-login.css ================================================ /* white-login.css */ /* see: https://leaverou.github.io/css3patterns/ */ .login-pf body { background: radial-gradient(black 15%, transparent 16%) 0 0, radial-gradient(black 15%, transparent 16%) 8px 8px, radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 0 1px, radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 20%) 8px 9px !important; background-color: #282828 !important; background-size: 16px 16px !important; } ================================================ FILE: keycloak/themes/custom/login/resources/js/custom-login.js ================================================ // custom-login.js (function onCustomLogin() { console.log("custom login"); })(); ================================================ FILE: keycloak/themes/custom/login/theme.properties ================================================ parent=keycloak import=common/keycloak # Custom Styles styles=css/login.css css/custom-login.css stylesCommon=node_modules/@patternfly/patternfly/patternfly.min.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css # Custom JavaScript scripts=js/custom-login.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 ================================================ FILE: keycloak/themes/internal/account/account.ftl ================================================ <#import "template.ftl" as layout> <@layout.mainLayout active='account' bodyClass='user'; section>

${msg("editAccountHtmlTitle")}

* ${msg("requiredFields")}
<#if !realm.registrationEmailAsUsername>
<#if realm.editUsernameAllowed>*
disabled="disabled" value="${(account.username!'')}"/>
*
*
*
<#if url.referrerURI??>${kcSanitize(msg("backToApplication")?no_esc)}
================================================ FILE: keycloak/themes/internal/account/messages/messages_de.properties ================================================ phone_number=Mobilfunknummer trusted-device-display-name=Vertrautes Ger\u00e4t trusted-device-help-text=Vertrauter Browser f\u00fcr den MFA \u00fcbersprungen werden kann. mfa-sms-display-name=OTP Code via SMS mfa-sms-help-text=OTP Code versendet an die hinterlegte Mobilfunknummer mfa-email-code-display-name=E-Mail Code Authentifizierung mfa-email-code-help-text=Geben Sie einen Verifizierungscode aus einer E-Mail ein. acme-magic-link-display-name=Anmeldelink acme-magic-link-help-text=Melden Sie sich an, indem Sie auf einen Link klicken, den wir Ihnen per E-Mail schicken. ================================================ FILE: keycloak/themes/internal/account/messages/messages_en.properties ================================================ phone_number=Mobile Phonenumber trusted-device-display-name=Trusted Device trusted-device-help-text=Trusted browser that allows to skip MFA. mfa-sms-display-name=OTP Code via SMS mfa-sms-help-text=OTP Code sent via text message to the registered mobile phone number. # Used in account console mfa-email-code-form-display-name=Email Code mfa-email-code-form-help-text=Enter a valid access code sent via email. acme-magic-link-display-name=MagicLink acme-magic-link-help-text=Login by clicking a link we send via email. ================================================ FILE: keycloak/themes/internal/account/theme.properties ================================================ #parent=keycloak #import=common/keycloak # #styles=css/account.css #stylesCommon=node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css # ###### css classes for form buttons ## main class used for all buttons #kcButtonClass=btn ## classes defining priority of the button - primary or default (there is typically only one priority button for the form) #kcButtonPrimaryClass=btn-primary #kcButtonDefaultClass=btn-default ## classes defining size of the button #kcButtonLargeClass=btn-lg parent=keycloak.v3 developmentMode=true # This file is a workaround to add new messages to the account-console ================================================ FILE: keycloak/themes/internal/email/html/acme-account-blocked.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeAccountBlockedBodyHtml",user.username))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-account-deletion-requested.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeAccountDeletionRequestedBodyHtml",user.username,actionTokenUrl))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-account-updated.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeAccountUpdatedBodyHtml",user.username,update.changedAttribute,update.changedValue))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-email-verification-with-code.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeEmailVerificationBodyCodeHtml",code))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-magic-link.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeMagicLinkEmailBodyHtml", userDisplayName, link))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-mfa-added.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeMfaAddedBodyHtml",user.username,msg(mfaInfo.type)))?no_esc} <#if mfaInfo.label?? && mfaInfo.label?has_content>

Details: ${kcSanitize(mfaInfo.label)}

================================================ FILE: keycloak/themes/internal/email/html/acme-mfa-removed.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeMfaRemovedBodyHtml",user.username,msg(mfaInfo.type)))?no_esc} <#if mfaInfo.label?? && mfaInfo.label?has_content>

Details: ${kcSanitize(mfaInfo.label)}

================================================ FILE: keycloak/themes/internal/email/html/acme-passkey-added.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmePasskeyAddedBodyHtml",user.username,msg(passkeyInfo.label)))?no_esc} <#if passkeyInfo.label?? && passkeyInfo.label?has_content>

Details: ${kcSanitize(passkeyInfo.label)}

================================================ FILE: keycloak/themes/internal/email/html/acme-passkey-removed.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmePasskeyRemovedBodyHtml",user.username,msg(passkeyInfo.label)))?no_esc} <#if passkeyInfo.label?? && passkeyInfo.label?has_content>

Details: ${kcSanitize(passkeyInfo.label)}

================================================ FILE: keycloak/themes/internal/email/html/acme-trusted-device-added.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeTrustedDeviceAddedBodyHtml",user.username,trustedDeviceInfo.deviceName))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-trusted-device-removed.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeTrustedDeviceRemovedBodyHtml",user.username,trustedDeviceInfo.deviceName))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/acme-welcome.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("acmeWelcomeBodyHtml",realm.displayName, username, userDisplayName))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/code-email.ftl ================================================ <#import "template.ftl" as layout> <@layout.emailLayout> ${kcSanitize(msg("emailCodeBody", code))?no_esc} ================================================ FILE: keycloak/themes/internal/email/html/template.ftl ================================================ <#macro emailLayout>
Acme Header
<#nested>
Acme Footer
================================================ FILE: keycloak/themes/internal/email/messages/messages_de.properties ================================================ eventUpdateTotpSubject=2-Faktor Authentifizierung (OTP) Aktualisiert eventUpdateTotpBody=2-Faktor Authentifizierung (OTP) wurde am {0} von {1} ge\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin. eventUpdateTotpBodyHtml=

2-Faktor Authentifizierung (OTP) wurde am {0} von {1} ge\u00E4ndert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.

acmeEmailVerifySubject=Verifizierung der Email \u00c4nderung f\u00fcr {0} Benutzerkonto acmeEmailVerificationBodyCode=Bitte verifizieren Sie Ihre E-Mail-Adresse, indem Sie den folgenden Code eingeben.\n\nCode: {0}\n\n. acmeEmailVerificationBodyCodeHtml=

Bitte verifizieren Sie Ihre E-Mail-Adresse, indem Sie den folgenden Code eingeben.

Code: {0}

acmeTrustedDeviceAddedSubject=Neues vertrautes Ger\u00e4t hinzugef\u00fcgt f\u00fcr {0} Benutzerkonto acmeTrustedDeviceAddedBody=Ein neues vertrautes Ger\u00e4t mit dem Namen {1} wurde ihrem Konto hinzugef\u00fcgt. acmeTrustedDeviceAddedBodyHtml=

Ein neues vertrautes Ger\u00e4t mit dem Namen {1} wurde ihrem Konto hinzugef\u00fcgt.

acmeTrustedDeviceRemovedSubject=Vertrautes Ger\u00e4t entfernt f\u00fcr {0} Benutzerkonto acmeTrustedDeviceRemovedBody=Ein vertrautes Ger\u00e4t mit dem Namen {1} wurde aus ihrem Konto entfernt. acmeTrustedDeviceRemovedBodyHtml=

Ein vertrautes Ger\u00e4t mit dem Namen {1} wurde aus ihrem Konto entfernt.

acmeMfaAddedSubject=Neue Zweifaktorauthentifizierung hinzugef\u00fcgt f\u00fcr {0} Benutzerkonto acmeMfaAddedBody=Eine neue Zweifaktorauthentifizierung vom Typ {1} wurde ihrem Konto hinzugef\u00fcgt. acmeMfaAddedBodyHtml=

Eine neue Zweifaktorauthentifizierung vom Typ {1} wurde ihrem Konto hinzugef\u00fcgt.

acmeMfaRemovedSubject=Zweifaktorauthentifizierung entfernt f\u00fcr {0} acmeMfaRemovedBody=Eine Zweifaktorauthentifizierung vom Typ {1} wurde aus ihrem Konto entfernt. acmeMfaRemovedBodyHtml=

Eine Zweifaktorauthentifizierung vom Typ {1} wurde aus ihrem Konto entfernt.

acmePasskeyAddedSubject=Neuer Passkey hinzugef\u00fcgt f\u00fcr {0} Benutzerkonto acmePasskeyAddedBody=Ein neuer Passkey {1} wurde ihrem Konto hinzugef\u00fcgt. acmePasskeyAddedBodyHtml=

Ein neuer Passkey {1} wurde ihrem Konto hinzugef\u00fcgt.

acmePasskeyRemovedSubject=Passkey entfernt f\u00fcr {0} acmePasskeyRemovedBody=Der Passkey {1} wurde aus ihrem Konto entfernt. acmePasskeyRemovedBodyHtml=

Der Passkey {1} wurde aus ihrem Konto entfernt.

acmeAccountDeletionRequestedSubject=L\u00f6schung ihres {0} Benutzerkontos acmeAccountDeletionRequestedBody=Bitte best\u00e4tigen Sie die L\u00f6schung ihres Benutzerkontos f\u00fcr {0}, indem Sie den folgenden Link aufrufen.\n\nLink: {1}.\n\n acmeAccountDeletionRequestedBodyHtml=

Bitte best\u00e4tigen Sie die L\u00f6schung ihres Benutzerkontos f\u00fcr {0}, indem Sie den folgenden Link aufrufen.

Link: Benutzerkonto l\u00f6schung best\u00e4tigen.

acmeAccountBlockedSubject=Sperrung ihres {0} Benutzerkontos acmeAccountBlockedBody=Wegen zu vieler ung\u00fcltiger Anmeldeversuche wurde ihr Benutzerkonto {0} gesperrt. Bitte wenden Sie sich an den Support. acmeAccountBlockedBodyHtml=Wegen zu vieler ung\u00fcltiger Anmeldeversuche wurde ihr Benutzerkonto {0} gesperrt. Bitte wenden Sie sich an den Support. acmeAccountUpdatedSubject=Aktualisierung ihres {0} Benutzerkontos acmeAccountUpdatedBody=Ihr Benutzerkonto {0} wurde aktualisiert.\n\n{1} -> {2}\n\n acmeAccountUpdatedBodyHtml=

Ihr Benutzerkonto {0} wurde aktualisiert.

{1} -> {2}

# realmDisplayName, userDisplayName acmeWelcomeSubject=Willkommen bei {0} # realm.displayName, user.username, userDisplayName acmeWelcomeBody=Hallo {2}, willkommen bei {0}. Ihr Benutzername lautet: {1} acmeWelcomeBodyHtml=Hallo {2}, willkommen bei {0}. Ihr Benutzername lautet: {1} emailCodeSubject={0} Zugangscode emailCodeBody=Zugangscode: {0} resendCode=Erneut versenden mfa-email-code=E-Mail Code mfa-sms=SMS Code otp=OTP acmeMagicLinkText=Wir haben Ihnen einen Anmeldelink per E-Mail geschickt. Bitte pr\u00fcfen Sie Ihren Posteingang. # RealmName MagicLink acmeMagicLinkEmailSubject={0}: Anmeldelink acmeMagicLinkEmailBody=Hallo {0},\n\nKlicken Sie hier, um sich anzumelden: {1} acmeMagicLinkEmailBodyHtml=

Hallo {0},

Klicken Sie hier, um sich anzumelden

================================================ FILE: keycloak/themes/internal/email/messages/messages_en.properties ================================================ eventUpdateTotpSubject=2nd Factor Authentication (OTP) Updated eventUpdateTotpBody=2nd Factor Authentication (OTP) was updated for your account on {0} from {1}. If this was not you, please contact an administrator. eventUpdateTotpBodyHtml=

2nd Factor Authentication (OTP) was updated for your account on {0} from {1}. If this was not you, please contact an administrator.

acmeEmailVerifySubject=Verify email update for {0} Account acmeEmailVerificationBodyCode=Please verify your email address by entering in the following code.\n\nCode: {0} acmeEmailVerificationBodyCodeHtml=

Please verify your email address by entering in the following code.

Code: {0}

acmeTrustedDeviceAddedSubject=New trusted device added for {0} acmeTrustedDeviceAddedBody=A new trusted device with the name {1} has been added to your account. acmeTrustedDeviceAddedBodyHtml=

A new trusted device with the name {1} has been added to your account.

acmeTrustedDeviceRemovedSubject=Trusted device removed from {0} acmeTrustedDeviceRemovedBody=A trusted device with the name {1} has been removed from your account. acmeTrustedDeviceRemovedBodyHtml=

A trusted device with the name {1} has been removed from your account.

acmeMfaAddedSubject=New multi-factor authentication added for {0} acmeMfaAddedBody=A new multi-factor authentication of type {1} has been added to your account. acmeMfaAddedBodyHtml=

A new multi-factor authentication of type {1} has been added to your account.

acmeMfaRemovedSubject=Multi-factor authentication removed from {0} acmeMfaRemovedBody=A multi-factor authentication of type {1} has been removed from your account. acmeMfaRemovedBodyHtml=

A multi-factor authentication of type {1} has been removed from your account.

acmePasskeyAddedSubject=New passkey authentication added for {0} acmePasskeyAddedBody=A new passkey authentication {1} has been added to your account. acmePasskeyAddedBodyHtml=

A new passkey authentication {1} has been added to your account.

acmePasskeyRemovedSubject=Passkey authentication removed from {0} acmePasskeyRemovedBody=A passkey authentication {1} has been removed from your account. acmePasskeyRemovedBodyHtml=

A passkey authentication {1} has been removed from your account.

acmeAccountDeletionRequestedSubject={0} Account Deletion acmeAccountDeletionRequestedBody=Please confirm the deletion of your User account {0} by clicking on the following link.\n\nLink: {1}.\n\n acmeAccountDeletionRequestedBodyHtml=

Please confirm the deletion of your User account {0} by clicking on the following link.

Link: Confirm Account Deletion.

acmeAccountBlockedSubject={0} Account Locked acmeAccountBlockedBody=Due to too many invalid login attempts, your user account {0} has been locked. Please contact support. acmeAccountBlockedBodyHtml=Due to too many invalid login attempts, your user account {0} has been locked. Please contact support. acmeAccountUpdatedSubject={0} Account Updated acmeAccountUpdatedBody=Your account {0} was updated.\n\n{1} -> {2}\n\n acmeAccountUpdatedBodyHtml=

Your account {0} was updated.

{1} -> {2}

acmeWelcomeSubject=Welcome to {0} acmeWelcomeBody=Hello {2}, welcome to {0}. Username: {1} acmeWelcomeBodyHtml=Hello {2}, welcome to {0}. Username: {1} emailCodeSubject={0} access code emailCodeBody=Access code: {0} resendCode=Resend Code mfa-email-code=Email Code mfa-sms=SMS Code otp=OTP # RealmName MagicLink acmeMagicLinkEmailSubject={0}: MagicLink acmeMagicLinkEmailBody=Hello {0},\n\nClick here to sign-in: {1} acmeMagicLinkEmailBodyHtml=

Hello {0},

Click here to sign in

================================================ FILE: keycloak/themes/internal/email/text/acme-account-blocked.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeAccountBlockedBody",user.username)} ================================================ FILE: keycloak/themes/internal/email/text/acme-account-deletion-requested.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeAccountDeletionRequestedBody",user.username,actionTokenUrl)} ================================================ FILE: keycloak/themes/internal/email/text/acme-account-updated.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeAccountUpdatedBodyHtml",user.username,update.changedAttribute,update.changedValue)} ================================================ FILE: keycloak/themes/internal/email/text/acme-email-verification-with-code.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeEmailVerificationBodyCode",code)} ================================================ FILE: keycloak/themes/internal/email/text/acme-magic-link.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeMagicLinkEmailBody", userDisplayName, link)} ================================================ FILE: keycloak/themes/internal/email/text/acme-mfa-added.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeMfaAddedBody",user.username,msg(mfaInfo.type))} <#if mfaInfo.label?? && mfaInfo.label?has_content> Details: ${kcSanitize(mfaInfo.label)} ================================================ FILE: keycloak/themes/internal/email/text/acme-mfa-removed.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeMfaRemovedBody",user.username,msg(mfaInfo.type))} <#if mfaInfo.label?? && mfaInfo.label?has_content> Details: ${kcSanitize(mfaInfo.label)} ================================================ FILE: keycloak/themes/internal/email/text/acme-passkey-added.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmePasskeyAddedBody",user.username,msg(passkeyInfo.label))} <#if passkeyInfo.label?? && passkeyInfo.label?has_content> Details: ${kcSanitize(passkeyInfo.label)} ================================================ FILE: keycloak/themes/internal/email/text/acme-passkey-removed.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmePasskeyRemovedBody",user.username,msg(passkeyInfo.label))} <#if passkeyInfo.label?? && passkeyInfo.label?has_content> Details: ${kcSanitize(passkeyInfo.label)} ================================================ FILE: keycloak/themes/internal/email/text/acme-trusted-device-added.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeTrustedDeviceAddedBody",user.username,trustedDeviceInfo.deviceName)} ================================================ FILE: keycloak/themes/internal/email/text/acme-trusted-device-removed.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeTrustedDeviceRemovedBody",user.username,trustedDeviceInfo.deviceName)} ================================================ FILE: keycloak/themes/internal/email/text/acme-welcome.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("acmeWelcomeBodyHtml",realm.displayName, username, userDisplayName)} ================================================ FILE: keycloak/themes/internal/email/text/code-email.ftl ================================================ <#ftl output_format="plainText"> <#import "template.ftl" as layout> <@layout.emailLayout> ${msg("emailCodeBody", code)} ================================================ FILE: keycloak/themes/internal/email/text/template.ftl ================================================ <#macro emailLayout> Acme Header ------------ <#nested> ------------ Acme Footer ================================================ FILE: keycloak/themes/internal/email/theme.properties ================================================ parent=keycloak import=common/keycloak # Custom Styles styles=css/login.css css/custom-login.css stylesCommon=web_modules/@patternfly/react-core/dist/styles/base.css web_modules/@patternfly/react-core/dist/styles/app.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css # Custom JavaScript scripts=js/custom-login.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 ================================================ FILE: keycloak/themes/internal/login/email-code-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg('emailCodeFormTitle')} <#elseif section = "header"> ${msg('emailCodeFormTitle')} <#elseif section = "form">

${msg('emailCodeFormCta')}

required autocomplete="one-time-code"/>
================================================ FILE: keycloak/themes/internal/login/login-confirm-cookie-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('code') showAnotherWayIfPresent=false; section> <#if section = "header"> Confirm Cookie: ${realm.displayName} <#elseif section = "form">

Confirm Cookie

<#elseif section = "info" > Confirm Cookie Instruction ================================================ FILE: keycloak/themes/internal/login/login-magic-link.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=false; section> <#if section = "form">

${msg("acmeMagicLinkTitle")}

${message.summary}

================================================ FILE: keycloak/themes/internal/login/login-otp.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section> <#if section="header"> ${msg("doLogIn")} <#elseif section="form">
<#if otpLogin.userOtpCredentials?size gt 1>
<#list otpLogin.userOtpCredentials as otpCredential> checked="checked">
<#if messagesPerField.existsError('totp')> ${kcSanitize(messagesPerField.get('totp'))?no_esc}
================================================ FILE: keycloak/themes/internal/login/login-password.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('password') displayRequiredFields=true; section> <#if section = "header"> ${msg("doLogIn")} <#elseif section = "form">

<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<#if realm.resetPasswordAllowed> ${msg("doForgotPassword")}
================================================ FILE: keycloak/themes/internal/login/login-select-mfa-method.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('code'); section> <#if section = "header"> ${msg("selectMfaMethodTitle",realm.displayName)} <#elseif section = "form">
    <#list mfaMethods as mfaMethod>
<#elseif section = "info" > ${msg("selectMfaMethodInstruction")} ================================================ FILE: keycloak/themes/internal/login/login-skippable-action.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> Skippable Action <#elseif section = "header"> Skippable Action <#elseif section = "form">

Example Skippable Action

<#if canSkip>
================================================ FILE: keycloak/themes/internal/login/login-sms.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('code'); section> <#if section = "header"> ${msg("smsAuthTitle",realm.displayName)} <#elseif section = "form">

${msg("smsAuthInstruction")}

<#if messagesPerField.existsError('code')> ${kcSanitize(messagesPerField.get('code'))?no_esc}
================================================ FILE: keycloak/themes/internal/login/login-username.ftl ================================================ <#import "template.ftl" as layout> <#import "passkeys.ftl" as passkeys> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section> <#if section = "header"> ${msg("loginAccountTitle")} <#elseif section = "form"> <#--<#list .data_model?keys as key>--> <#-- ${key}
--> <#---->
<#if realm.password>
<#if !usernameHidden??>
<#if messagesPerField.existsError('username')> ${kcSanitize(messagesPerField.get('username'))?no_esc}
<#if realm.rememberMe && !usernameHidden??>
<@passkeys.conditionalUIData /> <#if realm.password && social.providers??>

${msg("identity-provider-login-label")}

<#elseif section = "info" > <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
${msg("noAccount")} ${msg("doRegister")}
================================================ FILE: keycloak/themes/internal/login/manage-trusted-device-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg("acmeRegisterTrustedDeviceTitle")} <#elseif section = "header"> ${msg("acmeRegisterTrustedDeviceTitle")} <#elseif section = "form">

${msg("acmeRegisterTrustedDeviceCta")}

<#if isAppInitiatedAction??> <#else>
================================================ FILE: keycloak/themes/internal/login/messages/messages_de.properties ================================================ smsAuthText=Ihr SMS Code lautet %1$s und ist gltig fr %2$d Minuten. smsAuthTitle=SMS Code smsAuthLabel=SMS Code smsAuthInstruction=Geben Sie den Code ein, der an Ihr Ger\u00E4t gesendet wurde. smsSentInfo=SMS mit code versendet an: {0}. smsAuthSmsNotSent=Die SMS konnte nicht gesendet werden: {0} smsAuthCodeExpired=Die G\u00FCltigkeit des Codes ist abgelaufen. smsAuthCodeInvalid=Ung\u00FCltiger Code angegeben. smsAuthAttemptsExceeded=Zu viele ung\u00FCltige Eingaben. smsResendCode=Neuen Code senden trustThisDevice=Diesem Ger\u00E4t vertrauen acme-sms-authenticator-display-name=Authentifizierung mit SMS acme-sms-authenticator-help-text=Eingabe eines Verifizierungscodes aus einer SMS Nachricht. birthdate=Geburtsdatum phoneNumber=Mobilfunknummer acmePhoneNumberTitle=Mobilfunknummer \u00e4ndern acmePhoneNumberCta=Bitte geben sie ihre neue Mobilfunknummer an. acmePhoneNumberVerifyCta=Bitte geben Sie den per SMS gesendeten Verifizierungscode ein. acceptTerms=Bedingungen akzeptieren termsText=Die Allgemeinen Gesch\u00e4ftsbedingungen finden Sie hier. termsRequired=Um sich zu registrieren, m\u00fcssen Sie unseren Gesch\u00e4ftsbedingungen zustimmen. proceed=Weiter legalImprint=Impressum legalTerms=Nutzungsbedigungen legalPrivacy=Datenschutz loginAccountTitle=Anmeldung acmeEmailUpdateTitle=Email \u00e4ndern acmeEmailUpdateCta=Bitte geben Sie ihre neue E-Mail Adresse ein. acmeEmailUpdateVerifyTitle=Best\u00e4tigung der Email \u00c4nderung emailSentInfo=E-Mail mit Code wurde versendet an: {0}. acmeEmailVerifyCta=Geben Sie den Code ein, den wir an ihre neue E-Mail Adresse gesendet haben. acmeRegisterTrustedDeviceTitle=Vertrauensw\u00fcrdiges Ger\u00e4t registrieren acmeRegisterTrustedDeviceCta=Vertrauen Sie diesem Ger\u00e4t? device=Ger\u00e4t removeAllTrustedDevices=Alle vertrauten Ger\u00e4te entfernen yes=Ja no=Nein emailAuthLabel=Email Code invalidEmailSameAddressMessage=Email nicht ge\u00e4ndert. acmeProfileScopeConsentText=Zugriff auf Acme Profile acmeConsentSelectionTitle=Datenfreigabe acmeConsentSelection=M\u00f6chten Sie Zugriff auf folgende Daten freigeben? phone=Telefon profile=Profil name=Name given_name=Vorname family_name=Nachname firstname=Vorname emailCodeFormTitle=E-Mail Zugangscode eingeben emailCodeFormCta=Bitte E-Mail Zugangscode eingeben emailCodeSubject=Ihr {0} Zugangscode emailCodeBody=Zugangscode: {0} accessCode=E-Mail Zugangscode resendCode=Erneut versenden reauthenticate=Bitte melden Sie sich erneut an, um fortzufahren phone_number=Mobilfunknummer mfa-sms-display-name=SMS Code Authentifizierung mfa-sms-help-text=Geben Sie einen Verifizierungscode aus einer SMS Nachricht ein. mfa-sms-code-invalid=Der angegebene Verifizierungscode ist ung\u00fcltig! trusted-device-display-name=Vertrauensw\u00fcrdige Ger\u00E4t trusted-device-help-text=Mehrstufige Authentifizierung auf vertrauensw\u00fcrdigen Ger\u00E4t \u00fcberspringen. userNotAllowedToAccess=Zugriff f\u00fcr Benutzer {0} verweigert. mfa-email-code-display-name=E-Mail Code Authentifizierung mfa-email-code-help-text=Geben Sie einen Verifizierungscode aus einer E-Mail ein. acme-email-code-form-display-name=E-Mail Code Authentifizierung acme-email-code-form-help-text=Geben Sie einen Verifizierungscode aus einer E-Mail ein. error-invalid-code=Code ung\u00fcltig acmeMagicLinkTitle=Anmeldelink acmeMagicLinkText=Wir haben Ihnen einen Anmeldelink per E-Mail geschickt. Bitte pr\u00fcfen Sie Ihren Posteingang. mfaMethods=Zwei-Faktor Methoden selectMfaMethodInstruction=Bitte whlen Sie eine Methode zur Mehr-Faktor-Authentifizierung aus. webauthn=WebAuthN otp=OTP ================================================ FILE: keycloak/themes/internal/login/messages/messages_en.properties ================================================ smsAuthText=Your SMS code is %1$s and is valid for %2$d minutes. smsAuthTitle=SMS Code smsAuthLabel=SMS Code smsAuthInstruction=Enter the code we sent to your device via SMS. smsSentInfo=SMS with code sent to: {0}. smsAuthSmsNotSent=Failed to send SMS {0} smsAuthCodeExpired=The code has expired. smsAuthCodeInvalid=Invalid code entered. smsAuthAttemptsExceeded=Too many invalid attempts. smsResendCode=Resend code trustThisDevice=Trust this device acmeRegisterTrustedDeviceTitle=Register trusted device acmeRegisterTrustedDeviceCta=Do you trust this device? device=Device removeAllTrustedDevices=Remove all trusted devices acme-sms-authenticator-display-name=Authentication with SMS code acme-sms-authenticator-help-text=Enter a verification code from an SMS message. birthdate=Birthdate phoneNumber=Mobile Phone Number acmePhoneNumberTitle=Mobile Phone Number Update acmePhoneNumberCta=Please enter your new Mobile Phone Number. acmePhoneNumberVerifyCta=Please enter the verification code sent via SMS. acceptTerms=Accept Terms termsText=The terms and conditions Terms can be found here. termsRequired=You must agree to our terms and conditions to register. proceed=Next yes=Yes no=No legalImprint=Imprint legalTerms=Terms & Conditions legalPrivacy=Privacy loginAccountTitle=Login acmeEmailUpdateTitle=Email Update acmeEmailUpdateVerifyTitle=Verify Email Update acmeEmailUpdateCta=Please enter your new Email address. emailSentInfo=Email with Code sent to: {0}. acmeEmailVerifyCta=Please enter the Email verification code. emailAuthLabel=Email Code invalidEmailSameAddressMessage=Different Email address required. acmeProfileScopeConsentText=Acme Profile Access acmeConsentSelectionTitle=Grant Access acmeConsentSelection=Do you grant access to the following information? phone=Phone profile=Profile name=Name given_name=Firstname family_name=Lastname firstname=Firstname emailCodeFormTitle=Enter Access Code emailCodeFormCta=Please enter Email Access Code emailCodeSubject=Your {0} access code emailCodeBody=Access code: {0} accessCode=Email Access Code resendCode=Resend Code phone_number=Mobile Phonenumber mfa-sms-display-name=SMS Authentication mfa-sms-help-text=Enter a verification code sent via SMS mfa-sms-code-invalid=The given verification code is invalid trusted-device-display-name=Trusted Devices trusted-device-help-text=Skip MFA for a trusted browser. userNotAllowedToAccess=Access for user {0} denied. mfa-email-code-display-name=Email Code mfa-email-code-help-text=Enter a valid access code sent via email. # Used in account console mfa-email-code-form-display-name=Email Code mfa-email-code-form-help-text=Enter a valid access code sent via email. # Used by authenticator selector acme-email-code-form-display-name=Email Code acme-email-code-form-help-text=Enter a valid access code sent via email. error-invalid-code=Invalid code acmeMagicLinkTitle=Magic Link acmeMagicLinkText=We sent you a login link via email. Check your inbox for details. mfaMethods=MFA Methods selectMfaMethodInstruction=Please select a Multi-Factor Authentication method webauthn=WebAuthN otp=OTP ================================================ FILE: keycloak/themes/internal/login/register.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section> <#if section = "header"> ${msg("registerTitle")} <#elseif section = "form">
<#if !realm.registrationEmailAsUsername>
<#if messagesPerField.existsError('username')> ${kcSanitize(messagesPerField.get('username'))?no_esc}
<#if messagesPerField.existsError('email')> ${kcSanitize(messagesPerField.get('email'))?no_esc}
<#if messagesPerField.existsError('firstName')> ${kcSanitize(messagesPerField.get('firstName'))?no_esc}
<#if messagesPerField.existsError('lastName')> ${kcSanitize(messagesPerField.get('lastName'))?no_esc}
<#if passwordRequired??>
<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<#if messagesPerField.existsError('password-confirm')> ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
<#if acceptTermsRequired??>
${msg("termsTitle")}
${kcSanitize(msg("termsText"))?no_esc}
<#if messagesPerField.existsError('terms')> ${kcSanitize(messagesPerField.get('terms'))?no_esc}
<#if recaptchaRequired??>
================================================ FILE: keycloak/themes/internal/login/resources/css/custom-login.css ================================================ /* acme login css */ .card-pf { background-color: lightyellow; } .login-pf { background: darkgray; } .login-pf body { background: none; } ================================================ FILE: keycloak/themes/internal/login/resources/js/custom-login.js ================================================ // custom-login.js (function initTheme() { console.log("internal theme"); // hack to add mobile icon for sms authenticator, needs to be called after dom ready function updateMobileIconOnSmsAuthenticatorInAuthenticationSelector() { let elements = [...document.querySelectorAll('div.pf-c-title')].filter(elem => elem.textContent.includes('SMS')); if (elements && elements.length > 0) { console.log("patch mobile icon"); elements[0].parentElement.parentElement.querySelector("i").classList.add("fa-mobile"); } } function onDomContentLoaded() { updateMobileIconOnSmsAuthenticatorInAuthenticationSelector(); } document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded()); })(); ================================================ FILE: keycloak/themes/internal/login/select-consent-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg('acmeConsentSelectionTitle')} <#elseif section = "header"> ${msg('acmeConsentSelectionTitle')} <#elseif section = "form">

${msg('acmeConsentSelection')}

<#list scopes as scope>
disabled <#if scope.granted || !scope.optional>checked /> <#if !scope.optional> <#if scope.optional>(optional)

${msg(scope.description)}

<#-- Field details by scope --> <#--
<#list scope.fields as scopeField>
--> <#-- -->
================================================ FILE: keycloak/themes/internal/login/theme.properties ================================================ parent=keycloak import=common/keycloak # Custom Styles styles=css/login.css css/custom-login.css stylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css # Custom JavaScript scripts=js/custom-login.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 ## Password visibility kcFormPasswordVisibilityButtonClass=pf-c-button pf-m-control kcFormPasswordVisibilityIconShow=fa fa-eye kcFormPasswordVisibilityIconHide=fa fa-eye-slash kcAuthenticatorMfaEmailCodeClass=fa fa-envelope-o ================================================ FILE: keycloak/themes/internal/login/update-email-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg('acmeEmailUpdateTitle')} <#elseif section = "header"> ${msg('acmeEmailUpdateTitle')} <#elseif section = "form">

${msg('acmeEmailUpdateCta')}

<#if messagesPerField.existsError('email')> ${kcSanitize(messagesPerField.get('email'))?no_esc}
<#if isAppInitiatedAction??> <#else>
================================================ FILE: keycloak/themes/internal/login/update-phone-number-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg('acmePhoneNumberTitle')} <#elseif section = "header"> ${msg('acmePhoneNumberTitle')} <#elseif section = "form">

${msg('acmePhoneNumberCta')}

<#if messagesPerField.existsError('mobile')> ${kcSanitize(messagesPerField.get('mobile'))?no_esc}
<#if isAppInitiatedAction??> <#else>
================================================ FILE: keycloak/themes/internal/login/verify-email-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg('acmeEmailUpdateVerifyTitle')} <#elseif section = "header"> ${msg('acmeEmailUpdateVerifyTitle')} <#elseif section = "form">

${msg('acmeEmailVerifyCta')}

<#if messagesPerField.existsError('code')> ${kcSanitize(messagesPerField.get('code'))?no_esc}
<#if isAppInitiatedAction??> <#else>
================================================ FILE: keycloak/themes/internal/login/verify-phone-number-form.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> ${msg('acmePhoneNumberTitle')} <#elseif section = "header"> ${msg('acmePhoneNumberTitle')} <#elseif section = "form">

${msg('acmePhoneNumberVerifyCta')}

<#if messagesPerField.existsError('code')> ${kcSanitize(messagesPerField.get('code'))?no_esc}
<#if isAppInitiatedAction??> <#else>
================================================ FILE: keycloak/themes/internal-modern/account/theme.properties ================================================ parent=internal import=common/keycloak ================================================ FILE: keycloak/themes/internal-modern/email/messages/messages_de.properties ================================================ ================================================ FILE: keycloak/themes/internal-modern/email/messages/messages_en.properties ================================================ ================================================ FILE: keycloak/themes/internal-modern/email/theme.properties ================================================ parent=internal import=common/keycloak ================================================ FILE: keycloak/themes/internal-modern/login/context-selection.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "title"> Context Selection <#elseif section = "header"> Context selection <#elseif section = "form">

Please select a Context:

<#list contextOptions as contextOption>
placeholder="${currentContext.label!''}" onchange="restrictInputToAllowedOptions(this);" required/> value="${currentContext.value!''}" type="hidden"/>
================================================ FILE: keycloak/themes/internal-modern/login/login-applications.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section> <#if section = "header"> ${msg("loginApplicationsTitle")} <#elseif section = "form"> <#-- <#list .data_model?keys as key>--> <#-- ${key}
--> <#-- -->

${msg('loginApplicationsGreeting', user.username)}

${msg('loginApplicationsInfo')}

<#elseif section = "info" > ================================================ FILE: keycloak/themes/internal-modern/login/login-idp-selection.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section> <#if section = "header"> ${msg("loginAccountTitle")} <#elseif section = "form">

<#--

${msg("identity-provider-login-label")}

-->
================================================ FILE: keycloak/themes/internal-modern/login/login-password.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('password'); section> <#if section = "header"> ${msg("doLogIn")} <#elseif section = "form">

<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<#if realm.resetPasswordAllowed> ${msg("doForgotPassword")}
================================================ FILE: keycloak/themes/internal-modern/login/login-username.ftl ================================================ <#import "template.ftl" as layout> <#import "passkeys.ftl" as passkeys> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section> <#if section = "header"> ${msg("loginAccountTitle")} <#elseif section = "form"> <#--<#list .data_model?keys as key>--> <#-- ${key}
--> <#---->
<#if realm.password>
<#if !usernameHidden??>
<#if messagesPerField.existsError('username')> ${kcSanitize(messagesPerField.get('username'))?no_esc}
<#if realm.rememberMe && !usernameHidden??>
<@passkeys.conditionalUIData /> <#elseif section = "info" > <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
${msg("noAccount")} ${msg("doRegister")}
<#elseif section = "socialProviders" > <#if realm.password && social?? && social.providers?has_content>

${msg("identity-provider-login-label")}

================================================ FILE: keycloak/themes/internal-modern/login/login-verify-email.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=true; section> <#if section = "header"> ${msg("emailVerifyTitle")} <#elseif section = "form">

${msg("emailVerifyInstruction1",user.email)}

<#elseif section = "info">

${msg("emailVerifyInstruction2")}
${msg("doClickHere")} ${msg("emailVerifyInstruction3")}

================================================ FILE: keycloak/themes/internal-modern/login/login.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section> <#assign showLoginForm = true> <#if section = "header"> ${msg("loginAccountTitle")} <#elseif section = "form">
<#if realm.password && showLoginForm >
<#if !usernameHidden??>
<#if messagesPerField.existsError('username','password')> ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
<#if usernameHidden?? && messagesPerField.existsError('username','password')> ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
<#if friendlyCaptchaEnabled??> <#-- See: https://docs.friendlycaptcha.com/#/widget_api?id=attribute-api-html-tags -->
<#if realm.rememberMe && !usernameHidden??>
<#if realm.resetPasswordAllowed> ${msg("doForgotPassword")}
value="${auth.selectedCredential}"/>
<#elseif section = "info" > <#if realm.password && realm.registrationAllowed && !registrationDisabled?? && showLoginForm>
${msg("noAccount")} ${msg("doRegister")}
<#elseif section = "socialProviders" > <#if realm.password && social?? && social.providers?has_content>

<#if showLoginForm>

${msg("identity-provider-login-label")}

================================================ FILE: keycloak/themes/internal-modern/login/messages/messages_de.properties ================================================ loginApplicationsTitle=Verfgbare Anwendungen loginApplicationsInfo=Hier sind ihre Anwendungen: loginApplicationsGreeting=Willkommen, {0}! ================================================ FILE: keycloak/themes/internal-modern/login/messages/messages_en.properties ================================================ loginApplicationsTitle=Available Applications loginApplicationsInfo=Here are your available applications: loginApplicationsGreeting=Welcome, {0}! ================================================ FILE: keycloak/themes/internal-modern/login/register.ftl ================================================ <#import "template.ftl" as layout> <#import "user-profile-commons.ftl" as userProfileCommons> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('firstName','lastName','email','username','password','password-confirm'); section> <#if section = "header"> ${msg("registerTitle")} <#elseif section = "form">
<#if !realm.registrationEmailAsUsername>
<#if messagesPerField.existsError('username')> ${kcSanitize(messagesPerField.get('username'))?no_esc}
<#if messagesPerField.existsError('email')> ${kcSanitize(messagesPerField.get('email'))?no_esc}
<#if messagesPerField.existsError('firstName')> ${kcSanitize(messagesPerField.get('firstName'))?no_esc}
<#if messagesPerField.existsError('lastName')> ${kcSanitize(messagesPerField.get('lastName'))?no_esc}
<#if passwordRequired??>
<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<#if messagesPerField.existsError('password-confirm')> ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
<#if customProfile??> <#-- for support dynamic custom profile fields in registration --> <@userProfileCommons.userProfileFormFields/> <#if acceptTermsRequired??>
${msg("termsTitle")}
${kcSanitize(msg("termsText", acmeUrl.termsUrl))?no_esc}
<#if messagesPerField.existsError('terms')> ${kcSanitize(messagesPerField.get('terms'))?no_esc}
<#if recaptchaRequired??>
<#if friendlyCaptchaEnabled??> <#-- See: https://docs.friendlycaptcha.com/#/widget_api?id=attribute-api-html-tags -->
disabled="" type="submit" value="${msg("doRegister")}"/>
================================================ FILE: keycloak/themes/internal-modern/login/resources/css/custom-modern-login.css ================================================ /* acme internal-modern login css */ .login-pf { background: none; } body { font-size: 16px; } .custom-hr { width: 100%; border-top: 1px solid #707070; } #kc-registration { margin-top: 10px; } #custom-kc-header-wrapper { font-size: 29px; text-transform: uppercase; letter-spacing: 3px; line-height: 1.2em; padding: 62px 0; white-space: normal; } @media (max-width: 767px) { #custom-kc-header-wrapper { font-size: 16px; font-weight: bold; padding: 20px 0; color: #72767b; letter-spacing: 0; } } .custom-header-container { justify-content: center; display: flex; align-items: center; flex-wrap: wrap; } .custom-main-realm { font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: bold; color: #000000; font-size: 28px; text-decoration: underline; text-decoration-color: #22F4AE; text-decoration-thickness: 5px; text-transform: uppercase; padding-top: 10px; } #custom-kc-page-title { font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: bold; color: #000000; font-size: 30px; /*margin-bottom: 43px;*/ } #custom-kc-app-name { font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: normal; color: #000000; font-size: 24px; } .custom-card-pf { margin: 0 auto; padding: 0 20px; max-width: 700px; border-top: 0; box-shadow: 0 0 0; } #custom-kc-content { width: 100%; background: #FFFFFF; padding: 10px 10px; } .custom-control-label { font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: normal; font-size: 18px; margin-bottom: 0px; } .custom-form-control { display: block; width: 100%; height: 70px !important; padding: 4px 12px; font-size: 28px; line-height: 1.66666667; color: #363636; background-color: #fff; background-image: none; border: 1px solid #D0CCCC; border-radius: 1px; box-shadow: inset 0px 0px 3px #22f4ae; transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: normal; } .custom-form-control :focus { outline: #707070; } .custom-form-control :-moz-focusring { outline: #707070; } #custom-kc-form-options label { font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: 200; font-size: 18px; color: #000000; } #custom-kc-form-options .checkbox { margin-top: 0; color: #72767b; } #custom-kc-form-options input { margin-top: 6px; margin-left: -20px; } .custom-forgot-password a { font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: normal; font-size: 16px; color: #707070 !important; } .custom-btn-primary { border: none; box-shadow: none; background-color: #22f4ae; color: #000000; } .custom-btn-primary.active, .custom-btn-primary:active, .custom-btn-primary:focus, .custom-btn-primary:hover, .custom-open .dropdown-toggle.custom-btn-primary { background-color: #000000; background-image: none; color: #22f4ae; } .custom-btn { height: 76px; width: 527px; background: #22f4ae; font-family: "Open Sans", Helvetica, Arial, sans-serif; font-weight: bold; font-size: 32px; color: #000000; border: none; } .custom-btn :hover { background: #FFFFFF !important; } .alert-error { background-color: #ffffff; border-color: #cc0000; color: #333333; } #kc-locale ul { display: none; /*position: absolute;*/ background-color: #fff; list-style: none; /*right: 0;*/ /*top: 20px;*/ min-width: 100px; padding: 2px 0; border: solid 1px #bbb; } #kc-locale:hover ul { display: block; margin: 0; } #kc-locale ul li a { display: block; padding: 5px 14px; color: #000 !important; text-decoration: none; line-height: 20px; } #kc-locale ul li a:hover { color: #4d5258; background-color: #d4edfa; } #kc-locale-dropdown a { color: #4d5258; background: 0 0; padding: 0 15px 0 0; font-weight: 300; } #kc-locale-dropdown a:hover { text-decoration: none; } a#kc-current-locale-link { display: block; padding: 0 5px; } /* a#kc-current-locale-link:hover { background-color: rgba(0,0,0,0.2); } */ a#kc-current-locale-link::after { content: "\2c5"; margin-left: 4px; } .login-pf .container { padding-top: 40px; } .login-pf a:hover { color: #0099d3; } #kc-logo { width: 100%; } .kcInfoMessage { margin-bottom: 20px; } #kc-logo-wrapper { /*background-image: url(../img/keycloak-logo-2.png);*/ background-repeat: no-repeat; height: 63px; width: 300px; margin: 62px auto 0; } div.kc-logo-text { /*background-image: url(../img/keycloak-logo-text.png);*/ background-repeat: no-repeat; height: 63px; width: 300px; margin: 0 auto; } div.kc-logo-text span { display: none; } #kc-header { color: #000000; overflow: visible; white-space: nowrap; } #kc-header-wrapper { font-size: 29px; text-transform: uppercase; letter-spacing: 3px; line-height: 1.2em; padding: 62px 10px 20px; white-space: normal; } #kc-content { width: 100%; } #kc-attempted-username { font-size: 20px; font-family: inherit; font-weight: normal; padding-right: 10px; } #kc-username { text-align: center; } #kc-webauthn-settings-form { padding-top: 8px; } /* #kc-content-wrapper { overflow-y: hidden; } */ /*#kc-info {*/ /* padding-bottom: 200px;*/ /* margin-bottom: -200px;*/ /*}*/ #kc-info-wrapper { font-size: 13px; } #kc-form-options span { display: block; } #kc-form-options .checkbox { margin-top: 0; color: #72767b; } #kc-terms-text { margin-bottom: 20px; } #kc-registration { margin-bottom: 15px; } /* TOTP */ .subtitle { text-align: right; margin-top: 30px; color: #909090; } .required { color: #CB2915; } ol#kc-totp-settings { margin: 0; padding-left: 20px; } ul#kc-totp-supported-apps { margin-bottom: 10px; } #kc-totp-secret-qr-code { max-width: 150px; max-height: 150px; } #kc-totp-secret-key { background-color: #fff; color: #333333; font-size: 16px; padding: 10px 0; } /* OAuth */ #kc-oauth h3 { margin-top: 0; } #kc-oauth ul { list-style: none; padding: 0; margin: 0; } #kc-oauth ul li { border-top: 1px solid rgba(255, 255, 255, 0.1); font-size: 12px; padding: 10px 0; } #kc-oauth ul li:first-of-type { border-top: 0; } #kc-oauth .kc-role { display: inline-block; width: 50%; } /* Code */ #kc-code textarea { width: 100%; height: 8em; } /* Social */ #kc-social-providers ul { padding: 0; } #kc-social-providers li { display: block; } #kc-social-providers li:first-of-type { margin-top: 0; } .kc-login-tooltip { position: relative; display: inline-block; } .kc-login-tooltip .kc-tooltip-text { top: -3px; left: 160%; background-color: black; visibility: hidden; color: #fff; min-width: 130px; text-align: center; border-radius: 2px; box-shadow: 0 1px 8px rgba(0, 0, 0, 0.6); padding: 5px; position: absolute; opacity: 0; transition: opacity 0.5s; } /* Show tooltip */ .kc-login-tooltip:hover .kc-tooltip-text { visibility: visible; opacity: 0.7; } /* Arrow for tooltip */ .kc-login-tooltip .kc-tooltip-text::after { content: " "; position: absolute; top: 15px; right: 100%; margin-top: -5px; border-width: 5px; border-style: solid; border-color: transparent black transparent transparent; } .zocial, a.zocial { width: 100%; font-weight: normal; font-size: 14px; text-shadow: none; border: 0; background: #f5f5f5; color: #72767b; border-radius: 0; white-space: normal; } .zocial:before { border-right: 0; margin-right: 0; } .zocial span:before { padding: 7px 10px; font-size: 14px; } .zocial:hover { background: #ededed !important; } .zocial.facebook, .zocial.github, .zocial.google, .zocial.microsoft, .zocial.stackoverflow, .zocial.linkedin, .zocial.twitter { background-image: none; border: 0; box-shadow: none; text-shadow: none; } /* Copy of zocial windows classes to be used for microsoft's social provider button */ .zocial.microsoft:before { content: "\f15d"; } .zocial.stackoverflow:before { color: inherit; } @media (min-width: 768px) { #kc-container-wrapper { position: absolute; width: 100%; } .login-pf .container { padding-right: 80px; } #kc-locale { position: relative; /*text-align: right;*/ z-index: 9999; } } .login-pf body { background: lightgrey; } .login-pf .card-pf { border-radius: 5px; } .legal-links > ul { list-style: none; } .legal-links > ul > li { display: inline; font-size: smaller; } @media (max-width: 767px) { .login-pf body { background: lightgrey; } #kc-header { padding-left: 15px; padding-right: 15px; float: none; text-align: left; } #kc-header-wrapper { font-size: 16px; font-weight: bold; padding: 20px 60px 0 0; color: #72767b; letter-spacing: 0; } div.kc-logo-text { margin: 0; width: 150px; height: 32px; background-size: 100%; } #kc-form { float: none; } #kc-info-wrapper { border-top: 1px solid rgba(255, 255, 255, 0.1); margin-top: 15px; padding-top: 15px; padding-left: 0px; padding-right: 15px; } #kc-social-providers li { display: block; margin-right: 5px; } .login-pf .container { padding-top: 15px; padding-bottom: 15px; } #kc-locale { position: absolute; width: 200px; top: 20px; right: 20px; /*text-align: left;*/ z-index: 9999; } #kc-logo-wrapper { background-size: 100px 21px; height: 21px; width: 100px; margin: 20px 0 0 20px; } } @media (min-height: 646px) { #kc-container-wrapper { bottom: 12%; } } @media (max-height: 645px) { #kc-container-wrapper { padding-top: 50px; top: 20%; } } .card-pf form.form-actions .btn { float: right; margin-left: 10px; } #kc-form-buttons { margin-top: 40px; } .login-pf-page .login-pf-brand { margin-top: 20px; max-width: 360px; width: 40%; } .card-pf { background: #fff; margin: 0 auto; padding: 0 20px; max-width: 500px; border-top: 0; box-shadow: 0 0 0; } .select-auth-box-parent { border-top: 1px solid var(--pf-global--palette--black-200); padding-top: 1rem; padding-bottom: 1rem; cursor: pointer; } .select-auth-box-headline { font-size: var(--pf-global--FontSize--md); color: var(--pf-global--primary-color--100); font-weight: bold; } .select-auth-box-icon { display: flex; flex: 0 0 2em; justify-content: center; margin-right: 1rem; margin-left: 3rem; } .pf-l-split__item.pf-m-fill { flex-grow: 1; } /*tablet*/ @media (max-width: 840px) { .login-pf-page .card-pf { max-width: none; margin-left: 20px; margin-right: 20px; padding: 20px 20px 30px 20px; } } @media (max-width: 767px) { .login-pf-page .card-pf { max-width: none; margin-left: 0; margin-right: 0; padding-top: 0; } .card-pf.login-pf-accounts { max-width: none; } } .login-pf-page .login-pf-signup { font-size: 15px; color: #72767b; } #kc-content-wrapper .row { margin-left: 0; margin-right: 0; } @media (min-width: 768px) { .login-pf-page .login-pf-social-section:first-of-type { padding-right: 39px; border-right: 1px solid #d1d1d1; margin-right: -1px; } .login-pf-page .login-pf-social-section:last-of-type { padding-left: 40px; } .login-pf-page .login-pf-social-section .login-pf-social-link:last-of-type { margin-bottom: 0; } } .login-pf-page .login-pf-social-link { margin-bottom: 25px; } .login-pf-page .login-pf-social-link a { padding: 2px 0; } .login-pf-page.login-pf-page-accounts { margin-left: auto; margin-right: auto; } .login-pf-page .btn-primary { margin-top: 0; } .login-pf-page .list-view-pf .list-group-item { border-bottom: 1px solid #ededed; } .login-pf-page .list-view-pf-description { width: 100%; } .login-pf-page .card-pf { margin-bottom: 10px; } #kc-form-login div.form-group:last-of-type, #kc-register-form div.form-group:last-of-type, #kc-update-profile-form div.form-group:last-of-type { margin-bottom: 0px; } #kc-back { margin-top: 5px; } form#kc-select-back-form div.login-pf-social-section { padding-left: 0px; border-left: 0px; } ================================================ FILE: keycloak/themes/internal-modern/login/resources/js/custom-modern-login.js ================================================ // custom-login.js (function initTheme() { console.log("internal modern theme"); // hack to add mobile icon for sms authenticator, needs to be called after dom ready function updateAuthenticatorIconsInAuthenticationSelector() { { let elements = [...document.querySelectorAll('div.pf-c-title')].filter(elem => elem.textContent.includes('SMS')); if (elements && elements.length > 0) { console.log("patch mobile icon"); elements[0].parentElement.parentElement.querySelector("i").classList.add("fa-mobile"); } } { let emailCodeAuthElements = [...document.querySelectorAll('div.pf-c-title')].filter(elem => elem.textContent.toLowerCase().replace("-","").includes('email code')); if (emailCodeAuthElements && emailCodeAuthElements.length > 0) { console.log("patch email-code icon"); emailCodeAuthElements[0].parentElement.parentElement.querySelector("i").classList.add("fa-envelope"); } } } function enableInactivityMonitoring() { let idleSinceTimestamp = Date.now(); const maxIdleMinutesBeforeAutoReload = 29; const autoReloadInactivityThresholdMillis = maxIdleMinutesBeforeAutoReload * 60 * 1000 var hidden, visibilityChange; if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support hidden = "hidden"; visibilityChange = "visibilitychange"; } else if (typeof document.msHidden !== "undefined") { hidden = "msHidden"; visibilityChange = "msvisibilitychange"; } else if (typeof document.webkitHidden !== "undefined") { hidden = "webkitHidden"; visibilityChange = "webkitvisibilitychange"; } function handleVisibilityChange() { const now = Date.now(); if (document[hidden]) { idleSinceTimestamp = now; } else { if (now > idleSinceTimestamp + autoReloadInactivityThresholdMillis) { location.reload(); } } } if (typeof document.addEventListener === "undefined" || hidden === undefined) { console.log("This demo requires a browser, such as Google Chrome or Firefox, that supports the Page Visibility API."); } else { // Handle page visibility change document.addEventListener(visibilityChange, handleVisibilityChange, false); } } function autoSubmitLoginHintForUsernameFormForCompanyApps() { if (window.location.href.includes("/company-users/") && new URLSearchParams(window.location.search).get("login_hint")) { // only for company-users realm if login hint is present if (document.querySelector("input[name=username]") && !document.querySelector("input[name=password]")) { log.info("autoSubmitLoginHintForUsernameFormForCompanyApps"); // we are in username name form document.querySelector("#kc-form-login").submit(); } } } function onDomContentLoaded() { updateAuthenticatorIconsInAuthenticationSelector(); autoSubmitLoginHintForUsernameFormForCompanyApps(); enableInactivityMonitoring(); } document.addEventListener('DOMContentLoaded', evt => onDomContentLoaded()); })(); ================================================ FILE: keycloak/themes/internal-modern/login/select-authenticator.ftl ================================================ <#import "template.ftl" as layout> <@layout.registrationLayout displayInfo=false; section> <#if section = "header" || section = "show-username"> <#if section = "header"> ${msg("loginChooseAuthenticator")} <#elseif section = "form">
<#-- auth.authenticationSelections changed to acme.authenticationSelections to narrow down authenticator selections--> <#list acmeLogin.authenticationSelections as authenticationSelection>
${msg('${authenticationSelection.displayName}')}
${msg('${authenticationSelection.helpText}')}
================================================ FILE: keycloak/themes/internal-modern/login/template.ftl ================================================ <#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false displayWide=false showAnotherWayIfPresent=true> lang="${locale.currentLanguageTag}" dir="${(locale.rtl)?then('rtl','ltr')}"> <#if properties.meta?has_content> <#list properties.meta?split(' ') as meta> ${msg("loginTitle",(realm.displayName!''))} <#if properties.stylesCommon?has_content> <#list properties.stylesCommon?split(' ') as style> <#if properties.styles?has_content> <#list properties.styles?split(' ') as style> <#if properties.scripts?has_content> <#list properties.scripts?split(' ') as script> <#if scripts??> <#list scripts as script> <#if authenticationSession??>
${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> <#if displayRequiredFields>
* ${msg("requiredFields")}

<#nested "header">

<#else>

<#nested "header">

<#else> <#if displayRequiredFields>
* ${msg("requiredFields")}
<#nested "show-username">
<#else> <#nested "show-username">
<#if client??>

${advancedMsg(client.name!'')}

<#-- App-initiated actions should not see warning messages about the need to complete the action --> <#-- during login. --> <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
<#if message.type = 'success'> <#if message.type = 'warning'> <#if message.type = 'error'> <#if message.type = 'info'>
${kcSanitize(message.summary)?no_esc}
<#nested "form"> <#if auth?has_content && auth.showTryAnotherWayLink() && showAnotherWayIfPresent>
<#if displayInfo>
<#nested "info">
<#nested "socialProviders">

<#if realm.internationalizationEnabled && locale.supported?size gt 1>
${locale.current}
================================================ FILE: keycloak/themes/internal-modern/login/theme.properties ================================================ parent=internal import=common/keycloak # Custom Styles styles=css/custom-modern-login.css stylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css # Custom JavaScript scripts=js/custom-modern-login.js # Custom Page Metadata meta=viewport==width=device-width,initial-scale=1 kcResetFlowIcon=pficon pficon-edit fa kcUserIcon=pficon pficon-user fa kcSignUpClass=kcInfoMessage ## Password visibility kcFormPasswordVisibilityButtonClass=pf-c-button pf-m-control kcFormPasswordVisibilityIconShow=fa fa-eye kcFormPasswordVisibilityIconHide=fa fa-eye-slash ================================================ FILE: keycloak/themes/minimal/login/resources/css/custom-login.css ================================================ .login-pf body { background: #ffa93b; } .card-pf { border-radius: 20px; border: 2px; } #kc-header-wrapper { text-transform: none; } .login-pf-page .login-pf-header h1 { text-align: left; } body { font-family: Verdana, sans-serif; } .pf-c-button.pf-m-primary { background-color: #faa020; } .custom-form-group { margin-bottom: 30px; } #kc-header-wrapper { background-image: url("../img/logo.svg"); background-position: center top; background-repeat: no-repeat; background-size: auto 60px; padding-top: 100px; margin-top: 30px; } ================================================ FILE: keycloak/themes/minimal/login/resources/js/custom-login.js ================================================ console.log("Custom JS provided by theme.") ================================================ FILE: keycloak/themes/minimal/login/theme.properties ================================================ parent=keycloak import=common/keycloak styles=css/login.css css/custom-login.css scripts=js/custom-login.js ================================================ FILE: keycloak/themes/minimal-branding/login/customizations.ftl ================================================ <#macro passwordPolicyCheck> <#if acmeLogin.passwordPolicy??>

Password Policy Check

${(acmeLogin.passwordPolicy)!'#'}
<#macro requiredActionInfo> <#if acmeLogin.lastProcessedAction??>

Required Action Info

${(acmeLogin.lastProcessedAction)!'#'}
================================================ FILE: keycloak/themes/minimal-branding/login/info.ftl ================================================ <#import "template.ftl" as layout> <#import "customizations.ftl" as customizations> <@layout.registrationLayout displayMessage=false; section> <#if section = "header"> <#if messageHeader??> ${kcSanitize(msg("${messageHeader}"))?no_esc} <#else> ${message.summary} <#elseif section = "form">

${message.summary}<#if requiredActions??><#list requiredActions>: <#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, <#else>

<@customizations.requiredActionInfo/> <#if skipLink??> <#else> <#if pageRedirectUri?has_content>

${kcSanitize(msg("backToApplication"))?no_esc}

<#elseif actionUri?has_content>

${kcSanitize(msg("proceedWithAction"))?no_esc}

<#elseif (client.baseUrl)?has_content>

${kcSanitize(msg("backToApplication"))?no_esc}

================================================ FILE: keycloak/themes/minimal-branding/login/login-update-password.ftl ================================================ <#import "template.ftl" as layout> <#import "customizations.ftl" as customizations> <#import "password-commons.ftl" as passwordCommons> <@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section> <#if section = "header"> ${msg("updatePasswordTitle")} <#elseif section = "form">
<#if messagesPerField.existsError('password')> ${kcSanitize(messagesPerField.get('password'))?no_esc}
<@customizations.passwordPolicyCheck/>
<#if messagesPerField.existsError('password-confirm')> ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc}
<@passwordCommons.logoutOtherSessions/>
<#if isAppInitiatedAction??> <#else>
================================================ FILE: keycloak/themes/minimal-branding/login/resources/css/custom-login.css ================================================ .login-pf body { background: #ffa93b; } .card-pf { border-radius: 20px; border: 2px; } #kc-header-wrapper { text-transform: none; } .login-pf-page .login-pf-header h1 { text-align: left; } body { font-family: Verdana, sans-serif; } .pf-c-button.pf-m-primary { background-color: #faa020; } .custom-form-group { margin-bottom: 30px; } #kc-header-wrapper { background-image: url("../img/logo.svg"); background-position: center top; background-repeat: no-repeat; background-size: auto 60px; padding-top: 100px; margin-top: 30px; } ================================================ FILE: keycloak/themes/minimal-branding/login/resources/js/custom-login.js ================================================ console.log("Custom JS provided by theme.") ================================================ FILE: keycloak/themes/minimal-branding/login/template.ftl ================================================ <#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false> lang="${locale.currentLanguageTag}"> <#if properties.meta?has_content> <#list properties.meta?split(' ') as meta> ${msg("loginTitle",(realm.displayName!''))} <#if properties.stylesCommon?has_content> <#list properties.stylesCommon?split(' ') as style> <#if properties.styles?has_content> <#list properties.styles?split(' ') as style> <#if properties.scripts?has_content> <#list properties.scripts?split(' ') as script> <#if scripts??> <#list scripts as script> <#if authenticationSession??>
${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}
<#if realm.internationalizationEnabled && locale.supported?size gt 1>
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> <#if displayRequiredFields>
* ${msg("requiredFields")}

<#nested "header">

<#else>

<#nested "header">

<#else> <#if displayRequiredFields>
* ${msg("requiredFields")}
<#nested "show-username">
<#else> <#nested "show-username">
<#-- App-initiated actions should not see warning messages about the need to complete the action --> <#-- during login. --> <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
<#if message.type = 'success'> <#if message.type = 'warning'> <#if message.type = 'error'> <#if message.type = 'info'>
${kcSanitize(message.summary)?no_esc}
<#nested "form"> <#if auth?has_content && auth.showTryAnotherWayLink()>
<#nested "socialProviders"> <#if displayInfo>
<#nested "info">
================================================ FILE: keycloak/themes/minimal-branding/login/theme.properties ================================================ parent=keycloak import=common/keycloak styles=css/login.css css/custom-login.css scripts=js/custom-login.js ================================================ FILE: keycloak.env ================================================ # Global configuration for Keycloak environment KEYCLOAK_VERSION=26.5.7 USER=1000 GROUP=1000 ================================================ FILE: maven-settings.xml ================================================ jboss-public-repository jboss-public-repository-group JBoss Public Maven Repository Group https://repository.jboss.org/nexus/content/groups/public/ default true never true never jboss-public-repository-group JBoss Public Maven Repository Group https://repository.jboss.org/nexus/content/groups/public/ default true never true never jboss-public-repository ================================================ FILE: pom.xml ================================================ 4.0.0 com.github.thomasdarimont.keycloak keycloak-project-example pom ${revision}.${changelist} ${project.organization.name} Keycloak Project keycloak/extensions keycloak/docker apps/backend-api-quarkus apps/backend-api-springboot apps/backend-api-springboot-reactive apps/backend-api-springboot3 apps/offline-session-client apps/spring-boot-device-flow-client apps/frontend-webapp-springboot apps/frontend-webapp-springboot3 apps/bff-springboot apps/bff-springboot3 apps/jwt-client-authentication apps/java-opa-embedded Acme UTF-8 21 ${java.version} ${java.version} acme/acme-keycloak keycloakx/Dockerfile.plain 26.5.7 26.0.8 1.0.0 0-SNAPSHOT 2.3.32 5.9.2 3.24.2 3.3.1 1.1.1 1.18.42 0.43.4 3.2.5 3.2.5 3.4.0 3.6.0 3.12.1 3.3.2 3.3.1 2.19.1 org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} org.apache.maven.plugins maven-clean-plugin ${maven-clean-plugin.version} org.apache.maven.plugins maven-resources-plugin ${maven-resources-plugin.version} org.codehaus.mojo versions-maven-plugin ${versions-maven-plugin.version} true false false ================================================ FILE: readme.md ================================================ Keycloak Project Example --- # Introduction This repository contains a project setup for keycloak based projects. This setup serves as a starting point to support the full lifecycle of development in a keycloak based project. This may include develop and deploy a set of Keycloak extensions, custom themes and configuration into a customized keycloak docker container (or tar-ball). The project also shows how to write integration tests via [Keycloak-Testcontainers](https://github.com/dasniko/testcontainers-keycloak). After successful test-run package all extensions and themes as a custom docker image. This image is meant to be the project base image fulfilling the projects requirements in contrast to the general keycloak image. ## Use-Cases These requirements work in different contexts, roles and use-cases: a) **Developer** for keycloak themes, extensions and image 1) build and integration-test with test-containers (uses standard keycloak image) 2) run external keycloak with hot-deploy (theme, extension, ...), run integrationtest, e2e testing a) **Developer** publishing an image: 1) Standard keycloak docker image with [extensions](./keycloak-extensions), themes und server config. 2) Slim custom docker image with extensions, themes und server config (basis alpine) chose jdk version, base-os image version, base keycloak version. c) **Tester/Developer** acceptance/e2e-testing with cypress d) **Operator** configuring realm and server for different stages ## Some Highlights - Extensions: SMS Authenticator, Backup-Codes, Remote Claim Mapper, Audit Event Listener, and Custom REST Endpoint the can expose custom endpoints: `CustomResource` - Support for deploying extensions to a running Keycloak container - Support for instant reloading of theme and extension code changes - Support Keycloak configuration customization via CLI scripts - Examples for Integration Tests with [Keycloak-Testcontainers](https://github.com/dasniko/testcontainers-keycloak) - Example for End-to-End Tests with [Cypress](https://www.cypress.io/) - Realm configuration as Configuration as Code via [keycloak-config-cli](https://github.com/adorsys/keycloak-config-cli) - Example configurations to run Keycloak against different databases (PostgreSQL, MySQL, Oracle, MSSQL) - Multi-realm setup example with OpenID Connect and SAML based Identity Brokering - LDAP based User Federation backed by [Docker-OpenLDAP](https://github.com/osixia/docker-openldap) - Mail Server integration backed by [maildev](https://github.com/maildev/maildev) - TLS Support - Support for exposing metrics via smallrye-metrics - Examples for running a cluster behind a reverse proxy with examples for [HAProxy](deployments/local/cluster/haproxy), [Apache](deployments/local/cluster/apache), [nginx](deployments/local/cluster/nginx), [caddy](deployments/local/cluster/caddy) - Examples for running a Keycloak cluster with an external infinispan cluster with [remote cache store](deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-remote.yml) and [hotrod cache store](deployments/local/cluster/haproxy-external-ispn/docker-compose-haproxy-ispn-hotrod.yml). - Example for Keycloak with [Graylog](https://www.graylog.org/) for log analysis, dashboards and alerting. - Example for metrics collection and dashboards with [Prometheus](https://prometheus.io) and [Grafana](https://grafana.com/oss). - Example for tracing with [OpenTelemetry](https://opentelemetry.io/) and [Jaeger](https://www.jaegertracing.io/) ## Usage envcheck | Tool | Version |------|-------- | Java | 17 | mvn | 3.8 | docker | 24.0 (with docker compose) # Development Environment ## Build The project can be build with the following maven command: ``` mvn clean verify ``` ### Build with Integration Tests The example can be build with integration tests by running the following maven command: ``` mvn clean verify -Pwith-integration-tests ``` ## Run We provide a platform-agnostic single-file source-code Java launcher [start.java](start.java) to start the Keycloak environment. To speed up development we can mount the [keycloak/extensions](keycloak/extensions) class-folder and [keycloak/themes](keycloak/themes) folder into a Keycloak container that is started via docker-compose (see below). This allows for quick turnarounds while working on themes and extensions. The default Keycloak admin username is `admin` with password `admin`. ### Run with HTTP You can start the Keycloak container via: ``` java start.java ``` Keycloak will be available on http://localhost:8080/auth. ### Enable HTTPS The example environment can be configured with https via the `--https` flag. #### Preparation Generate a certificate and Key for the example domain `acme.test` with [mkcert](https://github.com/FiloSottile/mkcert). ``` java bin/createTlsCerts.java # AND java bin/createTlsCerts.java --pkcs12 --keep ``` This will generate a TLS certificates and key file in `.pem` format in `config/stage/dev/tls`. The later command will create a certificate in `.p12` PKCS12 format, which will be used as a custom truststore by Keycloak. Register map the following host names in your hosts file configuration, e.g. `/etc/hosts` on linux / OSX or `c:\Windows\System32\Drivers\etc\hosts` on Windows: ``` 127.0.0.1 acme.test id.acme.test apps.acme.test admin.acme.test ops.acme.test ``` #### Run with HTTPS ``` java start.java --https ``` The Keycloak admin-console will be available on https://admin.acme.test:8443/auth/admin. Note that after changing extensions code you need to run the `java bin/triggerDockerExtensionDeploy.java` script to trigger a redeployment of the custom extension by Keycloak. ### Enable OpenLDAP The example environment can be configured with OpenLDAP via the `--openldap` flag. #### Run with OpenLDAP ``` java start.java --openldap ``` ### Enable Postgresql The example environment can be configured to use Postgresql as a database via the `--database=postgres` flag to override the default `h2` database. #### Run with Postgresql ``` java start.java --database=postgres ``` ### Access metrics The example environment includes an smallrye-metrics and eclipse-metrics integration for wildfly. Metrics are exposed via the wildfly management interface on http://localhost:9990/metrics Realm level metrics are collected by a custom `EventListenerProvider` called `metrics`. ### Enable Graylog The example environment can be configured to send Keycloak's logout output to Graylog via the `--logging=graylog` option. Note that you need to download the [`logstash-gelf` wildfly module](https://search.maven.org/remotecontent?filepath=biz/paluch/logging/logstash-gelf/1.14.1/logstash-gelf-1.14.1-logging-module.zip) and unzip the libraries into the [deployments/local/dev/graylog/modules](deployments/local/dev/graylog/modules) folder. ``` cd deployments/local/dev/graylog/modules wget -O logstash-gelf-1.14.1-logging-module.zip https://search.maven.org/remotecontent?filepath=biz/paluch/logging/logstash-gelf/1.14.1/logstash-gelf-1.14.1-logging-module.zip unzip -o logstash-gelf-1.14.1-logging-module.zip rm *.zip ``` #### Run with Graylog ``` java start.java --logging=graylog ``` ### Enable Prometheus Prometheus can scrape0 metrics from configured targets and persists the collected data in a time series database. The metrics data can be used to create monitoring dashboards with tools like grafana (see [Grafana](#enable-grafana)). Scrape targets configured: |System| Target |Additional Labels |------|----------------------------------------|------ |keycloak | http://acme-keycloak:8080/auth/metrics | env #### Run with Prometheus ``` java start.java --metrics=prometheus ``` ### Enable Grafana Grafana supports dashboards and alerting based on data from various datasources. Note: To enable grafana with tls, a permission change is required as docker does not support a way to map users for shared files. You need to add read permissions for the key file `acme.test+1-key.pem` in config/stage/dev/tls for the group of the current user. Access to Grafana can be configured in multiple ways, even a login with Keycloak is possible. In this example we use configured admin user account to access Grafana, but we also offer a login via Keycloak by leveraging the generic OAuth integration. Grafana is configured to not allow login as guest. #### Run with Grafana ``` java start.java --grafana ``` Open [Grafana](https://apps.acme.test:3000/grafana) Manual steps when logged in as an Admin (Example User: devops_fallback, Password: test) * Configure datasource * Add e.g. prometheus as datasource (http://acme-prometheus:9090/ installed by default) (see [Grafana](#enable-prometheus)) * Add e.g. elastic-search as datasource (http://acme-graylog-lo:9090/) (see [Graylog](#enable-graylog) services) * Import Boards of your choice from [Grafana](https://grafana.com/grafana/dashboards) (for testing an [exported board](../../../config/stage/dev/grafana/microprofile-wildfly-16-metrics_rev1.json) can be used) ### Enable Tracing With [OpenTelemetry](https://opentelemetry.io/) and [Jaeger](https://www.jaegertracing.io/), it is possible to trace requests traveling through Keycloak and the systems integrating it. This uses the Quarkus OpenTelemetry extension in order to create traces, which are then sent to the [otel-collector](https://opentelemetry.io/docs/collector/). The collector then passes the information on to Jaeger, where they can be viewed in the web interface #### Run with Tracing ``` java start.java --tracing ``` Open [Jaeger](http://ops.acme.test:16686) or [Jaeger with TLS](https://ops.acme.test:16686), depending on configuration. When TLS is enabled, it is enabled for all three of the following: * Jaeger UI * Keycloak -> Collector communication * Collector -> Jaeger communication #### Instrumentation In order to gain additional insights, other applications that integrate with Keycloak can also send traces to the collector. The [OpenTelemetry Documentation](https://opentelemetry.io/docs/instrumentation/) contains tools to instrument applications in various languages. You can use the `bin/downloadOtel.java` scrtipt to download the otel agent. Quarkus applications like Keycloak can also use the [Quarkus OpenTelemetry extension](https://quarkus.io/guides/opentelemetry) instead of the agent. An example for running an instrumented Spring Boot app could look like this: ``` OTEL_METRICS_EXPORTER=none \ OTEL_SERVICE_NAME="frontend-webapp-springboot" \ OTEL_PROPAGATORS="b3multi" \ OTEL_EXPORTER_OTLP_ENDPOINT="http://id.acme.test:4317" \ java -javaagent:bin/opentelemetry-javaagent.jar \ -jar apps/frontend-webapp-springboot/target/frontend-webapp-springboot-0.0.1-SNAPSHOT.jar ``` The included IDEA run-config for the frontend-webapp-springboot module contains the necessary configuration to run that module with tracing enabled. If you then navigate to the [frontend webapp](https://apps.acme.test:4633/webapp/), you can navigate through the application, and then later check the Jaeger UI for traces. ### Clustering Clustering examples can be found in the [deployments/local/cluster](deployments/local/cluster) folder. ### Running with non-default docker networks Some features of this project setup communicate with services inside the docker stack through the host. By default, the IP of the host in Docker is `172.17.0.1`, but this can be changed by configuration. One reason to change it is because Wi-Fi on ICE trains uses IP addresses from the same network. An example for a changed setup from `/etc/docker/daemon.json` can look like this: ````json { "default-address-pools": [ {"base":"172.19.0.0/16","size":24} ] } ```` In this case, the host IP is `172.19.0.1`, which can be configured for the project using the start option `--docker-host=172.19.0.1` ## Acme Example Realm Configuration ### Realms The example environment contains several realms to illustrate the interaction of different realms. #### Acme-Apps Realm The `acme-apps` realm contains a simple demo application and provides integration with the `acme-internal`, `acme-ldap` and `acme-saml` realm via Identity Brokering. The idea behind this setup is to provide a global `acme-apps` realm for applications that are shared between internal and external users. The `acme-internal` realm provides applications that are only intended for internal users. The `acme-ldap` realm provides applications that are only intended for employees. The `acme-internal` and `acme-ldap` realms serve as an OpenID Connect based Identity Provider for the `acme-apps` realm. The `acme-saml` realm provides applications is similar to the `acme-internal` and serves as a SAML based Identity Provider for the `acme-apps` realm. #### Acme-Internal Realm The `acme-internal` realm contains a test users which are stored in the Keycloak database. Users: - Username `tester` and password `test` (from database) - Username `support` and password `test` (from database) The support user has access to a [dedicated realm scoped admin-console](https://www.keycloak.org/docs/latest/server_admin/index.html#_per_realm_admin_permissions) and can perform user and group lookups. An example for a realm scoped admin-console URL is: `https://admin.acme.test:8443/auth/admin/acme-internal/console`. #### Acme-LDAP Realm The `acme-ldap` realm contains a test user and is connected to a federated user store (LDAP directory) provided via openldap. - Username `FleugelR` and password `Password1` (from LDAP federation) #### Acme-SAML Realm The `acme-saml` realm contains a test user and stores the users in the Keycloak database. Users: - Username `acmesaml` and password `test` (from database) #### Example App A simple demo app can be used to show information from the Access-Token, ID-Token and UserInfo endpoint provided by Keycloak. The demo app is started and will be accessible via http://localhost:4000/?realm=acme-internal or https://apps.acme.test:4443/?realm=acme-internal. # Deployment ## Custom Docker Image ### Build a custom Docker Image The dockerfile for the docker image build uses the [keycloak/Dockerfile.plain](keycloak/docker/src/main/docker/keycloak/Dockerfile.plain) by default. To build a custom Keycloak Docker image that contains the custom extensions and themes, you can run the following command: ```bash mvn clean verify -Pwith-integration-tests io.fabric8:docker-maven-plugin:build ``` The dockerfile can be customized via `-Ddocker.file=keycloak/Dockerfile.alpine-slim` after `mvn clean verify`. It is also possible to configure the image name via `-Ddocker.image=acme/acme-keycloak2`. To build the image with Keycloak.X use: ``` mvn clean package -DskipTests -Ddocker.file=keycloakx/Dockerfile.plain io.fabric8:docker-maven-plugin:build ``` ### Running the custom Docker Image locally The custom docker image created during the build can be stared with the following command: ``` docker run \ --name acme-keycloak \ -e KEYCLOAK_ADMIN=admin \ -e KEYCLOAK_ADMIN_PASSWORD=admin \ -e KC_HTTP_RELATIVE_PATH=auth \ -it \ --rm \ -p 8080:8080 \ acme/acme-keycloak:latest \ start-dev \ --features=preview ``` # Testing ## Run End to End Tests The [cypress](https://www.cypress.io/) based End to End tests can be found in the [keycloak-e2e](./keycloak-e2e) folder. To run the e2e tests, start the Keycloak environment and run the following commands: ``` cd keycloak-e2e yarn run cypress:open # yarn run cypress:test ``` # Scripts ## Check prerequisites To manually check if all prerequisites are fulfilled. ``` java bin/envcheck.java ``` ## Import-/Exporting a Realm To import/export of an existing realm as JSON start the docker-compose infrastructure and run the following script. The export will create a file like `acme-apps-realm.json` in the `./keycloak/imex` folder. ``` java bin/realmImex.java --realm=acme-internal --verbose ``` The import would search an file `acme-apps-realm.json` in the `./keycloak/imex` folder. ``` java bin/realmImex.java --realm=acme-internal --verbose --action=import ``` # Tools ## maildev Web Interface: http://localhost:1080/mail Web API: https://github.com/maildev/maildev/blob/master/docs/rest.md ## phpldapadmin Web Interface: http://localhost:17080 Username: cn=admin,dc=corp,dc=acme,dc=local Password: admin # Misc ## Add external tool in IntelliJ to trigger realm configuration Instead of running the Keycloak Config CLI script yourself, you can register it as an external tool in IntelliJ as shown below. - Name: `kc-deploy-config` - Description: `Deploy Realm Config to Keycloak Docker Container` - Program: `$JDKPath$/bin/java` - Arguments: `$ProjectFileDir$/bin/applyRealmConfig.java` - Working directory: `$ProjectFileDir$` - Only select: `Synchronize files after execution.` The extensions can now be redeployed by running `Tools -> External Tools -> kc-deploy-config` ================================================ FILE: start.java ================================================ import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.CopyOption; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; import java.util.stream.Collectors; /** * Controller script to start the Keycloak environment. * *

Run Keycloak with http

*
{@code
 *  java start.java
 * }
* *

Run Keycloak with https

*
{@code
 *  java start.java --https
 * }
* *

Run Keycloak with https and openldap

*
{@code
 *  java start.java --https --openldap
 * }
* *

Run Keycloak with https, openldap and postgres database

*
{@code
 *  java start.java --https --openldap --database=postgres
 * }
*/ class start { static final String HELP_CMD = "--help"; static final String VERBOSE_OPT = "--verbose"; static final String CI_OPT = "--ci"; static final String HTTP_OPT = "--http"; static final String HTTPS_OPT = "--https"; static final String PROVISION_OPT = "--provision"; static final String OPENLDAP_OPT = "--openldap"; static final String OPA_OPT = "--opa"; static final String KEYCLOAK_OPT = "--keycloak=keycloak"; static final String POSTGRES_OPT = "--database=postgres"; static final String NATS_OPT = "--messaging=nats"; static final String ORACLE_OPT = "--database=oracle"; static final String MSSQL_OPT = "--database=mssql"; static final String MYSQL_OPT = "--database=mysql"; static final String GRAYLOG_OPT = "--logging=graylog"; static final String GRAFANA_OPT = "--grafana"; static final String PROMETHEUS_OPT = "--metrics=prometheus"; static final String EXTENSIONS_OPT = "--extensions="; static final String EXTENSIONS_OPT_CLASSES = "classes"; static final String EXTENSIONS_OPT_JAR = "jar"; static final String DETACH_OPT = "--detach"; static final String TRACING_OPT = "--tracing"; static final String DOCKER_HOST_OPT = "--docker-host="; public static void main(String[] args) throws Exception { var argList = Arrays.asList(args); var useKeycloakx = !argList.contains(KEYCLOAK_OPT); // --keycloak=keycloakx is implied by default var useHttp = !argList.contains(HTTP_OPT + "=false"); // --http is implied by default var useHttps = argList.contains(HTTPS_OPT) || argList.contains(HTTPS_OPT + "=true"); var useProvision = !argList.contains(PROVISION_OPT + "=false"); var useOpenLdap = argList.contains(OPENLDAP_OPT) || argList.contains(OPENLDAP_OPT + "=true"); var usePostgres = argList.contains(POSTGRES_OPT); var useOpa = argList.contains(OPA_OPT); var useMssql = argList.contains(MSSQL_OPT); var useMysql = argList.contains(MYSQL_OPT); var useOracle = argList.contains(ORACLE_OPT); var useDatabase = usePostgres || useMysql || useMssql || useOracle; var useGraylog = argList.contains(GRAYLOG_OPT); var useGrafana = argList.contains(GRAFANA_OPT); var usePrometheus = argList.contains(PROMETHEUS_OPT); var extension = argList.stream().filter(s -> s.startsWith(EXTENSIONS_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(EXTENSIONS_OPT_CLASSES); var ci = argList.stream().filter(s -> s.startsWith(CI_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst().orElse(null); var useDetach = argList.contains(DETACH_OPT); var verbose = argList.contains(VERBOSE_OPT); var useTracing = argList.contains(TRACING_OPT); var dockerHost = argList.stream().filter(s -> s.startsWith(DOCKER_HOST_OPT)).map(s -> s.substring(s.indexOf("=") + 1)).findFirst(); var useNats = argList.contains(NATS_OPT); var useSaml = true; var showHelp = argList.contains(HELP_CMD); if (showHelp) { showHelp(); System.exit(0); return; } if (useDatabase && !(useMysql ^ usePostgres ^ useMssql ^ useOracle)) { System.out.println("Invalid database configuration detected. Only one --database parameter is allowed!"); showHelp(); System.exit(-1); } // Keycloak createFolderIfMissing("deployments/local/dev/run/keycloak/logs"); createFolderIfMissing("deployments/local/dev/run/keycloak/data"); createFolderIfMissing("deployments/local/dev/run/keycloak/perf"); // Keycloak-X createFolderIfMissing("deployments/local/dev/run/keycloakx/logs"); createFolderIfMissing("deployments/local/dev/run/keycloakx/data"); createFolderIfMissing("deployments/local/dev/run/keycloakx/perf"); System.out.println("### Starting Keycloak Environment with HTTP" + (useHttps ? "S" : "")); System.out.printf("# Keycloak: %s%n", useHttps ? "https://id.acme.test:8443/auth" : "http://localhost:8080/auth"); System.out.printf("# MailHog: %s%n", "http://localhost:1080"); if (useOpenLdap) { System.out.printf("# PhpMyLdapAdmin: %s%n", "http://localhost:17080"); } var envFiles = new ArrayList(); var requiresBuild = true; var commandLine = new ArrayList(); commandLine.add("docker"); commandLine.add("compose"); envFiles.add("keycloak.env"); envFiles.add("deployments/local/dev/keycloak-common.env"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose.yml"); commandLine.add("--file"); if (useKeycloakx) { commandLine.add("deployments/local/dev/docker-compose-keycloakx.yml"); } else { commandLine.add("deployments/local/dev/docker-compose-keycloak.yml"); } if ("github".equals(ci)) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-ci-github.yml"); } if (useHttp) { envFiles.add("deployments/local/dev/keycloak-http.env"); } if (useHttps) { envFiles.remove("deployments/local/dev/keycloak-http.env"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-tls.yml"); envFiles.add("deployments/local/dev/keycloak-tls.env"); } if (useOpenLdap) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-openldap.yml"); envFiles.add("deployments/local/dev/keycloak-openldap.env"); } if (useOpa) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-opa.yml"); } // if (EXTENSIONS_OPT_CLASSES.equals(extension)) { // commandLine.add("--file"); // commandLine.add("deployments/local/dev/docker-compose-extensions-classes.yml"); // } else if (EXTENSIONS_OPT_JAR.equals(extension)) { // commandLine.add("--file"); // commandLine.add("deployments/local/dev/docker-compose-extensions-jar.yml"); // } else { // System.err.printf("Unkown extension include option %s, valid ones are %s and %s%n", extension, EXTENSIONS_OPT_CLASSES, EXTENSIONS_OPT_JAR); // System.exit(-1); // } if (usePostgres) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-postgres.yml"); createFolderIfMissing("deployments/local/dev/run/postgres/data/"); requiresBuild = true; } else if (useMysql) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-mysql.yml"); createFolderIfMissing("deployments/local/dev/run/mysql/data/"); requiresBuild = true; } else if (useMssql) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-mssql.yml"); createFolderIfMissing("deployments/local/dev/run/mssql/data/"); requiresBuild = true; } else if (useOracle) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-oracle.yml"); createFolderIfMissing("deployments/local/dev/run/oracle/data/"); requiresBuild = true; } if (useGraylog) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-graylog.yml"); createFolderIfMissing("deployments/local/dev/run/graylog/data/mongodb"); requiresBuild = true; } if (useGrafana) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-grafana.yml"); createFolderIfMissing("deployments/local/dev/run/grafana"); } if (usePrometheus) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-prometheus.yml"); createFolderIfMissing("deployments/local/dev/run/prometheus"); } if (useProvision) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-provisioning.yml"); envFiles.add("deployments/local/dev/keycloak-provisioning.env"); } if (useNats) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-nats.yml"); } if (useTracing) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-tracing.yml"); if (useHttps) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-tracing-tls.yml"); var certPath = Path.of("config/stage/dev/tls/acme.test+1.pem"); if (certPath.toFile().exists()) { var targetPath = Path.of("deployments/local/dev/otel-collector").resolve(certPath.getFileName()); System.out.printf("Copy cert files for otel-collector from %s to %s%n", certPath, targetPath); Files.copy(certPath, targetPath, StandardCopyOption.REPLACE_EXISTING); } var keyPath = Path.of("config/stage/dev/tls/acme.test+1-key.pem"); if (keyPath.toFile().exists()) { var targetPath = Path.of("deployments/local/dev/otel-collector").resolve(keyPath.getFileName()); System.out.printf("Copy cert files for otel-collector from %s to %s%n", keyPath, targetPath); Files.copy(keyPath, targetPath, StandardCopyOption.REPLACE_EXISTING); } } envFiles.add("deployments/local/dev/keycloak-tracing.env"); } if (useSaml) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-simplesaml.yml"); } if (Files.exists(Path.of("local.env"))) { System.out.println("Adding local.env"); envFiles.add("local.env"); } //-BEGIN env vars StringBuilder envVariables = new StringBuilder(); for (String envFile : envFiles) { envVariables.append(Files.readString(Paths.get(envFile))).append("\n"); } if (useHttps) { // add quotes around path in case of spaces in path envVariables.append("CA_ROOT_CERT=\"" + getRootCALocation() + "/rootCA.pem\""); envVariables.append("\n"); } if (useHttp && useKeycloakx) { Path certPath = Path.of("config/stage/dev/tls/acme.test+1.pem"); if (certPath.toFile().exists()) { Path targetPath = Path.of("deployments/local/dev/keycloakx").resolve(certPath.getFileName()); System.out.printf("Copy cert file for truststore import from %s to %s%n", certPath, targetPath); Files.copy(certPath, targetPath, StandardCopyOption.REPLACE_EXISTING); } } if (dockerHost.isPresent()) { envVariables.append(String.format("DOCKER_HOST_IP=\"%s\"", dockerHost.get())); } if (!envVariables.toString().isBlank()) { String generatedEnvFile = "generated.env.tmp"; Files.writeString(Paths.get(generatedEnvFile), envVariables.toString()); commandLine.add("--env-file"); commandLine.add(generatedEnvFile); } //-END env vars commandLine.add("up"); if (useDetach) { commandLine.add("--detach"); } if (requiresBuild) { commandLine.add("--build"); } commandLine.add("--remove-orphans"); if (verbose) { System.out.printf("Generated command: %n```%n%s%n```%n", commandLine.stream().collect(Collectors.joining(" \\\n"))); } System.exit(runCommandAndWait(commandLine)); } private static void showHelp() { System.out.println("Keycloak Environment starter"); System.out.printf("%n%s supports the following options: %n", "start.java"); System.out.println(""); System.out.printf(" %s: %s%n", HTTP_OPT, "enables HTTP support."); System.out.printf(" %s: %s%n", HTTPS_OPT, "enables HTTPS support. (Optional) Implies --http. If not provided, plain HTTP is used"); System.out.printf(" %s: %s%n", PROVISION_OPT, "enables provisioning via keycloak-config-cli."); System.out.printf(" %s: %s%n", OPENLDAP_OPT, "enables OpenLDAP support. (Optional)"); System.out.printf(" %s: %s%n", POSTGRES_OPT, "enables PostgreSQL database support. (Optional) If no other database is provided, H2 database is used"); System.out.printf(" %s: %s%n", MYSQL_OPT, "enables MySQL database support. (Optional) If no other database is provided, H2 database is used"); System.out.printf(" %s: %s%n", ORACLE_OPT, "enables Oracle database support. (Optional) If no other database is provided, H2 database is used"); System.out.printf(" %s: %s%n", GRAYLOG_OPT, "enables Graylog database support. (Optional)"); System.out.printf(" %s: %s%n", EXTENSIONS_OPT, "choose dynamic extensions extension based on \"classes\" or static based on \"jar\""); System.out.printf(" %s: %s%n", DETACH_OPT, "Detached mode: Run containers in the background and prints the container name.. (Optional)"); System.out.printf(" %s: %s%n", VERBOSE_OPT, "Shows debug information, such as the generated command"); System.out.printf(" %s: %s%n", DOCKER_HOST_OPT, "Allows configuring of a non-default IP for reaching the docker host from inside the containers, " + "which is used for name resolution. This is useful for using WiFi on ICE trains, which use the same network as docker by default. This causes the wifi to not work correctly."); System.out.printf(" %s: %s%n", TRACING_OPT, "enables tracing with open-telemetry. Injects the otel agent into Keycloak, starts an otel-collector and jaeger container"); System.out.printf(" %s: %s%n", PROMETHEUS_OPT, "enables metrics collection to prometheus. Starts a prometheus metrics container"); System.out.printf(" %s: %s%n", NATS_OPT, "enables nats message broker"); System.out.printf("%n%s supports the following commands: %n", "start.java"); System.out.println(""); System.out.printf(" %s: %s%n", HELP_CMD, "Shows this help message"); System.out.printf("%n Usage examples: %n"); System.out.println(""); System.out.printf(" %s %s%n", "java start.java", "# Start Keycloak Environment with http"); System.out.printf(" %s %s%n", "java start.java --https", "# Start Keycloak Environment with https"); System.out.printf(" %s %s%n", "java start.java --https --verbose", "# Start Keycloak Environment with https and print command"); System.out.printf(" %s %s%n", "java start.java --provision=false", "# Start Keycloak Environment without provisioning"); System.out.printf(" %s %s%n", "java start.java --https --database=postgres", "# Start Keycloak Environment with PostgreSQL database"); System.out.printf(" %s %s%n", "java start.java --https --openldap --database=postgres", "# Start Keycloak Environment with PostgreSQL database and OpenLDAP"); System.out.printf(" %s %s%n", "java start.java --extensions=classes", "# Start Keycloak with extensions mounted from classes folder. Use --extensions=jar to mount the jar file into the container"); System.out.printf(" %s %s%n", "java start.java --docker-host=172.19.0.1", "# Configure a non-default IP for the docker host."); } private static int runCommandAndWait(ArrayList commandLine) { var pb = new ProcessBuilder(commandLine); pb.directory(new File(".")); // disable docker compose menu in shell pb.environment().put("COMPOSE_MENU", "false"); pb.inheritIO(); try { var process = pb.start(); return process.waitFor(); } catch (Exception ex) { System.err.printf("Could not run command: %s.", commandLine); ex.printStackTrace(); return 1; } } private static void createFolderIfMissing(String folderPath) { var folder = new File(folderPath); if (!folder.exists()) { System.out.printf("Creating missing %s folder at %s success:%s%n" , folderPath, folder.getAbsolutePath(), folder.mkdirs()); } } private static String getRootCALocation() throws IOException { Runtime rt = Runtime.getRuntime(); String[] mkcertCommand = {"mkcert", "-CAROOT"}; Process proc = rt.exec(mkcertCommand); BufferedReader stdInput = new BufferedReader(new InputStreamReader(proc.getInputStream())); return stdInput.readLine().replace('\\','/'); } } ================================================ FILE: stop.java ================================================ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.stream.Collectors; /** * Controller script to stop the Keycloak environment. * *

Stop Keycloak

*
{@code
 *  java stop.java
 * }
*/ class start { static final String HELP_CMD = "help"; static final String VERBOSE_OPT = "--verbose"; public static void main(String[] args) throws IOException{ var argList = Arrays.asList(args); var verbose = argList.contains(VERBOSE_OPT); var showHelp = argList.contains(HELP_CMD); if (showHelp) { System.out.println("Keycloak Environment stopper"); System.out.println(""); System.exit(0); } System.out.println("### Stopping Keycloak Environment"); var commandLine = new ArrayList(); commandLine.add("docker"); commandLine.add("compose"); var envFile = Paths.get("generated.env.tmp"); var useHttps = false; if (Files.exists(envFile)) { commandLine.add("--env-file"); commandLine.add("generated.env.tmp"); useHttps = Files.readString(envFile).contains("CA_ROOT_CERT="); } commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose.yml"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-keycloak.yml"); if (useHttps) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-tls.yml"); } commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-openldap.yml"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-postgres.yml"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-provisioning.yml"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-graylog.yml"); commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-prometheus.yml"); if (argList.contains("--skip=grafana")) { // ignore grafana to fix invalid spec: :/etc/ssl/certs/ca-cert-acme-root.crt:z: empty section between colons } else { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-grafana.yml"); } commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-tracing.yml"); if (useHttps) { commandLine.add("--file"); commandLine.add("deployments/local/dev/docker-compose-tracing-tls.yml"); } commandLine.add("down"); commandLine.add("--remove-orphans"); commandLine.add("--volumes"); if (verbose) { System.out.printf("Generated command: %n```%n%s%n```%n", commandLine.stream().collect(Collectors.joining(" \\\n"))); } var pb = new ProcessBuilder(commandLine); pb.directory(new File(".")); pb.inheritIO(); try { var process = pb.start(); System.exit(process.waitFor()); } catch (Exception ex) { System.err.println("Could not run docker compose down."); ex.printStackTrace(); System.exit(1); } } } ================================================ FILE: tools/kcadm/readme.md ================================================ Keycloak Admin Client (kcadm) --- The Keycloak distribution ships with [kcadm.sh CLI tool](https://github.com/keycloak/keycloak-documentation/blob/master/server_admin/topics/admin-cli.adoc) that allows to manage Keycloak realm configurations. # kcadm setup Although it is possible to use a `kcadm.sh` from a local Keycloak installation, we recommend to use the `kcadm.sh` that is provided from the Keycloak docker image, to ensure that compatible versions are used. ## Setup command To use `kcadm.sh` from the Keycloak docker image, we define the alias `kcadm`: ``` alias kcadm="docker run --net=host -i --user=1000:1000 --rm -v $(echo $HOME)/.acme/.keycloak:/opt/keycloak/.keycloak:z --entrypoint /opt/keycloak/bin/kcadm.sh quay.io/keycloak/keycloak:26.3.5" ``` ## Setup environment variables for clean commands ``` KEYCLOAK_REALM=acme-internal TRUSTSTORE_PASSWORD=changeit KEYCLOAK_URL=https://id.acme.test:8443/auth KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_CLIENT=demo-client ``` ## Usage with http or https When keycloak is started locally with `--http` (default) nothing is encrypted and no truststore is used. The following three subsections can be skipped. Additionally, all the commands do not need the `--trustpass $TRUSTSTORE_PASSWORD` part. When using `--https` all traffic is encrypted, and the truststore is required. Please follow the steps to make the truststore available and pass it for all commands. ### Download server certificate (if necessary) ``` echo -n | openssl s_client -connect id.acme.test:8443 -servername id.acme.test \ | openssl x509 > /tmp/id.acme.test.cert ``` ### Generate kcadm Truststore ``` keytool \ -import \ -file "config/stage/dev/tls/acme.test+1.pem" \ -keystore $(echo $HOME)/.acme/.keycloak/kcadm-truststore.jks \ -alias keycloak \ -storepass $TRUSTSTORE_PASSWORD \ -noprompt ``` ### Configure Truststore with kcadm ``` kcadm config truststore --storepass $TRUSTSTORE_PASSWORD /opt/keycloak/.keycloak/kcadm-truststore.jks ``` ## Configure credentials ``` kcadm config credentials --server $KEYCLOAK_URL --realm master --user $KEYCLOAK_ADMIN --password $KEYCLOAK_ADMIN_PASSWORD --trustpass $TRUSTSTORE_PASSWORD ``` # Use cases We collect a list of useful commands here. More examples can be found in the [official documentation](https://github.com/keycloak/keycloak-documentation/blob/master/server_admin/topics/admin-cli.adoc). ## Get realms ``` kcadm get realms --fields="id,realm" --trustpass $TRUSTSTORE_PASSWORD kcadm get realms/$KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD ``` ## Create realms ``` kcadm create realms -s realm=$KEYCLOAK_REALM -s enabled=true ``` ## Update realms ``` kcadm update realms/$KEYCLOAK_REALM -s "enabled=false" --trustpass $TRUSTSTORE_PASSWORD kcadm update realms/$KEYCLOAK_REALM -s "displayNameHtml=Wonderful world" --trustpass $TRUSTSTORE_PASSWORD ``` ## Get clients ``` kcadm get clients -r $KEYCLOAK_REALM --fields="id,clientId" --trustpass $TRUSTSTORE_PASSWORD ``` ## Create clients ``` kcadm create clients -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD -f - << EOF { "clientId": "demo-client", "rootUrl": "http://localhost:8090", "baseUrl": "/", "surrogateAuthRequired": false, "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", "secret": "1f88bd14-7e7f-45e7-be27-d680da6e48d8", "redirectUris": ["/*"], "webOrigins": ["+"], "bearerOnly": false, "consentRequired": false, "standardFlowEnabled": true, "implicitFlowEnabled": false, "directAccessGrantsEnabled": false, "serviceAccountsEnabled": false, "publicClient": false, "frontchannelLogout": false, "protocol": "openid-connect", "defaultClientScopes": ["web-origins","role_list","roles","profile","email"], "optionalClientScopes": ["address","phone","offline_access","microprofile-jwt"] } EOF ``` ## Update clients (e.g. secret) Find id of client... ``` clientUuid=$(kcadm get clients -r $KEYCLOAK_REALM --fields 'id,clientId' --trustpass $TRUSTSTORE_PASSWORD | jq -c '.[] | select(.clientId == "'$KEYCLOAK_CLIENT'")' | jq -r .id) ``` ...update attributes by id (e.g. client secret) ``` kcadm update clients/$clientUuid -r $KEYCLOAK_REALM -s "secret=abc1234" --trustpass $TRUSTSTORE_PASSWORD kcadm update clients/$clientUuid -r $KEYCLOAK_REALM -s "publicClient=true" --trustpass $TRUSTSTORE_PASSWORD ``` ## Get client by id ``` kcadm get clients/$clientUuid -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD kcadm get clients/$clientUuid/client-secret -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD ``` ## Get users ``` kcadm get users -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD kcadm get users -r $KEYCLOAK_REALM --fields="id,username,email" --trustpass $TRUSTSTORE_PASSWORD ``` ## Create users ``` kcadm create users -r $KEYCLOAK_REALM -s username=demo -s firstName=Doris -s lastName=Demo -s email='doris@localhost' -s enabled=true --trustpass $TRUSTSTORE_PASSWORD kcadm create users -r $KEYCLOAK_REALM -s username=tester -s firstName=Theo -s lastName=Tester -s email='tom+tester@localhost' -s enabled=true --trustpass $TRUSTSTORE_PASSWORD kcadm create users -r $KEYCLOAK_REALM -s username=vadmin -s firstName=Vlad -s lastName=Admin -s email='tom+vlad@localhost' -s enabled=true --trustpass $TRUSTSTORE_PASSWORD ``` ## Update users Find id of client... ``` userUuid=$(kcadm get users -r $KEYCLOAK_REALM --fields 'id,username' --trustpass $TRUSTSTORE_PASSWORD | jq -c '.[] | select(.username == "'demo'")' | jq -r .id) ``` ...update attributes by id (e.g. username) ``` kcadm update users/$userUuid -r $KEYCLOAK_REALM -s "firstName=Dolores" --trustpass $TRUSTSTORE_PASSWORD kcadm update users/$userUuid -r $KEYCLOAK_REALM -s "enabled=false" --trustpass $TRUSTSTORE_PASSWORD ``` ## Set user password ``` kcadm set-password -r $KEYCLOAK_REALM --username tester --new-password test --trustpass $TRUSTSTORE_PASSWORD kcadm set-password -r $KEYCLOAK_REALM --username vadmin --new-password test --trustpass $TRUSTSTORE_PASSWORD ``` ## Get user by id ``` kcadm get users/$userUuid -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD ``` ## Get roles ``` kcadm get roles -r $KEYCLOAK_REALM --trustpass $TRUSTSTORE_PASSWORD ``` ## Create roles ``` kcadm create roles -r $KEYCLOAK_REALM -s name=user -o --trustpass $TRUSTSTORE_PASSWORD kcadm create roles -r $KEYCLOAK_REALM -s name=admin -o --trustpass $TRUSTSTORE_PASSWORD ``` ## Assign role to user ``` kcadm add-roles -r $KEYCLOAK_REALM --uusername tester --rolename user --trustpass $TRUSTSTORE_PASSWORD kcadm add-roles -r $KEYCLOAK_REALM --uusername vadmin --rolename user --rolename admin --trustpass $TRUSTSTORE_PASSWORD ``` ## Partial export ``` kcadm create realms/$KEYCLOAK_REALM/partial-export -s exportGroupsAndRoles=true -s exportClients=true -o --trustpass $TRUSTSTORE_PASSWORD ``` ## Export profile ``` kcadm get realms/workshop/users/profile -o --trustpass $TRUSTSTORE_PASSWORD ``` ================================================ FILE: tools/postman/acme.postman_collection.json ================================================ { "info": { "_postman_id": "e419cb32-6467-40e3-bece-2365395a445a", "name": "Acme Keycloak", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { "name": "Standards", "item": [ { "name": "OIDC Authorization Code Flow", "item": [ { "name": "Authentication Request", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", "", "if(pm.response.status === 'OK') {", " const $ = cheerio.load(pm.response.text());", " const querystring = require('querystring');", " const url = require('url');", "", " const mzUrl = url.parse($(\"form#kc-form-login\").attr('action'))", " const params = querystring.parse(mzUrl.query)", "", " pm.globals.set(\"keycloak_execution\", params.execution);", " pm.globals.set(\"keycloak_session_code\", params.session_code);", " pm.globals.set(\"keycloak_tab_id\", params.tab_id);", " console.log(\"Done Step 1\");", "}" ], "type": "text/javascript" } } ], "request": { "auth": { "type": "noauth" }, "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/auth?client_id={{KEYCLOAK_STANDARD_CLIENT}}&client_secret={{KEYCLOAK_STANDARD_CLIENT_SECRET}}&redirect_uri={{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}&state=12345678&response_type=code&scope=openid profile email", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "auth" ], "query": [ { "key": "client_id", "value": "{{KEYCLOAK_STANDARD_CLIENT}}" }, { "key": "client_secret", "value": "{{KEYCLOAK_STANDARD_CLIENT_SECRET}}" }, { "key": "redirect_uri", "value": "{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}" }, { "key": "state", "value": "12345678" }, { "key": "response_type", "value": "code" }, { "key": "scope", "value": "openid profile email" } ] } }, "response": [] }, { "name": "Authenticate", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 302\", function () {", " pm.response.to.have.status(302);", "});", "", "if(pm.response.status === 'Found') {", " const querystring = require('querystring');", " const urlModule = require('url');", "", " const headers = pm.response.headers", "", " const locationHeader = headers.get(\"Location\")", "", " const mzUrl = urlModule.parse(locationHeader)", " const params = querystring.parse(mzUrl.query)", "", " pm.globals.set(\"keycloak_code\", params.code);", " pm.globals.set(\"keycloak_session_state\",params.session_state);", " ", " console.log(\"Done Step 2\");", "}" ], "type": "text/javascript" } } ], "protocolProfileBehavior": { "followRedirects": false }, "request": { "auth": { "type": "noauth" }, "method": "POST", "header": [], "body": { "mode": "urlencoded", "urlencoded": [ { "key": "username", "value": "{{KEYCLOAK_TEST_USER_EMAIL}}", "type": "text" }, { "key": "username", "value": "{{KEYCLOAK_TEST_USER}}", "type": "text", "disabled": true }, { "key": "password", "value": "{{KEYCLOAK_TEST_PASS}}", "type": "text" }, { "key": "credentialId", "value": "", "type": "text" } ] }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/login-actions/authenticate?session_code={{keycloak_session_code}}&execution={{keycloak_execution}}&client_id={{KEYCLOAK_STANDARD_CLIENT}}&tab_id={{keycloak_tab_id}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "login-actions", "authenticate" ], "query": [ { "key": "session_code", "value": "{{keycloak_session_code}}" }, { "key": "execution", "value": "{{keycloak_execution}}" }, { "key": "client_id", "value": "{{KEYCLOAK_STANDARD_CLIENT}}" }, { "key": "tab_id", "value": "{{keycloak_tab_id}}" } ] } }, "response": [] }, { "name": "Token exchange", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", "", "if(pm.response.status === 'OK') {", " var jsonData = JSON.parse(responseBody);", " pm.globals.set(\"keycloak_user_access_token\", jsonData.access_token);", " pm.globals.set(\"keycloak_user_refresh_token\", jsonData.refresh_token);", " pm.globals.set(\"keycloak_user_id_token\", jsonData.id_token);", "}" ], "type": "text/javascript" } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "name": "Content-Type", "value": "application/x-www-form-urlencoded", "type": "text" } ], "body": { "mode": "urlencoded", "urlencoded": [ { "key": "client_id", "value": "{{KEYCLOAK_STANDARD_CLIENT}}", "type": "text" }, { "key": "client_secret", "value": "{{KEYCLOAK_STANDARD_CLIENT_SECRET}}", "type": "text" }, { "key": "grant_type", "value": "authorization_code", "type": "text" }, { "key": "state", "value": "12345678", "type": "text" }, { "key": "code", "value": "{{keycloak_code}}", "type": "text" }, { "key": "session_code", "value": "{{keycloak_session_code}}", "type": "text" }, { "key": "redirect_uri", "value": "{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}", "type": "text" } ] }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "token" ] }, "description": "Obtain SAT (service account token)" }, "response": [] } ] }, { "name": "OA2 ROPC Grant", "item": [ { "name": "Token exchange", "event": [ { "listen": "prerequest", "script": { "exec": [ "postman.setNextRequest()" ], "type": "text/javascript" } }, { "listen": "test", "script": { "exec": [ "if(pm.response.status === 'OK') {", " var jsonData = JSON.parse(responseBody);", " pm.globals.set(\"keycloak_user_access_token\", jsonData.access_token);", " pm.globals.set(\"keycloak_user_refresh_token\", jsonData.refresh_token);", " pm.globals.set(\"keycloak_user_id_token\", \"\");", "", " console.log(\"Done password flow\")", "}" ], "type": "text/javascript" } } ], "request": { "auth": { "type": "noauth" }, "method": "POST", "header": [ { "key": "Content-Type", "value": "application/x-www-form-urlencoded" } ], "body": { "mode": "urlencoded", "urlencoded": [ { "key": "client_id", "value": "{{KEYCLOAK_DA_CLIENT}}", "type": "text" }, { "key": "username", "value": "{{KEYCLOAK_TEST_USER_EMAIL}}", "type": "text" }, { "key": "password", "value": "{{KEYCLOAK_TEST_PASS}}", "type": "text" }, { "key": "grant_type", "value": "password", "type": "text" }, { "key": "client_secret", "value": "{{KEYCLOAK_DA_CLIENT_SECRET}}", "type": "text" } ] }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "token" ] }, "description": "Obtain UAT = user access token from a user in realm" }, "response": [] } ] }, { "name": "OA2 Client Credentials Grant", "item": [ { "name": "Token exchange", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", "", "if(pm.response.status === 'OK') {", " var jsonData = JSON.parse(responseBody);", " pm.globals.set(\"keycloak_user_access_token\", jsonData.access_token);", "}" ], "type": "text/javascript" } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "name": "Content-Type", "value": "application/x-www-form-urlencoded", "type": "text" } ], "body": { "mode": "urlencoded", "urlencoded": [ { "key": "client_id", "value": "{{KEYCLOAK_SERVICE_CLIENT}}", "type": "text" }, { "key": "client_secret", "value": "{{KEYCLOAK_SERVICE_CLIENT_SECRET}}", "type": "text" }, { "key": "grant_type", "value": "client_credentials", "type": "text" } ] }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "token" ] }, "description": "Obtain SAT (service account token)" }, "response": [] } ] }, { "name": "OA2 Refresh token", "item": [ { "name": "Token exchange", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});", "", "if(pm.response.status === 'OK') {", " var jsonData = JSON.parse(responseBody);", " pm.globals.set(\"keycloak_user_access_token\", jsonData.access_token);", " pm.globals.set(\"keycloak_user_refresh_token\", jsonData.refresh_token);", " pm.globals.set(\"keycloak_user_id_token\", jsonData.id_token);", "", " console.log(\"Done refresh\")", "}" ], "type": "text/javascript" } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "name": "Content-Type", "value": "application/x-www-form-urlencoded", "type": "text" } ], "body": { "mode": "urlencoded", "urlencoded": [ { "key": "client_id", "value": "{{KEYCLOAK_STANDARD_CLIENT}}", "type": "text" }, { "key": "client_secret", "value": "{{KEYCLOAK_STANDARD_CLIENT_SECRET}}", "type": "text" }, { "key": "grant_type", "value": "refresh_token", "type": "text" }, { "key": "state", "value": "12345678", "type": "text" }, { "key": "refresh_token", "value": "{{keycloak_user_refresh_token}}", "type": "text" }, { "key": "session_code", "value": "{{keycloak_session_code}}", "type": "text" }, { "key": "redirect_uri", "value": "{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}", "type": "text" } ] }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "token" ] }, "description": "Obtain SAT (service account token)" }, "response": [] } ] }, { "name": "OA2/OIDC Util", "item": [ { "name": "OIDC User Info", "request": { "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{keycloak_user_access_token}}", "type": "string" } ] }, "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/userinfo", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "userinfo" ] } }, "response": [] }, { "name": "OIDC Introspect", "request": { "auth": { "type": "basic", "basic": [ { "key": "password", "value": "{{KEYCLOAK_STANDARD_CLIENT_SECRET}}", "type": "string" }, { "key": "username", "value": "{{KEYCLOAK_STANDARD_CLIENT}}", "type": "string" } ] }, "method": "POST", "header": [], "body": { "mode": "urlencoded", "urlencoded": [ { "key": "token", "value": "{{keycloak_user_access_token}}", "type": "text" }, { "key": "token", "value": "{{keycloak_user_id_token}}", "type": "text", "disabled": true }, { "key": "token", "value": "{{keycloak_user_refresh_token}}", "type": "text", "disabled": true } ] }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/token/introspect", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "token", "introspect" ] } }, "response": [] }, { "name": "OIDC Logout", "request": { "auth": { "type": "noauth" }, "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/logout", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "logout" ] } }, "response": [] }, { "name": "OIDC Logout IDToken", "request": { "auth": { "type": "noauth" }, "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/logout?id_token_hint={{keycloak_user_id_token}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "openid-connect", "logout" ], "query": [ { "key": "id_token_hint", "value": "{{keycloak_user_id_token}}" } ] } }, "response": [] }, { "name": "OIDC Discovery Document", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});" ], "type": "text/javascript" } } ], "request": { "auth": { "type": "noauth" }, "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/.well-known/openid-configuration", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", ".well-known", "openid-configuration" ] } }, "response": [] }, { "name": "OIDC Certs", "request": { "auth": { "type": "noauth" }, "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/openid-connect/certs", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "auth", "realms", "acme-demo", "protocol", "openid-connect", "certs" ] } }, "response": [] } ] }, { "name": "SAML", "item": [ { "name": "SAML Metadata Descriptor", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(200);", "});" ], "type": "text/javascript" } } ], "request": { "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/protocol/saml/descriptor", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "protocol", "saml", "descriptor" ] } }, "response": [] } ] } ] }, { "name": "Admin API", "item": [ { "name": "Realm", "item": [ { "name": "GET realm", "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}" ] }, "description": "Get realm keys " }, "response": [] }, { "name": "GET realm keys", "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/keys", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "keys" ] }, "description": "Get realm keys " }, "response": [] } ] }, { "name": "Groups", "item": [ { "name": "Search groups", "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/groups?first=0&max=20&search=test2&briefRepresentation=false", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "groups" ], "query": [ { "key": "first", "value": "0" }, { "key": "max", "value": "20" }, { "key": "search", "value": "test2" }, { "key": "briefRepresentation", "value": "false" } ] }, "description": "Get groups" }, "response": [] } ] }, { "name": "Attack Detection", "item": [ { "name": "Brute force users", "request": { "method": "DELETE", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/attack-detection/brute-force/users", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "attack-detection", "brute-force", "users" ] }, "description": "Clear any user login failures for all users This can release temporary disabled users" }, "response": [] }, { "name": "Brute force specific user", "request": { "method": "DELETE", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/attack-detection/brute-force/users/{{keycloak_user_id}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "attack-detection", "brute-force", "users", "{{keycloak_user_id}}" ] }, "description": "Clear any user login failures for the user This can release temporary disabled user\n* Add userId" }, "response": [] }, { "name": "Brute force specific user", "request": { "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/attack-detection/brute-force/users/{{keycloak_user_id}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "attack-detection", "brute-force", "users", "{{keycloak_user_id}}" ] }, "description": "Get status of a username in brute force detection. \n* Add userId" }, "response": [] } ] }, { "name": "Users", "item": [ { "name": "Roles", "item": [] }, { "name": "UPDATE GLOBAL VARIABLE USER ID", "event": [ { "listen": "test", "script": { "exec": [ "var jsonData = pm.response.json()[0];", "pm.globals.set(\"keycloak_user_id\",jsonData.id);" ], "type": "text/javascript" } } ], "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users?username={{KEYCLOAK_TEST_USER}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "users" ], "query": [ { "key": "briefRepresentation", "value": "", "disabled": true }, { "key": "email", "value": "", "disabled": true }, { "key": "first", "value": "", "disabled": true }, { "key": "firstName", "value": "", "disabled": true }, { "key": "lastName", "value": "", "disabled": true }, { "key": "max", "value": "", "disabled": true }, { "key": "search", "value": "", "disabled": true }, { "key": "username", "value": "{{KEYCLOAK_TEST_USER}}" } ] }, "description": "Get users Returns a list of users, filtered according to query parameters" }, "response": [] }, { "name": "Get user by id", "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users/{{keycloak_user_id}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "users", "{{keycloak_user_id}}" ] }, "description": "Get representation of the user" }, "response": [] }, { "name": "Get users", "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "users" ], "query": [ { "key": "briefRepresentation", "value": "", "disabled": true }, { "key": "email", "value": "", "disabled": true }, { "key": "first", "value": "", "disabled": true }, { "key": "firstName", "value": "", "disabled": true }, { "key": "lastName", "value": "", "disabled": true }, { "key": "max", "value": "", "disabled": true }, { "key": "search", "value": "", "disabled": true }, { "key": "username", "value": "", "disabled": true } ] }, "description": "Get users Returns a list of users, filtered according to query parameters" }, "response": [] }, { "name": "Create user", "event": [ { "listen": "test", "script": { "exec": [ "const headers = pm.response.headers", "const url = require('url');", "", "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", "});", "", "const locationHeader = headers.get(\"Location\");", "const mzUrl = url.parse(locationHeader);", "", "const userId = mzUrl.path.substring(mzUrl.path.lastIndexOf('/') + 1);", "postman.setEnvironmentVariable(\"keycloak_user_id\",userId);" ], "type": "text/javascript" } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"createdTimestamp\": 1588880747548,\n \"username\": \"{{KEYCLOAK_TEST_USER}}\",\n \"enabled\": true,\n \"totp\": false,\n \"emailVerified\": true,\n \"firstName\": \"Us\",\n \"lastName\": \"Er\",\n \"email\": \"{{KEYCLOAK_TEST_USER_EMAIL}}\",\n \"disableableCredentialTypes\": [],\n \"requiredActions\": [],\n \"notBefore\": 0,\n \"access\": {\n \"manageGroupMembership\": true,\n \"view\": true,\n \"mapRoles\": true,\n \"impersonate\": true,\n \"manage\": true\n }\n }" }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "users" ] }, "description": "Create a new user Username must be unique." }, "response": [] }, { "name": "Set password", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\"type\":\"password\",\"value\":\"{{KEYCLOAK_TEST_PASS}}\",\"temporary\":false}" }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users/{{keycloak_user_id}}/reset-password", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "users", "{{keycloak_user_id}}", "reset-password" ] }, "description": "Set up a new password for the user.\n" }, "response": [] }, { "name": "Delete user", "event": [ { "listen": "test", "script": { "exec": [ "pm.test(\"Status code is 200\", function () {", " pm.response.to.have.status(204);", "});" ], "type": "text/javascript" } } ], "request": { "method": "DELETE", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/users/{{keycloak_user_id}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "users", "{{keycloak_user_id}}" ] }, "description": "Delete the user" }, "response": [] } ] }, { "name": "Clients", "item": [ { "name": "Roles", "item": [ { "name": "Get roles of client by id", "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}/roles", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients", "{{KEYCLOAK_STANDARD_CLIENT_ID}}", "roles" ] }, "description": "Get representation of the client" }, "response": [] } ] }, { "name": "Secret", "item": [ { "name": "Get secret by client.id", "event": [ { "listen": "test", "script": { "exec": [ "var jsonData = pm.response.json();", "pm.globals.set(\"KEYCLOAK_STANDARD_CLIENT_SECRET\", jsonData.value);" ], "type": "text/javascript" } } ], "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}/client-secret", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients", "{{KEYCLOAK_STANDARD_CLIENT_ID}}", "client-secret" ] }, "description": "Get representation of the client" }, "response": [] }, { "name": "Regenerate secret", "event": [ { "listen": "test", "script": { "exec": [ "var jsonData = pm.response.json();", "pm.globals.set(\"keycloak_test_client_secret\", jsonData.value);" ], "type": "text/javascript" } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "body": { "mode": "raw", "raw": "", "options": { "raw": { "language": "json" } } }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}/client-secret", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients", "{{KEYCLOAK_STANDARD_CLIENT_ID}}", "client-secret" ] }, "description": "Get representation of the client" }, "response": [] } ] }, { "name": "Get clients", "request": { "method": "GET", "header": [ { "key": "Content-Type", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients" ] }, "description": "Get clients belonging to the realm Returns a list of clients belonging to the realm" }, "response": [] }, { "name": "Get client by id", "event": [ { "listen": "test", "script": { "exec": [ "var jsonData = pm.response.json();", "pm.globals.set(\"keycloak_test_client_redirect_uri\", jsonData.rootUrl+jsonData.redirectUris[0]);" ], "type": "text/javascript" } } ], "request": { "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients", "{{KEYCLOAK_STANDARD_CLIENT_ID}}" ] }, "description": "Get representation of the client" }, "response": [] }, { "name": "Create a new client", "event": [ { "listen": "test", "script": { "exec": [ "const headers = pm.response.headers", "const url = require('url');", "", "pm.test(\"Status code is 201\", function () {", " pm.response.to.have.status(201);", "});", "", "const locationHeader = headers.get(\"Location\");", "const mzUrl = url.parse(locationHeader);", "", "const userId = mzUrl.path.substring(mzUrl.path.lastIndexOf('/') + 1);", "postman.setEnvironmentVariable(\"keycloak_test_client_id\",userId);" ], "type": "text/javascript" } } ], "request": { "method": "POST", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"clientId\": \"{{KEYCLOAK_STANDARD_CLIENT}}\",\n \"surrogateAuthRequired\": false,\n \"enabled\": true,\n \"alwaysDisplayInConsole\": false,\n \"clientAuthenticatorType\": \"client-secret\",\n \"redirectUris\": [\n \"{{KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI}}\"\n ],\n \"webOrigins\": [],\n \"notBefore\": 0,\n \"bearerOnly\": false,\n \"consentRequired\": false,\n \"standardFlowEnabled\": true,\n \"implicitFlowEnabled\": false,\n \"directAccessGrantsEnabled\": true,\n \"serviceAccountsEnabled\": false,\n \"publicClient\": false,\n \"frontchannelLogout\": false,\n \"protocol\": \"openid-connect\",\n \"attributes\": {\n \"saml.assertion.signature\": \"false\",\n \"saml.force.post.binding\": \"false\",\n \"saml.multivalued.roles\": \"false\",\n \"saml.encrypt\": \"false\",\n \"oauth2.device.authorization.grant.enabled\": \"false\",\n \"backchannel.logout.revoke.offline.tokens\": \"false\",\n \"saml.server.signature\": \"false\",\n \"saml.server.signature.keyinfo.ext\": \"false\",\n \"use.refresh.tokens\": \"true\",\n \"exclude.session.state.from.auth.response\": \"false\",\n \"oidc.ciba.grant.enabled\": \"false\",\n \"saml.artifact.binding\": \"false\",\n \"backchannel.logout.session.required\": \"true\",\n \"client_credentials.use_refresh_token\": \"false\",\n \"saml_force_name_id_format\": \"false\",\n \"saml.client.signature\": \"false\",\n \"tls.client.certificate.bound.access.tokens\": \"false\",\n \"saml.authnstatement\": \"false\",\n \"display.on.consent.screen\": \"false\",\n \"saml.onetimeuse.condition\": \"false\"\n },\n \"authenticationFlowBindingOverrides\": {},\n \"fullScopeAllowed\": true,\n \"nodeReRegistrationTimeout\": -1,\n \"defaultClientScopes\": [\n \"web-origins\",\n \"profile\",\n \"roles\",\n \"email\"\n ],\n \"optionalClientScopes\": [\n \"address\",\n \"phone\",\n \"offline_access\",\n \"microprofile-jwt\"\n ],\n \"access\": {\n \"view\": true,\n \"configure\": true,\n \"manage\": true\n }\n}" }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients" ] }, "description": "Create a new client Client’s client_id must be unique!" }, "response": [] }, { "name": "Update client", "request": { "method": "PUT", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json" } ], "body": { "mode": "raw", "raw": "{\n \"enabled\": true,\n \"secret\": \"{{KEYCLOAK_STANDARD_CLIENT_SECRET}}\",\n \"bearerOnly\": false,\n \"consentRequired\": false,\n \"standardFlowEnabled\": true,\n \"implicitFlowEnabled\": false,\n \"directAccessGrantsEnabled\": false,\n \"serviceAccountsEnabled\": false,\n \"publicClient\": false\n}" }, "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients", "{{KEYCLOAK_STANDARD_CLIENT_ID}}" ] }, "description": "Create a new client Client’s client_id must be unique!" }, "response": [] }, { "name": "Delete client", "request": { "method": "DELETE", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/admin/realms/{{KEYCLOAK_REALM}}/clients/{{KEYCLOAK_STANDARD_CLIENT_ID}}", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "admin", "realms", "{{KEYCLOAK_REALM}}", "clients", "{{KEYCLOAK_STANDARD_CLIENT_ID}}" ] }, "description": "Get representation of the client" }, "response": [] } ] } ], "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{KEYCLOAK_ADMIN_CLI_USER_TOKEN}}", "type": "string" } ] }, "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "const keycloak_url = pm.environment.get(\"KEYCLOAK_SERVER_URL\");", "const realm = pm.globals.get(\"KEYCLOAK_ADMIN_CLI_REALM\");", "console.log(\"Using keycloak url: \" + keycloak_url + \" and realm: \" + realm);", "const postRequest = {", " url: keycloak_url + '/realms/' + realm + '/protocol/openid-connect/token',", " method: 'POST',", " header: {", " 'Content-Type': 'application/x-www-form-urlencoded',", " },", " body: {", " mode: 'urlencoded',", " urlencoded: [", " {key: \"client_id\", value: \"admin-cli\", disabled: false},", " {key: \"username\", value: pm.globals.get(\"KEYCLOAK_ADMIN_CLI_USER\"), disabled: false},", " {key: \"password\", value: pm.globals.get(\"KEYCLOAK_ADMIN_CLI_PASS\"), disabled: false},", " {key: \"grant_type\", value: \"password\", disabled: false},", " ]", " }", "};", "pm.sendRequest(postRequest, (error, response) => {", "", " console.log(error ? error : response.text());", " var jsonData = JSON.parse(response.text());", " pm.globals.set(\"KEYCLOAK_ADMIN_CLI_USER_TOKEN\", jsonData.access_token);", "});", "" ] } }, { "listen": "test", "script": { "type": "text/javascript", "exec": [ "" ] } } ] }, { "name": "Account API", "item": [ { "name": "/", "protocolProfileBehavior": { "disabledSystemHeaders": {} }, "request": { "method": "GET", "header": [ { "key": "Accept", "value": "application/json", "type": "text", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/account", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "account" ] } }, "response": [] }, { "name": "/", "request": { "method": "POST", "header": [ { "key": "Accept", "value": "application/json", "type": "text" } ], "body": { "mode": "raw", "raw": "{\n \"username\": \"test@local.test\",\n \"firstName\": \"Us\",\n \"lastName\": \"Er\",\n \"email\": \"test@local.test\",\n \"emailVerified\": true,\n \"attributes\": {}\n}", "options": { "raw": { "language": "json" } } }, "url": { "raw": "http://localhost:8080/auth/realms/{{KEYCLOAK_REALM}}/account", "protocol": "http", "host": [ "localhost" ], "port": "8080", "path": [ "auth", "realms", "{{KEYCLOAK_REALM}}", "account" ] } }, "response": [] }, { "name": "Identity", "protocolProfileBehavior": { "disabledSystemHeaders": {} }, "request": { "method": "GET", "header": [ { "key": "Accept", "value": "application/json", "type": "text", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/account/identity", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "account", "identity" ] } }, "response": [] } ], "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{keycloak_user_access_token}}", "type": "string" } ] }, "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "" ] } }, { "listen": "test", "script": { "type": "text/javascript", "exec": [ "" ] } } ] }, { "name": "Extensions", "item": [ { "name": "Custom ping", "request": { "method": "GET", "header": [], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/custom-resources/ping", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "custom-resources", "ping" ] } }, "response": [] }, { "name": "Custom search groups", "request": { "auth": { "type": "bearer", "bearer": [ { "key": "token", "value": "{{KEYCLOAK_ADMIN_CLI_USER_TOKEN}}", "type": "string" } ] }, "method": "GET", "header": [ { "key": "Content-Type", "name": "Content-Type", "type": "text", "value": "application/json", "disabled": true } ], "url": { "raw": "{{KEYCLOAK_SERVER_URL}}/realms/{{KEYCLOAK_REALM}}/custom-resources/mygroups?first=0&max=20&search=test2&briefRepresentation=true", "host": [ "{{KEYCLOAK_SERVER_URL}}" ], "path": [ "realms", "{{KEYCLOAK_REALM}}", "custom-resources", "mygroups" ], "query": [ { "key": "first", "value": "0" }, { "key": "max", "value": "20" }, { "key": "search", "value": "test2" }, { "key": "briefRepresentation", "value": "true" } ] }, "description": "Get groups" }, "response": [] } ] } ], "auth": { "type": "oauth2", "oauth2": [ { "key": "addTokenTo", "value": "header", "type": "string" } ] }, "event": [ { "listen": "prerequest", "script": { "type": "text/javascript", "exec": [ "" ] } }, { "listen": "test", "script": { "type": "text/javascript", "exec": [ "" ] } } ] } ================================================ FILE: tools/postman/acme.postman_environment_http.json ================================================ { "id": "7d0f3a2d-48e7-4930-ba20-a5b1c9d4bf3c", "name": "acme-http", "values": [ { "key": "KEYCLOAK_SERVER_URL", "value": "http://localhost:8080/auth", "enabled": true } ], "_postman_variable_scope": "environment", "_postman_exported_at": "2021-06-18T14:48:53.533Z", "_postman_exported_using": "Postman/8.5.1" } ================================================ FILE: tools/postman/acme.postman_environment_https.json ================================================ { "id": "04e7f671-1872-4bec-9eab-9525d38e4fc3", "name": "acme-https", "values": [ { "key": "KEYCLOAK_SERVER_URL", "value": "https://id.acme.test:8443/auth", "enabled": true } ], "_postman_variable_scope": "environment", "_postman_exported_at": "2021-06-18T14:49:20.604Z", "_postman_exported_using": "Postman/8.5.1" } ================================================ FILE: tools/postman/acme.postman_globals.json ================================================ { "id": "146e52f0-fd32-4814-8e58-8a3c0f4d5eb7", "values": [ { "key": "KEYCLOAK_REALM", "value": "acme-demo", "enabled": true }, { "key": "KEYCLOAK_ADMIN_CLI_REALM", "value": "master", "enabled": true }, { "key": "KEYCLOAK_ADMIN_CLI_USER_TOKEN", "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVR1NVNjBnalRSdmk2S2RRcGFLZEZvTXRtd1RmSzU2MkhMVEpXVDdua1BVIn0.eyJleHAiOjE2MjM5NDA5NjMsImlhdCI6MTYyMzk0MDkwMywianRpIjoiOTUwYTg4ZTAtMWZiMS00OTYxLWI5MzItMWJjNjI5NTQ4NzU0IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsInN1YiI6Ijc0MTVmNTE3LWQzMDUtNDU1ZS05ODRjLTRkOTc5MzQ4N2E4NyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFkbWluLWNsaSIsInNlc3Npb25fc3RhdGUiOiJmMzI4OTdmYS0yOTJjLTRkNGMtOTg5Yy00OTBjMzAyZmI5MjEiLCJhY3IiOiIxIiwic2NvcGUiOiJwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiJ9.BE7ko7qm2JLX1M8dIRJpAr2fwpXIl7jzJALzi83IjGB6AoFMZ8iPDbFwDAw3t4Q22-GQruQh5GrWVo6yqNdnuOxUicMgiiBih58Irs0eSQTNwBp_S_UXjxJUpD5TAEXOKU4T9_LRUrUAoHaKHdWFvF_CopZUEawrPGzX0Azbc_RMISTYkbZ61mvoMA030yZ1z4orfH_G3ohbZyXzL6NrKtHbu0gd4DdnDBwuXO13DWwmI4fMkiMzdrxc2eHY4_yQTxOzH_j-xIQGno8Z9ZLEzmduiLYelHQfIr1XSZ_tnp1fG9PjeY75jkDbrzZoq0PUTiUzkPp2weqqH7lUeftAiA", "enabled": true }, { "key": "KEYCLOAK_ADMIN_CLI_USER", "value": "admin", "enabled": true }, { "key": "KEYCLOAK_ADMIN_CLI_PASS", "value": "admin", "enabled": true }, { "key": "KEYCLOAK_STANDARD_CLIENT", "value": "acme-standard-client", "enabled": true }, { "key": "KEYCLOAK_STANDARD_CLIENT_ID", "value": "acme-standard-client-1", "enabled": true }, { "key": "KEYCLOAK_STANDARD_CLIENT_SECRET", "value": "acme-standard-client-1-secret", "enabled": true }, { "key": "KEYCLOAK_STANDARD_CLIENT_REDIRECT_URI", "value": "http://localhost/acme-standard-client/login*", "enabled": true }, { "key": "KEYCLOAK_SERVICE_CLIENT", "value": "acme-service-client", "enabled": true }, { "key": "KEYCLOAK_SERVICE_CLIENT_ID", "value": "acme-service-client-1-id", "enabled": true }, { "key": "KEYCLOAK_SERVICE_CLIENT_SECRET", "value": "acme-service-client-1-secret", "enabled": true }, { "key": "KEYCLOAK_SERVICE_CLIENT_REDIRECT_URI", "value": "http://localhost/acme-service-client/login*", "enabled": true }, { "key": "KEYCLOAK_DA_CLIENT", "value": "acme-direct-access-client", "enabled": true }, { "key": "KEYCLOAK_DA_CLIENT_ID", "value": "acme-direct-access-client-1", "enabled": true }, { "key": "KEYCLOAK_DA_CLIENT_SECRET", "value": "acme-direct-access-client-1-secret", "enabled": true }, { "key": "KEYCLOAK_DA_CLIENT_REDIRECT_URI", "value": "http://localhost/acme-direct-access-client/login*", "enabled": true }, { "key": "keycloak_execution", "value": "", "enabled": true }, { "key": "keycloak_session_code", "value": "", "enabled": true }, { "key": "keycloak_tab_id", "value": "", "enabled": true }, { "key": "keycloak_code", "value": "", "enabled": true }, { "key": "keycloak_session_state", "value": "", "enabled": true }, { "key": "KEYCLOAK_TEST_USER", "value": "tester", "enabled": true }, { "key": "KEYCLOAK_TEST_USER_EMAIL", "value": "test@local.test", "enabled": true }, { "key": "KEYCLOAK_TEST_PASS", "value": "test", "enabled": true }, { "key": "keycloak_user_id", "value": "a9397148-d947-491a-8647-c9393c4f4851", "enabled": true }, { "key": "keycloak_user_id_token", "value": "", "enabled": true }, { "key": "keycloak_user_access_token", "value": "", "enabled": true }, { "key": "keycloak_user_refresh_token", "value": "", "enabled": true }, { "key": "keycloak_test_client_redirect_uri", "value": "", "enabled": true } ], "name": "My Workspace Globals", "_postman_variable_scope": "globals", "_postman_exported_at": "2021-06-18T14:47:51.978Z", "_postman_exported_using": "Postman/8.5.1" } ================================================ FILE: tools/postman/readme.md ================================================ Work with Postman(tm) --- This demonstrates an example setup for [Postman](https://www.postman.com/) that works with the demo environment that is described by [config/stage/demo/acme-demo.yaml](config/stage/demo/acme-demo.yaml). # Setup 1. Install/Open Postman 2. Import folder tools/postman or individual files, e.g. from `Scratch Pad` 1. Import [globals](acme.postman_globals.json) 2. Import [http-environment for http](acme.postman_environment_http.json) 3. Import [https-environment for https](acme.postman_environment_https.json) 4. Import [collection](acme.postman_collection.json) 3. Get familiar with the [configuration](config/stage/dev/realms/acme-demo.yaml). All data used in the collections can be found here. 4. From the imported collection named `Acme Keycloak` run `UPDATE GLOBAL VARIABLE USER ID` to work with the user_id based endpoints. ================================================ FILE: tools/session-generator/.gitignore ================================================ HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ !**/src/test/**/target/ ### STS ### .apt_generated .classpath .factorypath .project .settings .springBeans .sts4-cache ### IntelliJ IDEA ### .idea *.iws *.iml *.ipr ### NetBeans ### /nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ !**/src/main/**/build/ !**/src/test/**/build/ ### VS Code ### .vscode/ ================================================ FILE: tools/session-generator/.mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2007-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { private static final String WRAPPER_VERSION = "0.5.6"; /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if (mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if (mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if (!outputFile.getParentFile().exists()) { if (!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { String username = System.getenv("MVNW_USERNAME"); char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); } URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: tools/session-generator/.mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar ================================================ FILE: tools/session-generator/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi if [ -n "$MVNW_REPOURL" ]; then jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" else jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" fi while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if $cygwin; then wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` fi if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then wget "$jarUrl" -O "$wrapperJarPath" else wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" fi elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then curl -o "$wrapperJarPath" "$jarUrl" -f else curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then javaClass=`cygpath --path --windows "$javaClass"` fi if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: tools/session-generator/mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( if "%MVNW_VERBOSE%" == "true" ( echo Found %WRAPPER_JAR% ) ) else ( if not "%MVNW_REPOURL%" == "" ( SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% ) powershell -Command "&{"^ "$webclient = new-object System.Net.WebClient;"^ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% ) ) @REM End of extension @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: tools/session-generator/pom.xml ================================================ 4.0.0 org.springframework.boot spring-boot-starter-parent 3.0.5 com.github.thomasdarimont.keycloak session-generator 0.0.1-SNAPSHOT session-generator session-generator 20 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin org.projectlombok lombok org.apache.maven.plugins maven-compiler-plugin --enable-preview ================================================ FILE: tools/session-generator/src/main/java/com/github/thomasdarimont/keycloak/tools/sessiongenerator/SessionGeneratorApplication.java ================================================ package com.github.thomasdarimont.keycloak.tools.sessiongenerator; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @SpringBootApplication public class SessionGeneratorApplication { public static void main(String[] args) { new SpringApplicationBuilder(SessionGeneratorApplication.class).web(WebApplicationType.NONE).run(args); } @Bean CommandLineRunner clr() { return args -> { String baseUrl = "https://id.acme.test/auth"; var realmName = "acme-offline-test"; var issuerUri = baseUrl + "/realms/" + realmName; var adminUri = baseUrl + "/admin/realms/" + realmName; // deleteOfflineSession(issuerUri, adminUri, "897768ae-be97-3c8f-9e47-07e006360799", "app-mobile"); generateOfflineSessions(issuerUri, 100_000); }; } private boolean deleteOfflineSession(String issuerUri, String adminUri, String userUuid, String clientId) { var userUri = adminUri + "/users/" + userUuid; var consentUri = userUri + "/consents/" + clientId; var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBearerAuth(getAdminSvcAccessToken(issuerUri)); var request = new HttpEntity<>(headers); try { var deleteConsentResponse = rt.exchange(consentUri, HttpMethod.DELETE, request, Map.class); System.out.println(deleteConsentResponse.getStatusCode()); return deleteConsentResponse.getStatusCode().is2xxSuccessful(); } catch (HttpClientErrorException hcee) { System.out.printf("Could not delete client session %s%n", hcee.getMessage()); return false; } } private static String getAdminSvcAccessToken(String issuerUri) { var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", "acme-admin-svc"); requestBody.add("client_secret", "jOOgfhjFT2OWpimKUzRCj0or5FsUEqaK"); requestBody.add("grant_type", "client_credentials"); requestBody.add("scope", "email"); var request = new HttpEntity<>(requestBody, headers); var tokenUri = issuerUri + "/protocol/openid-connect/token"; var accessTokenResponse = rt.exchange(tokenUri, HttpMethod.POST, request, Map.class); var accessTokenResponseBody = accessTokenResponse.getBody(); return (String) accessTokenResponseBody.get("access_token"); } private static void generateOfflineSessions(String issuerUri, int sessions) { var rt = new RestTemplate(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); var tokenUri = issuerUri + "/protocol/openid-connect/token"; int maxConcurrentRequests = 180; var sessionsCreated = new AtomicInteger(); var sessionsFailed = new AtomicInteger(); var sessionsFile = Paths.get("data/" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").format(LocalDateTime.now()) + ".sessions"); var generatedTokens = new ConcurrentLinkedDeque>(); try (var offlineTokenWriter = new PrintWriter(Files.newBufferedWriter(sessionsFile, StandardOpenOption.CREATE_NEW))) { Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> { log.info("Sessions created: {} failed: {}", sessionsCreated.get(), sessionsFailed.get()); if (sessionsCreated.get() + sessionsFailed.get() >= sessions) { System.exit(0); return; } int tokenCount = 0; Map.Entry entry; while ((entry = generatedTokens.poll()) != null) { Integer idx = entry.getKey(); String refreshToken = entry.getValue(); offlineTokenWriter.print(idx); offlineTokenWriter.print('='); offlineTokenWriter.println(refreshToken); tokenCount++; } log.info("Wrote {} tokens to disk.", tokenCount); }, 1, 3, TimeUnit.SECONDS); var results = new ArrayList>(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var semaphore = new Semaphore(maxConcurrentRequests); for (var i = 0; i < sessions; i++) { var idx = i; results.add(executor.submit(() -> { while (true) { try { semaphore.acquire(); try { var requestBody = new LinkedMultiValueMap(); requestBody.add("client_id", "app-mobile"); requestBody.add("grant_type", "password"); requestBody.add("username", "user" + idx); requestBody.add("password", "test"); requestBody.add("scope", "openid profile offline_access"); HttpEntity request = new HttpEntity<>(requestBody, headers); var response = rt.exchange(tokenUri, HttpMethod.POST, request, Map.class); if (response.getStatusCode().value() == 200) { var refeshToken = (String) response.getBody().get("refresh_token"); // System.out.println("Session created " + i); sessionsCreated.incrementAndGet(); generatedTokens.add(new AbstractMap.SimpleImmutableEntry<>(idx, refeshToken)); } else { // System.err.println("Failed to create session status=" + response.getStatusCodeValue()); sessionsFailed.incrementAndGet(); } return; } finally { semaphore.release(); } } catch (Exception ex) { log.warn("Error during session creation waiting and retry... " + idx); try { TimeUnit.MILLISECONDS.sleep(500 + ThreadLocalRandom.current().nextInt(500)); } catch (InterruptedException e) { throw new RuntimeException(e); } } } })); } } results.forEach(f -> { try { f.get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } }); System.out.printf("Generation of %s completed.%n", sessions); } catch (IOException e) { throw new RuntimeException(e); } } } ================================================ FILE: tools/session-generator/src/main/resources/application.properties ================================================ ================================================ FILE: tools/session-generator/src/test/java/com/github/thomasdarimont/keycloak/tools/sessiongenerator/SessionGeneratorApplicationTests.java ================================================ package com.github.thomasdarimont.keycloak.tools.sessiongenerator; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SessionGeneratorApplicationTests { @Test void contextLoads() { } } ================================================ FILE: tools/tcpdump/Dockerfile ================================================ FROM alpine:3.18.2 RUN apk add tcpdump CMD tcpdump -i eth0 ================================================ FILE: tools/tcpdump/readme.md ================================================ # tcpdump [tcpdump](https://www.tcpdump.org/manpages/tcpdump.1.html) dumbs network traffic. It is useful for inspecting the traffic between Keycloak nodes. In our setup we want to investigate Keycloak traffic in various situations. Especially when it comes to clustering this is helpful. Kudos to: 1. https://rmoff.net/2019/11/29/using-tcpdump-with-docker/ 2. https://xxradar.medium.com/how-to-tcpdump-effectively-in-docker-2ed0a09b5406 ## build container ``` docker build -t thomasdarimont/tcpdump . ``` ## run examples Simply see what keycloak does with plain http: ``` docker run --tty --net=container:dev-acme-keycloak-1 thomasdarimont/tcpdump tcpdump -N -A 'port 8080' ``` Pipe https traffic directly into wireshark: ``` docker run --net=container:dev-acme-keycloak-1 thomasdarimont/tcpdump tcpdump -N -A 'port 8443' -U -s 65535 -w - 2>/dev/null | wireshark -k -i - ``` # Misc There are many ways to decrypt https/TLS traffic, one helpful article is this: https://www.alphr.com/wireshark-read-https-traffic/