diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index e3b508bf4..3ad5c4869 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -25,7 +25,7 @@ jobs: distribution: "temurin" cache: gradle - name: Start PostgreSQL - run: docker compose up --detach postgres14 redis + run: docker compose -f docker-compose-services.yaml up --detach - name: Start backend # GitHub actions automatically keep the process running and output is streamed to pipeline logs run: ./gradlew bootRun & working-directory: ./backend diff --git a/.github/workflows/push-docker-image-job.yml b/.github/workflows/push-docker-image-job.yml index 85d657cfd..c9b55592d 100644 --- a/.github/workflows/push-docker-image-job.yml +++ b/.github/workflows/push-docker-image-job.yml @@ -39,12 +39,12 @@ jobs: uses: sigstore/cosign-installer@e11c0892438d2c0a48e49dee376e4883f10f2e59 - name: Sign the published Docker image run: cosign sign --yes ${{ inputs.container-registry }}/${{ inputs.container-image-name }}:${{ inputs.container-image-version }} -# - name: Download cosign vulnerability scan record -# uses: actions/download-artifact@v4 -# with: -# name: "vuln.json" -# - name: Attest vulnerability scan -# run: cosign attest --yes --replace --predicate vuln.json --type vuln ${{ inputs.container-registry }}/${{ inputs.container-image-name }}:${{ inputs.container-image-version }} + # - name: Download cosign vulnerability scan record + # uses: actions/download-artifact@v4 + # with: + # name: "vuln.json" + # - name: Attest vulnerability scan + # run: cosign attest --yes --replace --predicate vuln.json --type vuln ${{ inputs.container-registry }}/${{ inputs.container-image-name }}:${{ inputs.container-image-version }} - name: Send status to Slack uses: digitalservicebund/notify-on-failure-gha@814d0c4b2ad6a3443e89c991f8657b10126510bf # v1.5.0 if: ${{ failure() }} diff --git a/DEVELOPING.md b/DEVELOPING.md index 66b89e3b2..374563cb9 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -59,7 +59,7 @@ with the name `SLACK_WEBHOOK_URL`, containing a url for [Incoming Webhooks](http Run dependencies from the root of the project: ```bash -docker compose up -d postgres14 redis +docker compose -f docker-compose-services.yaml up -d ``` Run from `./backend`: @@ -98,6 +98,12 @@ To stop them: docker compose down ``` +If you want to run the java application and frontend yourself you can start only the other services (redis, postgres & keycloak) by running: + +```bash +docker compose -f docker-compose-services.yaml up -d +``` + # Testing ## Unit and Integration tests diff --git a/backend/README.md b/backend/README.md index c6751b305..30db4b227 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,7 +18,7 @@ Install the latest LTS version of node for running Spotless with Prettier. Set up and boot the postgres database and the redis database (from the project root): ```sh -docker compose up postgres14 redis +docker compose -f docker-compose-services.yaml up ``` You can then start the backend with two different options: diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index c29dbdb79..7f9980e3c 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(platform(libs.aws.bom)) implementation(libs.aws.s3) implementation(libs.squareup.okio.jvm) + implementation(libs.spring.oauth2.client) compileOnly(libs.lombok) @@ -84,6 +85,7 @@ dependencies { testImplementation(libs.testcontainers.core) testImplementation(libs.testcontainers.junit.jupiter) testImplementation(libs.testcontainers.postgres) + testImplementation(libs.testcontainers.keycloak) schematronToXsltCompileOnly(libs.schxslt) schematronToXsltCompileOnly(libs.saxon.he) diff --git a/backend/gradle/libs.versions.toml b/backend/gradle/libs.versions.toml index 04636fdf4..cda8fd3e6 100644 --- a/backend/gradle/libs.versions.toml +++ b/backend/gradle/libs.versions.toml @@ -36,6 +36,7 @@ spring-boot-starter-validation = { module = "org.springframework.boot:spring-boo spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } spring-cloud-starter-kubernetes-client-config = "org.springframework.cloud:spring-cloud-starter-kubernetes-client-config:3.2.0" spring-security-test = "org.springframework.security:spring-security-test:6.4.2" +spring-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } spring-session-data-redis = { module = "org.springframework.session:spring-session-data-redis" } spring-starter-data-redis = { module = "org.springframework.boot:spring-boot-starter-data-redis" } # CVE-2023-3635 @@ -43,6 +44,8 @@ squareup-okio-jvm = "com.squareup.okio:okio-jvm:3.9.1" testcontainers-core = { module = "org.testcontainers:testcontainers", version.ref = "org-testcontainers" } testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "org-testcontainers" } testcontainers-postgres = { module = "org.testcontainers:postgresql", version.ref = "org-testcontainers" } +testcontainers-keycloak = "com.github.dasniko:testcontainers-keycloak:3.5.1" + [plugins] dependency-license-report = "com.github.jk1.dependency-license-report:2.9" diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java new file mode 100644 index 000000000..602205ada --- /dev/null +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/FrontendFallbackController.java @@ -0,0 +1,25 @@ +package de.bund.digitalservice.ris.norms.adapter.input.restapi.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * A fallback controller for serving the frontend's index.html file. + *

+ * This controller handles all requests that do not match an existing resource or API endpoint. + * It ensures that the Vue.js single-page application (SPA) can take over client-side routing. + */ +@Controller +public class FrontendFallbackController { + + /** + * Serves index.html for all unmatched routes, excluding requests for static resources. + * This ensures Vue Router can handle client-side routing. + * + * @return Forward to index.html. + */ + @GetMapping(value = { "/{path:^(?!assets|api|.*\\.).*}", "/{path:^(?!assets|api|.*\\.).*}/**" }) + public String serveIndexHtml() { + return "forward:/index.html"; + } +} diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java new file mode 100644 index 000000000..d9e597fee --- /dev/null +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/LogoutController.java @@ -0,0 +1,34 @@ +package de.bund.digitalservice.ris.norms.adapter.input.restapi.controller; + +import jakarta.servlet.http.HttpSession; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller for handling user logout requests. + *

+ * This controller provides an endpoint for logging out authenticated users by invalidating their session. + * Upon logout, the session data stored in Redis is removed, effectively ending the user's session. + */ +@RestController +@RequestMapping("/api/v1/logout") +public class LogoutController { + + /** + * Logs out the current user by invalidating their session. + *

+ * This endpoint is designed to be called by authenticated users who wish to log out. + * It invalidates the user's session, which removes the associated data from the Redis session store. + * + * @param session The current HTTP session of the user making the logout request. + * @return A confirmation message indicating that the user has been logged out successfully. + */ + @PostMapping + public String logout(final HttpSession session) { + if (session != null) { + session.invalidate(); // Invalidate the session (removes it from Redis) + } + return "Logged out successfully"; + } +} diff --git a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java index c1a5cad25..7ee78b13b 100644 --- a/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java +++ b/backend/src/main/java/de/bund/digitalservice/ris/norms/config/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -33,17 +34,15 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { "/.well-known/security.txt", "/favicon.svg", "/actuator/health/**", - "/actuator/prometheus", - "/api/**", - "/index.html", - "/", - "/assets/**" + "/actuator/prometheus" ) .permitAll() .anyRequest() - .denyAll() + .authenticated() ) + .oauth2Login(Customizer.withDefaults()) .csrf(AbstractHttpConfigurer::disable) + .cors(Customizer.withDefaults()) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) ); diff --git a/backend/src/main/resources/application-local.yaml b/backend/src/main/resources/application-local.yaml index 905d04b14..ef1b6eaa1 100644 --- a/backend/src/main/resources/application-local.yaml +++ b/backend/src/main/resources/application-local.yaml @@ -1,10 +1,19 @@ spring: flyway: locations: classpath:db/migration,classpath:db/data + security: + oauth2: + client: + registration: + oidcclient: + client-id: ris-norms-local + client-secret: ris-norms-local + provider: + keycloak: + issuer-uri: http://localhost:8443/realms/ris local: file-storage: .local-storage - #logging: # level: # org.springframework: DEBUG diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 309ce75dc..fcaf85c93 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -18,6 +18,17 @@ spring: multipart: max-file-size: 10MB max-request-size: 10MB + + security: + oauth2: + client: + registration: + oidcclient: + provider: keycloak + scope: + - openid + - profile + - email server: servlet: session: diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java index fe046aae9..60da06b4a 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/AnnouncementControllerTest.java @@ -3,6 +3,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -24,10 +25,11 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -35,8 +37,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(AnnouncementController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = AnnouncementController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class AnnouncementControllerTest { @Autowired @@ -266,6 +271,7 @@ void itReleaseAnAnnouncement() throws Exception { "/api/v1/announcements/eli/bund/bgbl-1/2023/413/2023-12-29/1/deu/regelungstext-1/releases" ) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) ) .andExpect(status().isOk()) .andExpect(jsonPath("releaseAt", equalTo("2024-01-02T10:20:30Z"))) @@ -306,7 +312,12 @@ void itCreatesAnAnnouncement() throws Exception { // When // Then mockMvc - .perform(multipart("/api/v1/announcements").file(file).accept(MediaType.APPLICATION_JSON)) + .perform( + multipart("/api/v1/announcements") + .file(file) + .accept(MediaType.APPLICATION_JSON) + .with(csrf()) + ) .andExpect(status().isOk()) .andExpect( jsonPath("eli", equalTo("eli/bund/bgbl-1/2017/s419/2017-03-15/1/deu/regelungstext-1")) @@ -335,6 +346,7 @@ void itCreatesAnAnnouncementWithForce() throws Exception { multipart("/api/v1/announcements?force=true") .file(file) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) ) .andExpect(status().isOk()) .andExpect( @@ -361,7 +373,12 @@ void itShouldNotExposeInternalInformationOnUnexpectedErrors() throws Exception { // When // Then mockMvc - .perform(multipart("/api/v1/announcements").file(file).accept(MediaType.APPLICATION_JSON)) + .perform( + multipart("/api/v1/announcements") + .file(file) + .accept(MediaType.APPLICATION_JSON) + .with(csrf()) + ) .andExpect(status().isInternalServerError()) .andExpect(jsonPath("type", equalTo("/errors/internal-server-error"))) .andExpect(jsonPath("title", equalTo("Internal Server Error"))) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java index 28cb4a041..1dbaf131b 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ArticleControllerTest.java @@ -17,9 +17,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -27,8 +28,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(ArticleController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = ArticleController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class ArticleControllerTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java index ef466e768..b4fa68686 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ElementControllerTest.java @@ -14,9 +14,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -24,8 +25,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(ElementController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = ElementController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class ElementControllerTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java index 3dc9d79c0..4a3e63a0c 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormExpressionControllerTest.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -20,9 +21,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -30,8 +32,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(NormExpressionController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = NormExpressionController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class NormExpressionControllerTest { @Autowired @@ -279,6 +284,7 @@ void itCallsNormServiceAndUpdatesNorm() throws Exception { .perform( put("/api/v1/norms/{eli}", eli) .accept(MediaType.APPLICATION_XML) + .with(csrf()) .contentType(MediaType.APPLICATION_XML) .content(xml) ) @@ -304,6 +310,7 @@ void itCallsNormServiceAndReturnsErrorMessage() throws Exception { .perform( put("/api/v1/norms/{eli}", eli) .accept(MediaType.APPLICATION_XML) + .with(csrf()) .contentType(MediaType.APPLICATION_XML) .content(xml) ) @@ -335,6 +342,7 @@ void itCallsNormServiceAndReturnsUnprocessableWhenNodeIsMissing() throws Excepti .perform( put("/api/v1/norms/{eli}", eli) .accept(MediaType.APPLICATION_XML) + .with(csrf()) .contentType(MediaType.APPLICATION_XML) .content(xml) ) @@ -362,6 +370,7 @@ void itCallsUpdateModUseCaseAndReturnsXml() throws Exception { .perform( put("/api/v1/norms/" + eli + "/mods/" + modEid) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"refersTo\": \"aenderungsbefehl-ersetzen\", \"timeBoundaryEid\": \"new-time-boundary-eid\", \"destinationHref\": \"new-destination-href\", \"newContent\": \"new test text\"}" @@ -392,6 +401,7 @@ void itCallsUpdateModUseCaseWithDryRunAndReturnsXml() throws Exception { .perform( put("/api/v1/norms/" + eli + "/mods/" + modEid + "?dryRun=true") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"refersTo\": \"aenderungsbefehl-ersetzen\", \"timeBoundaryEid\": \"new-time-boundary-eid\", \"destinationHref\": \"new-destination-href\", \"newContent\": \"new test text\"}" @@ -425,6 +435,7 @@ void itCallsUpdateModsUseCaseAndReturnsXml() throws Exception { .perform( patch("/api/v1/norms/" + eli + "/mods") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"mod-eid-1\": {\"timeBoundaryEid\": \"new-time-boundary-eid\"},\n" + @@ -455,6 +466,7 @@ void itCallsUpdateModsUseCaseWithDryRunAndReturnsXml() throws Exception { .perform( patch("/api/v1/norms/" + eli + "/mods?dryRun=true") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"mod-eid-1\": {\"timeBoundaryEid\": \"new-time-boundary-eid\"}}") ) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java index b19964b8f..f9f7d7d60 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/NormManifestationControllerTest.java @@ -10,9 +10,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -20,8 +21,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(NormManifestationController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = NormManifestationController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) public class NormManifestationControllerTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java index 88c941dd4..5964fb546 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/ProprietaryControllerTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -22,9 +23,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -32,8 +34,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(ProprietaryController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = ProprietaryController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class ProprietaryControllerTest { @Autowired @@ -224,6 +229,7 @@ void updatesProprietarySuccess() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{date}", eli, date.toString()) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"fna\": \"new-fna\"," + @@ -285,6 +291,7 @@ void itReturnsNotFoundIfNormIsNotFound() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{date}", eli, "1990-01-01") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "{\"fna\": \"new-fna\"," + @@ -455,6 +462,7 @@ void updatesProprietarySuccess() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{eid}/{date}", eli, eid, date.toString()) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"artDerNorm\": \"SN\"}") ) @@ -491,6 +499,7 @@ void itReturnsNotFoundIfNormIsNotFound() throws Exception { .perform( put("/api/v1/norms/{eli}/proprietary/{eid}/{date}", eli, eid, "1990-01-01") .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("{\"artDerNorm\": \"SN\"}") ) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java index fec0f9ea0..03015f1cd 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/RenderingControllerTest.java @@ -1,13 +1,13 @@ package de.bund.digitalservice.ris.norms.adapter.input.restapi.controller; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import de.bund.digitalservice.ris.norms.application.port.input.ApplyPassiveModificationsUseCase; import de.bund.digitalservice.ris.norms.application.port.input.TransformLegalDocMlToHtmlUseCase; -import de.bund.digitalservice.ris.norms.config.SecurityConfig; import de.bund.digitalservice.ris.norms.domain.entity.Norm; import de.bund.digitalservice.ris.norms.utils.XmlMapper; import de.bund.digitalservice.ris.norms.utils.exceptions.XmlProcessingException; @@ -16,9 +16,10 @@ import org.junit.jupiter.api.Test; import org.mockito.AdditionalMatchers; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; @@ -26,8 +27,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Therefore, manually * setting up the {@code mockMvc} including the ControllerAdvice */ -@WebMvcTest(RenderingController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = RenderingController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class RenderingControllerTest { @Autowired @@ -58,6 +62,7 @@ void itThrowsXmlProcessingException() throws Exception { .perform( post("/api/v1/renderings") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -93,6 +98,7 @@ void getHtmlPreviewWithShowMetadataTrue() throws Exception { post("/api/v1/renderings") .queryParam("showMetadata", "true") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -139,6 +145,7 @@ void getHtmlPreviewWithShowMetadataFalse() throws Exception { post("/api/v1/renderings") .queryParam("showMetadata", "false") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -185,6 +192,7 @@ void getHtmlPreviewForDate() throws Exception { .queryParam("showMetadata", "true") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -237,6 +245,7 @@ void getHtmlPreviewForCurrentDate() throws Exception { .perform( post("/api/v1/renderings") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -275,6 +284,7 @@ void getHtmlPreviewForDateWithCustomNorms() throws Exception { .queryParam("showMetadata", "false") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -312,6 +322,7 @@ void getHtmlPreviewWithSnippetTrue() throws Exception { post("/api/v1/renderings") .queryParam("snippet", "true") .accept(MediaType.TEXT_HTML) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -351,6 +362,7 @@ void getXmlPreviewForCurrentDate() throws Exception { .perform( post("/api/v1/renderings") .accept(MediaType.APPLICATION_XML_VALUE) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -399,6 +411,7 @@ void getXmlPreviewForDate() throws Exception { post("/api/v1/renderings") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.APPLICATION_XML_VALUE) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -445,6 +458,7 @@ void getXmlPreviewForDateWithCustomNorms() throws Exception { post("/api/v1/renderings") .queryParam("atIsoDate", "2024-01-01T00:00:00.0Z") .accept(MediaType.APPLICATION_XML_VALUE) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( """ diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java index c13766217..eb6bf6077 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/adapter/input/restapi/controller/TimeBoundaryControllerTest.java @@ -5,6 +5,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -28,9 +29,10 @@ import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.method.annotation.HandlerMethodValidationException; @@ -39,8 +41,11 @@ * Not using SpringBootTest annotation to avoid needing a database connection. Using @Import to load * the {@link SecurityConfig} in order to avoid http 401 Unauthorised */ -@WebMvcTest(TimeBoundaryController.class) -@Import(SecurityConfig.class) +@WithMockUser +@WebMvcTest( + controllers = TimeBoundaryController.class, + excludeAutoConfiguration = OAuth2ClientAutoConfiguration.class +) class TimeBoundaryControllerTest { @Autowired @@ -209,6 +214,7 @@ void updateTimeBoundariesReturnsSuccess() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "[{\"date\": \"1964-09-21\", \"eventRefEid\": \"meta-1_lebzykl-1_ereignis-2\"}]" @@ -242,6 +248,7 @@ void updateTimeBoundariesMultipleSameDatesReturns400() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content( "[" + @@ -275,6 +282,7 @@ void updateTimeBoundariesReturnsDateMalformed() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[{\"date\": \"THISISNODATE\", \"eventRefEid\": null}]") ) @@ -299,6 +307,7 @@ void updateTimeBoundariesWithEmptyListReturns400() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[]") ) @@ -334,6 +343,7 @@ void updateTimeBoundariesReturnsDateIsNull() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[{\"date\": null, \"eventRefEid\": null}]") ) @@ -361,6 +371,7 @@ void validationFailureInSingleQuotesForValidJsonResponse() throws Exception { .perform( put("/api/v1/norms/{eli}/timeBoundaries", eli) .accept(MediaType.APPLICATION_JSON) + .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content("[{\"date\": null, \"eventRefEid\": null}]") ) diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java index 0f48e9e8c..ca4774300 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/BaseIntegrationTest.java @@ -1,5 +1,6 @@ package de.bund.digitalservice.ris.norms.integration; +import dasniko.testcontainers.keycloak.KeycloakContainer; import org.junit.jupiter.api.Tag; import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -37,6 +38,7 @@ @Tag("integration") public abstract class BaseIntegrationTest { + protected static final KeycloakContainer keycloak; protected static final String LOCAL_STORAGE_PATH = ".local-storage-integration-test"; @Container @@ -51,6 +53,13 @@ public abstract class BaseIntegrationTest { ) .withExposedPorts(6379); + static { + keycloak = + new KeycloakContainer() + .withRealmImportFile("de/bund/digitalservice/ris/norms/keycloak/realm-export.json"); + keycloak.start(); + } + @DynamicPropertySource static void registerDynamicProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.username", postgreSQLContainer::getUsername); @@ -69,5 +78,15 @@ static void registerDynamicProperties(DynamicPropertyRegistry registry) { // Removing user/pass for redis registry.add("spring.data.redis.username", () -> ""); registry.add("spring.data.redis.password", () -> ""); + + registry.add( + "spring.security.oauth2.client.provider.keycloak.issuer-uri", + () -> keycloak.getAuthServerUrl() + "/realms/testing" + ); + registry.add("spring.security.oauth2.client.registration.oidcclient.client-id", () -> "norms"); + registry.add( + "spring.security.oauth2.client.registration.oidcclient.client-secret", + () -> "3LEwwv3DNY0N086lCOYeuri3aLicVDw1" + ); } } diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java index 88b7183cc..46fda603f 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/AnnouncementControllerIntegrationTest.java @@ -28,8 +28,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class AnnouncementControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java index 329a2c435..133e5d62d 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ArticleControllerIntegrationTest.java @@ -17,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class ArticleControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java index 4e5f9d457..c8cd54b3e 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ElementControllerIntegrationTest.java @@ -14,8 +14,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class ElementControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java index ae5e8d176..454e936e1 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormExpressionControllerIntegrationTest.java @@ -21,8 +21,10 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class NormExpressionControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java index 55a14b11a..eeef752f0 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/NormManifestationControllerIntegrationTest.java @@ -12,8 +12,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser public class NormManifestationControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java index 47066ae28..50d557cfa 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/ProprietaryControllerIntegrationTest.java @@ -17,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser public class ProprietaryControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java index b6d499d3e..62c6c3a10 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/RenderingControllerIntegrationTest.java @@ -15,8 +15,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser class RenderingControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java index 2866d9669..768284333 100644 --- a/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java +++ b/backend/src/test/java/de/bund/digitalservice/ris/norms/integration/adapter/input/restapi/TimeBoundaryControllerIntegrationTest.java @@ -17,8 +17,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser public class TimeBoundaryControllerIntegrationTest extends BaseIntegrationTest { @Autowired diff --git a/backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json b/backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json new file mode 100644 index 000000000..4944651a5 --- /dev/null +++ b/backend/src/test/resources/de/bund/digitalservice/ris/norms/keycloak/realm-export.json @@ -0,0 +1,1743 @@ +{ + "id": "testing", + "realm": "testing", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": { + "id": "858c23ee-e27c-4721-a004-7cb14e95b683", + "name": "default-roles-testing", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "testing" + }, + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": ["FreeOTP", "Google Authenticator"], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": ["manage-account"] + } + ] + }, + "clients": [ + { + "id": "c116fbb6-6592-49bf-ae9a-ee1984f736d6", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/testing/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/testing/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "343f0bf9-cfbb-48f6-8578-12c97012cc74", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/testing/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/realms/testing/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "3687b345-f36f-4994-8bc8-09136f5b8a88", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "5e19f3dd-c3df-4146-b8d1-b8657ebc4426", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "66e285d5-d28b-44a6-a770-bbb3a20eb977", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "c030a98d-48e4-4f75-9d4f-2c09f6132d18", + "clientId": "norms", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "3LEwwv3DNY0N086lCOYeuri3aLicVDw1", + "redirectUris": ["localhost:8080"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "saml.artifact.binding": "false", + "saml_force_name_id_format": "false", + "acr.loa.map": "{}", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "387bdeff-1f9b-4284-ae59-e24b5dce6bbf", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0e5ecc89-68bb-47d3-880f-a898f2314836", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/testing/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["/admin/testing/console/*"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "6556594d-416c-459f-82df-8c4f4770a242", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "profile", "roles", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "41b5bb79-7d6b-4e92-b7d7-64069c66af14", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "44261995-7352-4882-b285-0ed7362bcf79", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "8ed35d50-c66e-482b-aacd-47e6feca3fa4", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "6f6643ab-530d-4261-97ba-42fd77e32de7", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "662d9d44-e0d1-4606-9fe0-6510e07102d5", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "0d088c8e-0ec0-4e9a-a8a0-40deae194b00", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "c0304a2e-b49c-4a29-b577-cb75e669f397", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ec197e80-3409-46eb-a3aa-8a27fb39db8c", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "90cca696-5a33-49fd-82d2-ca89d3b79be7", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "d26afed6-ccfa-4929-b296-1ba339f8afdf", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "61d59219-5aa4-4aa2-b0c8-7f846409ce3d", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "733fbba1-39c6-466c-acc0-cce27379240e", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "e27310b4-0e8f-4dfe-a324-1e42b1277776", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "6e96348f-6d0e-498a-a9fc-6213291710c4", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "55478f17-6486-448f-b823-da1a6e9d555c", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "f67909b2-6a6a-496f-a3b8-bf2cdc3e10ce", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "1f6876f7-555d-4a7c-9fbe-fa4aab6f8602", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "e1aaccc7-b870-44fb-9e47-77e91b8f0fd8", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "c90efa04-4035-4870-8d27-339ab7636747", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "6b129f51-dbe0-4a6b-872d-f6c19706477a", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "9736eb2b-0887-44c1-947c-61599da9b08a", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "5ab1e866-ab0b-43d8-87d2-bf65a8598192", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "75b8cd24-353d-489a-b447-d87880e13bc8", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "46585087-34d3-4b6a-b49f-d5266fe0099b", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "4d296b06-a515-486f-8b53-45d1a71af0dc", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "29807788-247d-48aa-bd90-7289fc8df8b8", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "a1e04145-5b4c-4456-8de2-bd4f28d14f03", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "8ea4915e-34ca-4af8-a477-5e7273bab40b", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1bd6ee1f-745b-49c8-9f79-14ee99712302", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "b0deb610-8070-47cd-ad57-90f80151702f", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "57739318-89c1-4a5f-bede-53cec86cb755", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "9afafae2-4ba3-4e13-8338-8ab4d9200879", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "599e1985-8671-4905-94dd-3cd562ee4a1b", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "ef77b71f-8d7e-4c29-a38c-5e3e4bf8036d", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "d93a3ae3-6ba3-448a-8c58-23b45cd4d9ce", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "web-origins", + "email", + "roles", + "profile", + "role_list" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "phone", + "address", + "microprofile-jwt" + ], + "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" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "195515d9-7706-4f4c-b981-18e44c7905f9", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + }, + { + "id": "337a974c-0e18-4668-bd62-c8d9b17214b2", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "be083412-9979-496e-be1a-877e2ffad9b9", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "b603bd8c-ca25-4474-8a82-fb3b3d4cec64", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "c6f3c7e4-9b57-4205-b189-e463768622a5", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "oidc-full-name-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "6c8ce11c-60fb-4898-b90c-19cd9ffa9e12", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "15a45693-6a1c-45e8-913d-f9863489309e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "f20aa89a-85e4-43c1-9513-07394e6cf3ad", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "71e2003e-0168-4db0-b99c-a227c0f4829c", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": ["100"], + "algorithm": ["HS256"] + } + }, + { + "id": "5e591eb1-c79e-4b4a-9513-be4cb74cc042", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": ["100"] + } + }, + { + "id": "4342696d-56b7-4083-b11f-53577194e08f", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": ["100"] + } + }, + { + "id": "3f61d57f-8515-423e-9981-1b8944c8750d", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": ["100"], + "algorithm": ["RSA-OAEP"] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "d861511d-6db5-4ebc-9b80-addc63b22df7", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "60a3feba-5b5d-4b46-a087-d384ccc64820", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "85a7b608-d2af-4610-a5ff-c4db223e3b97", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "e80a120c-6fd1-41b7-861c-2b0ab3df5268", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b30d8d48-dae6-46a5-936b-e716bbb70256", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "83f3b43a-d7e5-44df-86f5-899203358982", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "e53adb44-5aa8-4be2-bd00-ffd99571c4fc", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1588dd80-71f6-412a-b385-30d30a294e83", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "fa62c6d4-8510-4f5b-bf32-d4917da5283a", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "772a641d-796b-4968-9922-0ab98e3f3f75", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "c3df8730-a054-49c6-9b96-3b564186ed3e", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "955e50f5-8bf0-4192-be2b-f77cf078b63f", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "59a54417-6238-4e43-9cf7-5d92c7de4a90", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "460e4113-b06d-4415-80b2-8ee2cac4b7c2", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "929da9e7-3a9c-4c7c-b07a-0efca204d46e", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "a3fd479c-d42e-4ee0-828f-ca5aaf28426a", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "11bcac6b-1266-4a90-89e1-8f4295c060bd", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "b1e71934-b6a2-46c3-9f0c-2ee2f1af341e", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "b2a17b38-d4ed-48d2-b699-1951d1f340bb", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "142c01b3-2e9c-4dcc-a931-b1264e620df0", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "0b9d2e29-a641-4fe6-bddb-74fbfd626fc1", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "38727c1d-d805-4689-875a-871ac7ac4dc3", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5" + }, + "keycloakVersion": "17.0.1", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + }, + "users": [ + { + "username": "test", + "email": "test@test.com", + "firstName": "test", + "lastName": "test", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "test" + } + ], + "clientRoles": { + "account": ["view-profile", "manage-account"] + } + } + ] +} diff --git a/docker-compose-services.yaml b/docker-compose-services.yaml new file mode 100644 index 000000000..52fbfe38e --- /dev/null +++ b/docker-compose-services.yaml @@ -0,0 +1,73 @@ +services: + postgres14: + image: postgres:14-alpine + restart: always + container_name: postgres14 + volumes: + - postgres14-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=risnorms + - POSTGRES_PASSWORD=test + - POSTGRES_USER=test + ports: + - 5432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U test -d risnorms"] + interval: 5s + retries: 3 + start_period: 5s + timeout: 3s + + redis: + image: cgr.dev/chainguard/redis:latest + extra_hosts: + - localhost:host-gateway + container_name: redis + command: + - "--maxmemory 256mb" + - "--maxmemory-policy allkeys-lru" + - "--timeout 300" + - "--tcp-keepalive 10" + - "--user redis on +@all -CONFIG ~* >password" + - "--user default off resetchannels -@all" + ports: + - "6379:6379" + healthcheck: + test: + [ + "CMD-SHELL", + "redis-cli -h 127.0.0.1 --user redis -a password PING | grep 'PONG' || exit 1", + ] + interval: 5s + retries: 5 + start_period: 3s + timeout: 5s + + keycloak: + image: quay.io/keycloak/keycloak:26.0.7 + command: ["start-dev", "--import-realm"] + container_name: keycloak + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: test + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + KC_HTTP_PORT: 8443 + KC_HOSTNAME_URL: http://localhost:8443/keycloak + ports: + - "8443:8443" + volumes: + - ./local/keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro + # health endpoint läuft unter 9000 + healthcheck: + test: + [ + "CMD-SHELL", + '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost:9000/health/live', + ] + interval: 5s + timeout: 5s + retries: 20 + +volumes: + postgres14-data: diff --git a/docker-compose.yaml b/docker-compose.yaml index e7ee2a532..90d23c824 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,14 +1,24 @@ services: - nginx: - build: - context: . - dockerfile: DockerfileNginxPlaywright + # We use a service called localhost to be able to address the keycloak service as "localhost:8443" in the + # application.yaml of our application. Using "keycloak:8443" is not possible as spring would then redirect the user to + # "keycloak:8443" to login, but that is a host that is not available on the host system. The spring boot oauth + # implementation is very restrictive on the issuer and therefore creates errors if the hosts used by it and the user + # to connect to keycloak differ. + # See also: + # - https://github.com/spring-projects/spring-security/issues/14633 + # - https://github.com/keycloak/keycloak/issues/29783 + # - https://github.com/keycloak/keycloak/issues/24252 + # - https://medium.com/@kostapchuk/integrating-keycloak-with-spring-boot-in-a-dockerized-environment-813eab1f140c + localhost: + image: alpine:3.21 + command: sleep infinity ports: - - 8080:8080 - depends_on: - - webapp + - "8443:8443" # Keycloak port + - "8080:8080" # Webapp port + webapp: image: ris-norms-app:001 + container_name: webapp build: context: . dockerfile: DockerfileApp @@ -16,53 +26,32 @@ services: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres14:5432/risnorms - REDIS_HOST=redis - SPRING_PROFILES_ACTIVE=local + network_mode: "service:localhost" depends_on: - - postgres14 - - redis + postgres14: + condition: service_healthy + redis: + condition: service_healthy + keycloak: + condition: service_healthy volumes: - ./local-storage:/app/.local-storage + postgres14: - image: postgres:14-alpine - restart: always - container_name: postgres14 - volumes: - - postgres14-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=risnorms - - POSTGRES_PASSWORD=test - - POSTGRES_USER=test - ports: - - 5432:5432 - healthcheck: - test: ["CMD-SHELL", "pg_isready -U test -d risnorms"] - interval: 5s - retries: 3 - start_period: 5s - timeout: 3s + extends: + file: docker-compose-services.yaml + service: postgres14 + redis: - image: cgr.dev/chainguard/redis:latest - extra_hosts: - - localhost:host-gateway - container_name: redis - command: - - "--maxmemory 256mb" - - "--maxmemory-policy allkeys-lru" - - "--timeout 300" - - "--tcp-keepalive 10" - - "--user redis on +@all -CONFIG ~* >password" - - "--user default off resetchannels -@all" - ports: - - "6379:6379" - healthcheck: - test: - [ - "CMD-SHELL", - "redis-cli -h 127.0.0.1 --user redis -a password PING | grep 'PONG' || exit 1", - ] - interval: 5s - retries: 5 - start_period: 3s - timeout: 5s + extends: + file: docker-compose-services.yaml + service: redis + + keycloak: + extends: + file: docker-compose-services.yaml + service: keycloak + network_mode: "service:localhost" volumes: postgres14-data: diff --git a/local/keycloak/realm.json b/local/keycloak/realm.json new file mode 100644 index 000000000..83f91796a --- /dev/null +++ b/local/keycloak/realm.json @@ -0,0 +1,82 @@ +{ + "id": "ris", + "realm": "ris", + "sslRequired": "none", + "ssoSessionIdleTimeout": 432000, + "ssoSessionMaxLifespan": 7776000, + "enabled": true, + "eventsEnabled": true, + "eventsExpiration": 900, + "adminEventsEnabled": true, + "adminEventsDetailsEnabled": true, + "attributes": { + "adminEventsExpiration": "900" + }, + "clients": [ + { + "id": "ris-norms-local", + "clientId": "ris-norms-local", + "name": "ris-norms-local", + "enabled": true, + "publicClient": true, + "rootUrl": "http://localhost:8080", + "baseUrl": "http://localhost:8080", + "redirectUris": [ + "http://localhost:8080/*" + ], + "webOrigins": [ + "http://localhost:8080" + ], + "clientAuthenticatorType": "client-secret", + "secret": "ris-norms-local", + "protocolMappers": [ + { + "id": "c4b86c90-3076-49df-9343-0928b135733a", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "multivalued": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "groups" + } + } + ] + } + ], + "groups": [ + { + "id": "4f928164-2bd4-455a-8fbd-174eeb8ea205", + "name": "Norms", + "subGroups": [], + "attributes": {}, + "realmRoles": [], + "clientRoles": {} + } + ], + "users": [ + { + "id": "jane.doe", + "email": "jane.doe@example.com", + "username": "jane.doe", + "firstName": "Jane", + "lastName": "Doe", + "enabled": true, + "emailVerified": true, + "credentials": [ + { + "temporary": false, + "type": "password", + "value": "test" + } + ], + "groups": ["Norms"] + } + ] +} diff --git a/nginx-playwright.conf b/nginx-playwright.conf index d4d228214..c38d57753 100644 --- a/nginx-playwright.conf +++ b/nginx-playwright.conf @@ -2,17 +2,19 @@ server { listen 8080; server_name localhost; + # proxy_set_header Host $http_host --> to preserve the HTTP host header so that Spring Security correctly resolve the external URL, ensuring the redirect URI matches what the OAuth2 provider (Bare.ID) expects. location ~ /(.+)\.[^\/]+ { proxy_pass "http://webapp:8080"; + proxy_set_header Host $http_host; } location /api { proxy_pass "http://webapp:8080"; + proxy_set_header Host $http_host; } location / { - try_files $uri $uri/ /; proxy_pass "http://webapp:8080"; + proxy_set_header Host $http_host; } - }