diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..ce8a28135f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,11 @@ +# Agent Instructions + +## Available skills + +- `docs/skills/security/SKILL.md` + Use for broader Spring Security, IDAM/OIDC, auth filter, and security-related regression work in this repo. + Prompt cue: `Use docs/skills/security/SKILL.md` + +- `docs/skills/security-jwt-issuer/SKILL.md` + Use for JWT issuer validation, issuer mismatch diagnosis, token `iss` checks, and pipeline verifier updates. + Prompt cue: `Use docs/skills/security-jwt-issuer/SKILL.md` diff --git a/Dockerfile b/Dockerfile index 6fc9cab769..502fb7f05a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG JAVA_OPTS="-Djava.security.egd=file:/dev/./urandom" ARG APP_INSIGHTS_AGENT_VERSION=3.7.7 ARG PLATFORM="" -FROM hmctspublic.azurecr.io/base/java${PLATFORM}:21-distroless +FROM hmctsprod.azurecr.io/base/java${PLATFORM}:21-distroless USER hmcts LABEL maintainer="https://github.com/hmcts/ccd-data-store-api" diff --git a/Jenkinsfile_CNP b/Jenkinsfile_CNP index 782a1cbc5e..8d2e8fbf44 100644 --- a/Jenkinsfile_CNP +++ b/Jenkinsfile_CNP @@ -107,13 +107,15 @@ env.ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_ID = "ccd_data" env.BEFTA_TEST_STUB_SERVICE_BASE_URL = "http://ccd-test-stubs-service-aat.service.core-compute-aat.internal" env.BEFTA_S2S_CLIENT_ID_OF_CCD_DATA = "ccd_data" env.BEFTA_S2S_CLIENT_ID_OF_XUI_WEBAPP = "xui_webapp" +env.OIDC_ISSUER = "https://forgerock-am.service.core-compute-idam-aat2.internal:8443/openam/oauth2/realms/root/realms/hmcts" +env.VERIFY_OIDC_ISSUER = "true" // BEFTA retry env variables env.BEFTA_RETRY_MAX_ATTEMPTS = "3" env.BEFTA_RETRY_STATUS_CODES = "500,502,503,504" env.BEFTA_RETRY_MAX_DELAY = "1000" env.BEFTA_RETRY_NON_RETRYABLE_HTTP_METHODS = "POST,PUT" -// Prevent Docker hub rate limit errors by ensuring that testcontainers uses images from hmctspublic ACR -env.TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX = "hmctspublic.azurecr.io/imported/" +// Prevent Docker hub rate limit errors by ensuring that testcontainers uses images from hmctsprod ACR +env.TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX = "hmctsprod.azurecr.io/imported/" withPipeline(type, product, component) { onMaster { diff --git a/Jenkinsfile_nightly b/Jenkinsfile_nightly index 3812977cde..f90c4fefee 100644 --- a/Jenkinsfile_nightly +++ b/Jenkinsfile_nightly @@ -81,13 +81,15 @@ def vaultOverrides = [ // vars needed for functional tests // Assume a feature build branched off 'develop', with dependencies develop-to-develop. env.TEST_URL = "http://ccd-data-store-api-aat.service.core-compute-aat.internal" -// Prevent Docker hub rate limit errors by ensuring that testcontainers uses images from hmctspublic ACR -env.TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX = "hmctspublic.azurecr.io/imported/" +// Prevent Docker hub rate limit errors by ensuring that testcontainers uses images from hmctsprod ACR +env.TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX = "hmctsprod.azurecr.io/imported/" // Other env variables needed for BEFTA. env.BEFTA_S2S_CLIENT_ID = "ccd_gw" env.ROLE_ASSIGNMENT_API_GATEWAY_S2S_CLIENT_ID = "ccd_data" env.BEFTA_S2S_CLIENT_ID_OF_CCD_DATA = "ccd_data" +env.OIDC_ISSUER = "https://forgerock-am.service.core-compute-idam-aat2.internal:8443/openam/oauth2/realms/root/realms/hmcts" +env.VERIFY_OIDC_ISSUER = "true" env.DM_STORE_BASE_URL = "http://dm-store-aat.service.core-compute-aat.internal" env.CASE_DOCUMENT_AM_URL = "http://ccd-case-document-am-api-aat.service.core-compute-aat.internal" env.RD_LOCATION_REF_API_BASE_URL = "http://rd-location-ref-api-aat.service.core-compute-aat.internal" diff --git a/README.md b/README.md index 617508e2d6..430554ce3f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ The following environment variables are required: | DATA_STORE_S2S_AUTHORISED_SERVICES | ccd_gw | Authorised micro-service names for S2S calls | | IDAM_USER_URL | - | Base URL for IdAM's User API service (idam-app). `http://localhost:4501` for the dockerised local instance or tunneled `dev` instance. | | IDAM_S2S_URL | - | Base URL for IdAM's S2S API service (service-auth-provider). `http://localhost:4502` for the dockerised local instance or tunneled `dev` instance. | +| IDAM_OIDC_URL | - | Base URL for IdAM OIDC discovery and JWKS lookup. This is used to resolve the OpenID configuration and signing keys. | +| OIDC_ISSUER | - | Enforced JWT issuer value. This must match the `iss` claim in real access tokens accepted by this service. Do not guess it; derive it from a real token for the target environment. | | USER_PROFILE_HOST | - | Base URL for the User Profile service. `http://localhost:4453` for the dockerised local instance. | | DEFINITION_STORE_HOST | - | Base URL for the Definition Store service. `http://localhost:4451` for the dockerised local instance. | | CCD_DOCUMENT_URL_PATTERN | - | URL Pattern for documents attachable to cases. | @@ -48,6 +50,11 @@ The following environment variables are required: | DRAFT_ENCRYPTION_KEY | - | Draft encryption key. The encryption key used by draft store to encrypt documents with. | | DRAFT_TTL_DAYS | - | Number of days after which the saved draft will be deleted if unmodified. | +`IDAM_OIDC_URL` and `OIDC_ISSUER` are intentionally separate. Discovery and JWKS retrieval use `IDAM_OIDC_URL`, while JWT validation enforces `OIDC_ISSUER`. If these do not align with the issuer used in real caller tokens, authenticated requests will be rejected with `401`. + +### Codex Workflow Docs +Repo-local workflow docs are indexed in `AGENTS.md`. + ### Building The project uses [Gradle](https://gradle.org/). diff --git a/acb.tpl.yaml b/acb.tpl.yaml index 730b380a3d..2f4555af09 100644 --- a/acb.tpl.yaml +++ b/acb.tpl.yaml @@ -1,7 +1,7 @@ version: 1.0-preview-1 steps: - id: pull-base-image-amd64 - cmd: docker pull --platform linux/amd64 hmctspublic.azurecr.io/base/java:21-distroless && docker tag hmctspublic.azurecr.io/base/java:21-distroless hmctspublic.azurecr.io/base/java/linux/amd64:21-distroless + cmd: docker pull --platform linux/amd64 hmctsprod.azurecr.io/base/java:21-distroless && docker tag hmctsprod.azurecr.io/base/java:21-distroless hmctsprod.azurecr.io/base/java/linux/amd64:21-distroless when: ["-"] retries: 3 retryDelay: 5 @@ -18,7 +18,7 @@ steps: retryDelay: 5 - id: pull-base-image-arm64 - cmd: docker pull --platform linux/arm64 hmctspublic.azurecr.io/base/java:21-distroless && docker tag hmctspublic.azurecr.io/base/java:21-distroless hmctspublic.azurecr.io/base/java/linux/arm64:21-distroless + cmd: docker pull --platform linux/arm64 hmctsprod.azurecr.io/base/java:21-distroless && docker tag hmctsprod.azurecr.io/base/java:21-distroless hmctsprod.azurecr.io/base/java/linux/arm64:21-distroless when: - pull-base-image-amd64 retries: 3 diff --git a/build.gradle b/build.gradle index 7141f7536e..bcab52df72 100644 --- a/build.gradle +++ b/build.gradle @@ -262,7 +262,7 @@ dependencies { implementation group: 'com.github.hmcts', name: 'service-auth-provider-java-client', version: '5.3.3' implementation group: 'com.github.hmcts', name: 'idam-java-client', version: '3.0.5' implementation group: 'com.github.hmcts', name: 'ccd-case-document-am-client', version: '1.59.2' - implementation group: 'com.github.hmcts.java-logging', name: 'logging', version: '6.1.9' + implementation group: 'com.github.hmcts.java-logging', name: 'logging', version: '8.0.0' implementation group: 'com.auth0', name: 'java-jwt', version: '4.5.1' implementation group: 'com.google.guava', name: 'guava', version: '33.5.0-jre' @@ -306,7 +306,7 @@ dependencies { testImplementation group: 'org.springframework.boot', name: 'spring-boot-starter-test' testImplementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-contract-stub-runner' testImplementation group: 'org.testcontainers', name: 'testcontainers', version: testContainersVersion - testImplementation group: 'org.testcontainers', name: 'postgresql', version: testContainersVersion + testImplementation group: 'org.testcontainers', name: 'postgresql', version: '1.21.3' testImplementation group: 'org.testcontainers', name: 'elasticsearch', version: testContainersVersion testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: testContainersVersion testImplementation group: 'org.openid4java', name: 'openid4java', version: '1.0.0' // for sonar analysis @@ -728,16 +728,59 @@ idea { } task highLevelDataSetup(type: JavaExec) { - dependsOn aatClasses + dependsOn 'aatClasses', 'verifyFunctionalTestJwtIssuer' mainClass = "uk.gov.hmcts.ccd.datastore.befta.HighLevelDataSetupApp" classpath += configurations.cucumberRuntime + sourceSets.aat.runtimeClasspath jvmArgs = ['--add-opens=java.base/java.lang.reflect=ALL-UNNAMED'] } +task verifyFunctionalTestJwtIssuer(type: JavaExec) { + description = 'Verifies the functional/smoke test token issuer matches OIDC_ISSUER' + group = 'Verification' + dependsOn aatClasses + + onlyIf { + System.getenv('VERIFY_OIDC_ISSUER')?.toBoolean() + } + + mainClass = 'uk.gov.hmcts.ccd.datastore.befta.JwtIssuerVerificationApp' + classpath += configurations.cucumberRuntime + sourceSets.aat.runtimeClasspath +} + +task verifyOidcIssuerPolicy { + description = 'Fails if oidc.issuer is derived from discovery configuration' + group = 'Verification' + + doLast { + def policyFiles = [ + 'src/main/resources/application.properties', + 'src/test/resources/test.properties', + 'src/contractTest/resources/application.properties' + ] + + def violations = policyFiles.findAll { path -> + def file = file(path) + file.exists() && file.readLines().any { line -> + def normalized = line.replaceAll(/\s+/, '') + normalized.startsWith('oidc.issuer=') && normalized.contains('IDAM_OIDC_URL') + || normalized.startsWith('oidc.issuer=') && normalized.contains('idam.api.url') + } + } + + if (!violations.isEmpty()) { + throw new GradleException( + "OIDC issuer policy violation. Do not derive oidc.issuer from discovery config in: ${violations.join(', ')}" + ) + } + } +} + +check.dependsOn verifyOidcIssuerPolicy + task smoke() { description = 'Executes smoke tests against an the CCD Data Store API instance just deployed' - dependsOn aatClasses + dependsOn 'aatClasses', 'verifyFunctionalTestJwtIssuer' new File("$buildDir/test-results/test").mkdirs() copy { @@ -778,7 +821,7 @@ def tags = (findProperty('tags') == null) ? 'not @Ignore' : '(' + findProperty(' task functional(type: JavaExec) { description = "Executes functional tests against an the CCD Data Store API instance just deployed" group = "Verification" - dependsOn aatClasses + dependsOn 'aatClasses', 'verifyFunctionalTestJwtIssuer' group = "Verification" diff --git a/charts/ccd-data-store-api/Chart.yaml b/charts/ccd-data-store-api/Chart.yaml index 88984aaf76..69dc755e86 100644 --- a/charts/ccd-data-store-api/Chart.yaml +++ b/charts/ccd-data-store-api/Chart.yaml @@ -2,14 +2,14 @@ description: Helm chart for the HMCTS CCD Data Store name: ccd-data-store-api apiVersion: v2 home: https://github.com/hmcts/ccd-data-store-api -version: 2.0.37 +version: 2.0.38 maintainers: - name: HMCTS CCD Dev Team email: ccd-devops@HMCTS.NET dependencies: - name: java version: 5.3.0 - repository: 'oci://hmctspublic.azurecr.io/helm' + repository: 'oci://hmctsprod.azurecr.io/helm' - name: elasticsearch version: 7.17.3 repository: 'https://helm.elastic.co' @@ -20,5 +20,5 @@ dependencies: condition: elastic.enabled - name: ccd version: 9.2.2 - repository: 'oci://hmctspublic.azurecr.io/helm' + repository: 'oci://hmctsprod.azurecr.io/helm' condition: ccd.enabled diff --git a/charts/ccd-data-store-api/values.preview.template.yaml b/charts/ccd-data-store-api/values.preview.template.yaml index 696373792b..34b10c191a 100644 --- a/charts/ccd-data-store-api/values.preview.template.yaml +++ b/charts/ccd-data-store-api/values.preview.template.yaml @@ -32,6 +32,8 @@ java: DEFINITION_STORE_HOST: http://${SERVICE_NAME}-ccd-definition-store USER_PROFILE_HOST: http://${SERVICE_NAME}-ccd-user-profile-api ROLE_ASSIGNMENT_URL: http://${SERVICE_NAME}-am-role-assignment-service + IDAM_OIDC_URL: https://idam-web-public.aat.platform.hmcts.net + OIDC_ISSUER: https://forgerock-am.service.core-compute-idam-aat2.internal:8443/openam/oauth2/realms/root/realms/hmcts ELASTIC_SEARCH_ENABLED: true ELASTIC_SEARCH_NODES_DISCOVERY_ENABLED: true ELASTIC_SEARCH_HOSTS: "{{ .Release.Name }}-es-master:9200" @@ -92,7 +94,7 @@ ccd: ccd-definition-store-api: java: ingressHost: ccd-definition-store-${SERVICE_FQDN} - image: hmctspublic.azurecr.io/ccd/definition-store-api:latest + image: hmctsprod.azurecr.io/ccd/definition-store-api:latest imagePullPolicy: Always devmemoryRequests: 2048Mi devcpuRequests: 2000m @@ -116,7 +118,7 @@ ccd: ccd-user-profile-api: java: ingressHost: ccd-user-profile-api-${SERVICE_FQDN} - image: hmctspublic.azurecr.io/ccd/user-profile-api:latest + image: hmctsprod.azurecr.io/ccd/user-profile-api:latest imagePullPolicy: Always environment: USER_PROFILE_DB_HOST: "{{ .Release.Name }}-postgresql" @@ -178,13 +180,13 @@ elasticsearch: # paths: # - path: / logstash: - image: "hmctspublic.azurecr.io/imported/logstash/logstash" + image: "hmctsprod.azurecr.io/imported/logstash/logstash" imageTag: "7.16.1" imagePullPolicy: "IfNotPresent" logstashJavaOpts: "-Xmx1g -Xms512M" extraInitContainers: | - name: download-postgres-jdbc - image: hmctspublic.azurecr.io/curl:7.70.0 + image: hmctsprod.azurecr.io/curl:7.70.0 command: ['curl', '-L', 'https://jdbc.postgresql.org/download/postgresql-42.2.18.jar', '-o', '/logstash-lib/postgresql.jar'] volumeMounts: - name: logstash-lib diff --git a/charts/ccd-data-store-api/values.yaml b/charts/ccd-data-store-api/values.yaml index c2ae820393..8caab5c4de 100644 --- a/charts/ccd-data-store-api/values.yaml +++ b/charts/ccd-data-store-api/values.yaml @@ -4,7 +4,7 @@ ccd: enabled: false java: - image: 'hmctspublic.azurecr.io/ccd/data-store-api:latest' + image: 'hmctsprod.azurecr.io/ccd/data-store-api:latest' ingressHost: ccd-data-store-api-{{ .Values.global.environment }}.service.core-compute-{{ .Values.global.environment }}.internal applicationPort: 4452 aadIdentityName: ccd @@ -76,7 +76,7 @@ java: CCD_DOCUMENT_URL_PATTERN: ^https?://(((?:api-gateway\.preprod\.dm\.reform\.hmcts\.net|dm-store-{{ .Values.global.environment }}\.service\.core-compute-{{ .Values.global.environment }}\.internal(?::\d+)?)\/documents\/[A-Za-z0-9-]+(?:\/binary)?)|(em-hrs-api-{{ .Values.global.environment }}\.service\.core-compute-{{ .Values.global.environment }}\.internal(?::\d+)?\/hearing-recordings\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\/((segments\/[0-9]+)|(file/\S+)))) IDAM_API_BASE_URL: https://idam-api.{{ .Values.global.environment }}.platform.hmcts.net IDAM_OIDC_URL: https://idam-web-public.{{ .Values.global.environment }}.platform.hmcts.net - OIDC_ISSUER: https://forgerock-am.service.core-compute-idam-{{ .Values.global.environment }}.internal:8443/openam/oauth2/hmcts + OIDC_ISSUER: https://forgerock-am.service.core-compute-idam-{{ .Values.global.environment }}.internal:8443/openam/oauth2/realms/root/realms/hmcts CCD_DRAFT_STORE_URL: http://draft-store-service-{{ .Values.global.environment }}.service.core-compute-{{ .Values.global.environment }}.internal CCD_DEFAULTPRINTURL: https://return-case-doc-ccd.nonprod.platform.hmcts.net/jurisdictions/:jid/case-types/:ctid/cases/:cid CASE_DOCUMENT_AM_URL: http://ccd-case-document-am-api-{{ .Values.global.environment }}.service.core-compute-{{ .Values.global.environment }}.internal diff --git a/docs/security/jwt-issuer-validation.md b/docs/security/jwt-issuer-validation.md new file mode 100644 index 0000000000..a6eb2a5c97 --- /dev/null +++ b/docs/security/jwt-issuer-validation.md @@ -0,0 +1,169 @@ +# JWT issuer validation + +## Service + +`ccd-data-store-api` + +## Summary + +- re-enables issuer validation in `ccd-data-store-api`, so JWTs must match `oidc.issuer` as well as pass timestamp checks +- follows the single configured issuer approach rather than an allow-list model +- any change to this repo's JWT issuer configuration should remain consistent with the HMCTS guidance in the [HMCTS Guidance](#hmcts-guidance) section and the externally agreed service issuer policy + +## HMCTS Guidance + +- [JWT iss Claim Validation guidance](https://tools.hmcts.net/confluence/spaces/SISM/pages/1958056812/JWT+iss+Claim+Validation+for+OIDC+and+OAuth+2+Tokens#JWTissClaimValidationforOIDCandOAuth2Tokens-Configurationrecommendation) +- Use that guidance as the reference point for service-level issuer decisions and configuration recommendations. + +## Current approach + +| Area | Current approach in this repo | +|---|---| +| JWT validation | Signature, timestamp, and issuer are all enforced | +| Discovery / JWKS source | `spring.security.oauth2.client.provider.oidc.issuer-uri` | +| Enforced issuer | `oidc.issuer` / `OIDC_ISSUER` | +| Issuer model | Single configured issuer, not allow-list; this is permitted by the HMCTS guidance in the [HMCTS Guidance](#hmcts-guidance) section and matches the one externally agreed issuer for this repo | + +## Context + +- `src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java` builds the decoder from `spring.security.oauth2.client.provider.oidc.issuer-uri`. +- The service separately configures `oidc.issuer` because the discovered issuer is not the value trusted for validation. +- The previous implementation instantiated `JwtIssuerValidator(issuerOverride)` but only applied `JwtTimestampValidator`, which meant an unexpected `iss` claim could still be accepted if signature and timestamps were valid. + +## Implemented fix + +`SecurityConfiguration.jwtDecoder()` now uses: + +```java +OAuth2TokenValidator withTimestamp = new JwtTimestampValidator(); +OAuth2TokenValidator withIssuer = new JwtIssuerValidator(issuerOverride); +OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>(withTimestamp, withIssuer); +``` + +## Runtime model + +| Setting | Purpose | Notes | +|---|---|---| +| `spring.security.oauth2.client.provider.oidc.issuer-uri` | OIDC discovery and JWKS lookup | Built from `IDAM_OIDC_URL` | +| `oidc.issuer` | Enforced token `iss` value | Supplied from `OIDC_ISSUER` | +| `IDAM_OIDC_URL` | Discovery base URL | Not the source of truth for token `iss` | +| `OIDC_ISSUER` | Expected JWT issuer | Must match real caller token `iss` exactly | + +For this repo, the FORGEROCK issuer used in deployed environments is an explicit `OIDC_ISSUER` value supplied by Helm/Jenkins configuration. It is not a runtime fallback that appears automatically when issuer settings are absent. + +## Tests + +| Test | Coverage | +|---|---| +| `src/test/java/uk/gov/hmcts/ccd/SecurityConfigurationTest.java` | Accept expected issuer, reject unexpected issuer, reject expired token | +| `src/test/java/uk/gov/hmcts/ccd/integrations/JwtIssuerValidationIT.java` | Full-stack rejection of a signed JWT whose `iss` does not match configured issuer | + +The test fixtures use valid JWT timelines so failures reflect validator behavior rather than builder constraints. + +Coverage is intentionally two-layered here: validator-only behavior in `SecurityConfigurationTest` and full integration wiring in `JwtIssuerValidationIT`. A lighter Spring web-security slice test was not kept because it introduced unwanted test-context complexity in this repo. + +## Configuration and deployment notes + +Before rollout, confirm: + +- each environment supplies the intended `OIDC_ISSUER` +- the `iss` claim in real caller tokens matches `OIDC_ISSUER` +- no pipeline or release-time override is supplying an older issuer value +- external callers, smoke tests, and AAT clients obtain tokens whose `iss` claim matches this service's configured `OIDC_ISSUER` + +### Guidance alignment + +| Item | Current repo state | +|---|---| +| Service issuer model | Single configured issuer | +| Issuer pattern used for this service | Canonical FORGEROCK issuer pattern, consistent with the HMCTS guidance in the [HMCTS Guidance](#hmcts-guidance) section and the external service issuer policy for `ccd-data-store-api` | +| Repo wiring status | Helm values, preview values, and Jenkins wiring are already aligned to that FORGEROCK issuer pattern | + +Do not infer `OIDC_ISSUER` from the public OIDC discovery URL. In preview/AAT for this repo, the correct +`OIDC_ISSUER` had to be taken from decoded real tokens and did not match the public `IDAM_OIDC_URL` base. + +Smoke and functional pipeline runs now perform a pre-check that acquires a real test token and fails fast if its +`iss` claim does not match `OIDC_ISSUER`. +This verifier is enabled in CI via `VERIFY_OIDC_ISSUER=true` and remains opt-in for local runs. +Because the verifier runs in the build JVM before deployed app env is available, issuer changes may need updating in +both Jenkins test env and Helm app config. + +If external services still send tokens with a different issuer, this change will reject them with `401` until configuration or token issuance is aligned. + +For local running, `IDAM_OIDC_URL` should point to the local OIDC discovery base, usually `http://localhost:5000`, and `OIDC_ISSUER` must exactly match the `iss` claim in the local access tokens being used. Common local values are `OIDC_ISSUER=http://fr-am:8080/openam/oauth2/hmcts` or `OIDC_ISSUER=http://localhost:5000/o`, depending on how the local token source is configured. + +## How to derive `OIDC_ISSUER` + +- Do not guess the issuer from the public discovery URL alone. +- Decode only the JWT payload from a real access token for the target environment and inspect the `iss` claim. +- Do not store or document full bearer tokens. Record only the derived issuer value. + +Example: + +```bash +TOKEN='eyJ...' +PAYLOAD=$(printf '%s' "$TOKEN" | cut -d '.' -f2) +python3 - <<'PY' "$PAYLOAD" +import base64, json, sys +payload = sys.argv[1] +payload += '=' * (-len(payload) % 4) +print(json.loads(base64.urlsafe_b64decode(payload))["iss"]) +PY +``` + +- JWTs are `header.payload.signature`. +- The second segment is base64url-encoded JSON. +- This decodes the payload only. It does not verify the signature. + +## Optional future variant + +Only switch to multi-issuer validation if production tokens genuinely need both values during migration. In that case, use an explicit allow-list for issuer values rather than dropping issuer validation. + +## Current implementation status + +| Area | Current status | +|---|---| +| Decoder / validator chain | `SecurityConfiguration` enforces both timestamp and issuer validation | +| Additional action needed | No further JWT issuer config change is required in this repo unless the external service issuer policy changes | + +## Merge checklist + +Before merging JWT issuer-validation changes, confirm: + +- the active `JwtDecoder` is built from `spring.security.oauth2.client.provider.oidc.issuer-uri` +- the active validator chain includes both `JwtTimestampValidator` and `JwtIssuerValidator(oidc.issuer)` +- there is no disabled, commented-out, or alternate runtime path that leaves issuer validation off +- `issuer-uri` is used for discovery and JWKS lookup only +- `oidc.issuer` / `OIDC_ISSUER` is used as the enforced token `iss` value only +- `OIDC_ISSUER` is explicitly configured and not guessed from the discovery URL +- app config, Helm values, preview values, and CI/Jenkins values are aligned for the target environment +- if `OIDC_ISSUER` changed, it was verified against a real token for the target environment +- there is a test that accepts a token with the expected issuer +- there is a test that rejects a token with an unexpected issuer +- there is a test that rejects an expired token +- there is decoder-level coverage using a signed token, not only validator-only coverage +- CI or build verification checks that a real token issuer matches `OIDC_ISSUER`, or the repo documents why that does not apply +- comments and docs do not describe the old insecure behavior + +Do not merge if: + +- issuer validation is constructed but not applied +- only timestamp validation is active +- `OIDC_ISSUER` was inferred rather than verified +- Helm and CI/Jenkins issuer values disagree without explanation +- only happy-path tests exist + +## Configuration Policy + +| Policy | Requirement | +|---|---| +| Discovery | `spring.security.oauth2.client.provider.oidc.issuer-uri` is used for OIDC discovery and JWKS lookup only | +| Enforcement | `oidc.issuer` / `OIDC_ISSUER` is the enforced JWT issuer and must match the token `iss` claim exactly | +| Derivation | Do not derive `OIDC_ISSUER` from `IDAM_OIDC_URL` or the discovery URL | +| Production-like environments | Must provide `OIDC_ISSUER` explicitly | +| Local / test-only fallbacks | Acceptable only when static, intentional, and clearly scoped to non-production use | +| Build guard | `verifyOidcIssuerPolicy` fails if `oidc.issuer` is derived from discovery config | + +## References + +See [HMCTS Guidance](#hmcts-guidance). diff --git a/docs/skills/security-jwt-issuer/SKILL.md b/docs/skills/security-jwt-issuer/SKILL.md new file mode 100644 index 0000000000..a0952f08ed --- /dev/null +++ b/docs/skills/security-jwt-issuer/SKILL.md @@ -0,0 +1,28 @@ +--- +name: security-jwt-issuer +description: Use for JWT issuer validation, issuer mismatch diagnosis, token iss checks, and pipeline verifier updates in ccd-data-store-api. +--- + +# Security JWT Issuer + +Use this skill when working specifically on JWT issuer validation in `ccd-data-store-api`. + +Read `docs/security/jwt-issuer-validation.md` first for the detailed behavior, config, and rollout guidance. + +## Workflow + +1. Check current diffs with `git status --short` before editing. +2. Review `SecurityConfiguration` and confirm how `IDAM_OIDC_URL` and `OIDC_ISSUER` are used. +3. For code changes, check: + - `src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java` + - `src/test/java/uk/gov/hmcts/ccd/SecurityConfigurationTest.java` + - `src/test/java/uk/gov/hmcts/ccd/integrations/JwtIssuerValidationIT.java` +4. For pipeline/test-run alignment, check: + - `src/aat/java/uk/gov/hmcts/ccd/datastore/befta/JwtIssuerVerificationApp.java` + - `build.gradle` + - `Jenkinsfile_CNP` + - `Jenkinsfile_nightly` +5. For issuer values, token `iss` diagnosis, CI verifier behavior, and Helm vs Jenkins env alignment, follow `docs/security/jwt-issuer-validation.md` rather than duplicating that guidance here. +6. Start verification with the narrowest useful test: + - `./gradlew test --tests uk.gov.hmcts.ccd.SecurityConfigurationTest` +7. Preserve in-flight local work and continue from the existing patch state rather than recreating it. diff --git a/docs/skills/security/SKILL.md b/docs/skills/security/SKILL.md new file mode 100644 index 0000000000..0f5666ee33 --- /dev/null +++ b/docs/skills/security/SKILL.md @@ -0,0 +1,25 @@ +--- +name: security +description: Use when working in the HMCTS `ccd-data-store-api` repository on Spring Security configuration, auth filters, IDAM/OIDC integration, or related regression testing. This skill is for resuming in-flight security patches, checking local diffs, and running focused Gradle tests before and after code changes. +--- + +# Security + +## Overview + +Use this skill for broader security changes in `ccd-data-store-api`, especially around Spring Security wiring, +auth filters, IDAM/OIDC integration, and narrowly scoped regression tests. + +For JWT issuer validation, issuer mismatch diagnosis, token `iss` checks, and pipeline issuer verification, +use `docs/skills/security-jwt-issuer/SKILL.md`. + +## Workflow + +1. Check current state with `git status --short` and inspect local diffs before editing. +2. Review `SecurityConfiguration` together with the auth filters and related properties before changing behavior. +3. Search for relevant security components before editing, for example `SecurityFilterChain`, `ServiceAuthFilter`, + `ExceptionHandlingFilter`, `SecurityLoggingFilter`, and IDAM/OIDC config. +4. Keep JWT issuer-specific changes out of this path unless they are tightly coupled. For issuer work, switch to + `docs/skills/security-jwt-issuer/SKILL.md`. +5. Start verification with the narrowest relevant tests for the touched area. +6. Preserve any in-flight local work and continue from the existing patch state instead of recreating it. diff --git a/lombok.config b/lombok.config index df71bb6a0f..e4e935d15b 100644 --- a/lombok.config +++ b/lombok.config @@ -1,2 +1,3 @@ config.stopBubbling = true lombok.addLombokGeneratedAnnotation = true +lombok.jacksonized.jacksonVersion += 2 diff --git a/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/JwtIssuerVerificationApp.java b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/JwtIssuerVerificationApp.java new file mode 100644 index 0000000000..6b196a7050 --- /dev/null +++ b/src/aat/java/uk/gov/hmcts/ccd/datastore/befta/JwtIssuerVerificationApp.java @@ -0,0 +1,75 @@ +package uk.gov.hmcts.ccd.datastore.befta; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import uk.gov.hmcts.ccd.datastore.tests.Env; +import uk.gov.hmcts.ccd.datastore.tests.helper.idam.IdamHelper; +import uk.gov.hmcts.ccd.datastore.tests.helper.idam.OAuth2; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public final class JwtIssuerVerificationApp { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private JwtIssuerVerificationApp() { + } + + public static void main(String[] args) throws Exception { + String expectedIssuer = Env.require("OIDC_ISSUER"); + String idamBaseUrl = Env.require("IDAM_API_URL_BASE"); + String[] credentials = firstAvailableCredentials( + "CCD_CASEWORKER_AUTOTEST_EMAIL", "CCD_CASEWORKER_AUTOTEST_PASSWORD", + "DEFINITION_IMPORTER_USERNAME", "DEFINITION_IMPORTER_PASSWORD" + ); + + IdamHelper idamHelper = new IdamHelper(idamBaseUrl, OAuth2.INSTANCE); + String accessToken = idamHelper.getIdamOauth2Token(credentials[0], credentials[1]); + String actualIssuer = decodeIssuer(accessToken); + + if (!expectedIssuer.equals(actualIssuer)) { + throw new IllegalStateException( + "OIDC_ISSUER mismatch: expected `" + expectedIssuer + "` but token iss was `" + actualIssuer + "`" + ); + } + + System.out.println("Verified OIDC_ISSUER matches functional test token iss: " + actualIssuer); + } + + private static String[] firstAvailableCredentials(String... envNames) { + for (int i = 0; i < envNames.length; i += 2) { + String username = System.getenv(envNames[i]); + String password = System.getenv(envNames[i + 1]); + if (username != null && password != null) { + return new String[]{username, password}; + } + } + + throw new IllegalStateException( + "No credentials available for JWT issuer verification. " + + "Expected one of: CCD_CASEWORKER_AUTOTEST_EMAIL/PASSWORD or " + + "DEFINITION_IMPORTER_USERNAME/PASSWORD" + ); + } + + private static String decodeIssuer(String accessToken) throws Exception { + String[] parts = accessToken.split("\\."); + if (parts.length < 2) { + throw new IllegalStateException("Access token is not a JWT"); + } + + byte[] decodedPayload = Base64.getUrlDecoder().decode(padBase64(parts[1])); + JsonNode payload = OBJECT_MAPPER.readTree(new String(decodedPayload, StandardCharsets.UTF_8)); + JsonNode issuer = payload.get("iss"); + if (issuer == null || issuer.isNull()) { + throw new IllegalStateException("Access token does not contain an iss claim"); + } + return issuer.asText(); + } + + private static String padBase64(String value) { + int remainder = value.length() % 4; + return remainder == 0 ? value : value + "=".repeat(4 - remainder); + } +} diff --git a/src/contractTest/resources/application.properties b/src/contractTest/resources/application.properties index 064ab7e8e2..c10f579b7a 100644 --- a/src/contractTest/resources/application.properties +++ b/src/contractTest/resources/application.properties @@ -77,7 +77,7 @@ spring.security.oauth2.client.provider.oidc.issuer-uri = ${idam.api.url}/o # Dummy oidc client required even though data-store doesn't use spring.security.oauth2.client.registration.oidc.client-id = internal spring.security.oauth2.client.registration.oidc.client-secret = internal -oidc.issuer = ${OIDC_ISSUER:http://fr-am:8080/openam/oauth2/hmcts} +oidc.issuer = ${OIDC_ISSUER:http://localhost:5000/o} # Required for the ServiceAuthorisationApi class in service-auth-provider-java-client library idam.s2s-auth.totp_secret=${DATA_STORE_IDAM_KEY:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB} diff --git a/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java b/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java index 3754275e36..53d53eac8d 100644 --- a/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java +++ b/src/main/java/uk/gov/hmcts/ccd/SecurityConfiguration.java @@ -103,7 +103,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .csrf(csrf -> csrf.disable()) // NOSONAR - CSRF is disabled purposely .formLogin(fl -> fl.disable()) .logout(logout -> logout.disable()) - .authorizeHttpRequests(auth -> + .authorizeHttpRequests(auth -> auth.requestMatchers("/error") .permitAll() .anyRequest() @@ -117,12 +117,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)JwtDecoders.fromOidcIssuerLocation(issuerUri); - // We are using issuerOverride instead of issuerUri as SIDAM has the wrong issuer at the moment + // See docs/security/jwt-issuer-validation.md for issuer-uri discovery and oidc.issuer enforcement. OAuth2TokenValidator withTimestamp = new JwtTimestampValidator(); OAuth2TokenValidator withIssuer = new JwtIssuerValidator(issuerOverride); - // FIXME : enable `withIssuer` once idam migration done RDM-8094 - // OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>(withTimestamp, withIssuer); - OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>(withTimestamp); + OAuth2TokenValidator validator = new DelegatingOAuth2TokenValidator<>(withTimestamp, withIssuer); jwtDecoder.setJwtValidator(validator); return jwtDecoder; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d597782b5a..d9cddb1225 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -94,7 +94,7 @@ spring.security.oauth2.client.provider.oidc.issuer-uri = ${IDAM_OIDC_URL:http:// # Dummy oidc client required even though data-store doesn't use spring.security.oauth2.client.registration.oidc.client-id = internal spring.security.oauth2.client.registration.oidc.client-secret = internal -oidc.issuer = ${OIDC_ISSUER:http://fr-am:8080/openam/oauth2/hmcts} +oidc.issuer = ${OIDC_ISSUER} # Required for the ServiceAuthorisationApi class in service-auth-provider-java-client library idam.s2s-auth.totp_secret=${DATA_STORE_IDAM_KEY:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB} diff --git a/src/test/java/uk/gov/hmcts/ccd/SecurityConfigurationTest.java b/src/test/java/uk/gov/hmcts/ccd/SecurityConfigurationTest.java new file mode 100644 index 0000000000..9bc06abdbf --- /dev/null +++ b/src/test/java/uk/gov/hmcts/ccd/SecurityConfigurationTest.java @@ -0,0 +1,115 @@ +package uk.gov.hmcts.ccd; + +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; +import org.springframework.security.oauth2.core.OAuth2TokenValidator; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtIssuerValidator; + +import org.springframework.security.oauth2.jwt.JwtValidationException; +import org.springframework.security.oauth2.jwt.JwtTimestampValidator; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static uk.gov.hmcts.ccd.util.KeyGenerator.getRsaJWK; + +// Validator-level coverage for issuer and timestamp enforcement. +class SecurityConfigurationTest { + + private static final String VALID_ISSUER = "http://localhost:5000/o"; + private static final String INVALID_ISSUER = "http://unexpected-issuer"; + + @Test + void shouldAcceptJwtFromConfiguredIssuer() { + Instant now = Instant.now(); + assertFalse( + validator().validate(buildJwt(VALID_ISSUER, now.minusSeconds(60), now.plusSeconds(300))).hasErrors() + ); + } + + @Test + void shouldRejectJwtFromUnexpectedIssuer() { + Instant now = Instant.now(); + assertTrue( + validator().validate(buildJwt(INVALID_ISSUER, now.minusSeconds(60), now.plusSeconds(300))).hasErrors() + ); + } + + @Test + void shouldRejectDecodedJwtFromUnexpectedIssClaim() throws JOSEException, ParseException { + JwtValidationException exception = assertThrows( + JwtValidationException.class, + () -> decoder().decode(signedJwt(INVALID_ISSUER)) + ); + + assertThat(exception.getMessage()).contains("iss"); + } + + @Test + void shouldRejectExpiredJwtEvenWhenIssuerMatches() { + Instant now = Instant.now(); + // Keep expiry clearly outside the default clock-skew allowance to avoid boundary flakiness. + assertTrue( + validator().validate(buildJwt(VALID_ISSUER, now.minusSeconds(300), now.minusSeconds(121))).hasErrors() + ); + } + + private OAuth2TokenValidator validator() { + return new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(), + new JwtIssuerValidator(VALID_ISSUER) + ); + } + + private NimbusJwtDecoder decoder() throws JOSEException { + NimbusJwtDecoder decoder = NimbusJwtDecoder.withPublicKey(getRsaJWK().toRSAPublicKey()).build(); + decoder.setJwtValidator(validator()); + return decoder; + } + + private Jwt buildJwt(String issuer, Instant issuedAt, Instant expiresAt) { + return Jwt.withTokenValue("token") + .header("alg", "RS256") + .issuer(issuer) + .subject("user") + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .build(); + } + + private String signedJwt(String issuer) throws JOSEException, ParseException { + Instant now = Instant.now(); + + SignedJWT signedJwt = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(getRsaJWK().getKeyID()) + .build(), + new JWTClaimsSet.Builder() + .jwtID(UUID.randomUUID().toString()) + .issuer(issuer) + .subject("user") + .issueTime(Date.from(now.minusSeconds(60))) + .expirationTime(Date.from(now.plusSeconds(300))) + .build() + ); + signedJwt.sign(new RSASSASigner(getRsaJWK().toPrivateKey())); + return signedJwt.serialize(); + } +} diff --git a/src/test/java/uk/gov/hmcts/ccd/WireMockBaseTest.java b/src/test/java/uk/gov/hmcts/ccd/WireMockBaseTest.java index 34f28c3ef4..55de5b5135 100644 --- a/src/test/java/uk/gov/hmcts/ccd/WireMockBaseTest.java +++ b/src/test/java/uk/gov/hmcts/ccd/WireMockBaseTest.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.util.ReflectionTestUtils; import uk.gov.hmcts.ccd.feign.FeignClientConfig; @@ -45,6 +46,7 @@ @AutoConfigureWireMock(port = 0) @Import({FeignClientConfig.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public abstract class WireMockBaseTest extends AbstractBaseIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(WireMockBaseTest.class); diff --git a/src/test/java/uk/gov/hmcts/ccd/domain/service/caselinking/CaseLinkServiceExternalDbDeadlockIT.java b/src/test/java/uk/gov/hmcts/ccd/domain/service/caselinking/CaseLinkServiceExternalDbDeadlockIT.java index f78ebe6fb0..a7a79e72a7 100644 --- a/src/test/java/uk/gov/hmcts/ccd/domain/service/caselinking/CaseLinkServiceExternalDbDeadlockIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/domain/service/caselinking/CaseLinkServiceExternalDbDeadlockIT.java @@ -58,7 +58,8 @@ "spring.datasource.hikari.minimum-idle=2", "spring.jpa.hibernate.ddl-auto=none", "spring.flyway.enabled=false", - "spring.security.oauth2.client.provider.oidc.issuer-uri=${IDAM_OIDC_URL:http://localhost:5000/o}" + "spring.security.oauth2.client.provider.oidc.issuer-uri=${IDAM_OIDC_URL:http://localhost:5000/o}", + "oidc.issuer=${OIDC_ISSUER:http://localhost:5000/o}" }) class CaseLinkServiceExternalDbDeadlockIT { diff --git a/src/test/java/uk/gov/hmcts/ccd/integrations/DefinitionsCachingIT.java b/src/test/java/uk/gov/hmcts/ccd/integrations/DefinitionsCachingIT.java index e4e684f0d6..b7ee5cb4f6 100644 --- a/src/test/java/uk/gov/hmcts/ccd/integrations/DefinitionsCachingIT.java +++ b/src/test/java/uk/gov/hmcts/ccd/integrations/DefinitionsCachingIT.java @@ -50,6 +50,7 @@ @ExtendWith(SpringExtension.class) @SpringBootTest @AutoConfigureWireMock(port = 0) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @TestPropertySource(locations = "classpath:test.properties") public class DefinitionsCachingIT { diff --git a/src/test/java/uk/gov/hmcts/ccd/integrations/JwtIssuerValidationIT.java b/src/test/java/uk/gov/hmcts/ccd/integrations/JwtIssuerValidationIT.java new file mode 100644 index 0000000000..0bbd7d6740 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/ccd/integrations/JwtIssuerValidationIT.java @@ -0,0 +1,81 @@ +package uk.gov.hmcts.ccd.integrations; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import uk.gov.hmcts.ccd.WireMockBaseTest; + +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static uk.gov.hmcts.ccd.util.KeyGenerator.getRsaJWK; + +// Full integration coverage for issuer rejection through the real app and OIDC/JWKS test wiring. +class JwtIssuerValidationIT extends WireMockBaseTest { + + private static final String INVALID_ISSUER = "http://unexpected-issuer"; + private static final String CASE_URL = + "/caseworkers/123/jurisdictions/TEST/case-types/TestAddressBook/cases/1234123412341238"; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void shouldRejectJwtWhenIssuerDoesNotMatchConfiguredIssuer() throws JOSEException, ParseException { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + signedJwt(INVALID_ISSUER)); + headers.add("ServiceAuthorization", "ServiceToken"); + headers.add(HttpHeaders.CONTENT_TYPE, "application/json"); + + ResponseEntity response = restTemplate.exchange( + CASE_URL, + HttpMethod.GET, + new HttpEntity<>(headers), + String.class + ); + + // This integration harness currently surfaces the rejected JWT as 403, although deployed runtime + // invalid-issuer responses are expected to return 401 with invalid_token details. + assertThat(response.getStatusCode().value()).isEqualTo(403); + WireMock.verify(1, getRequestedFor(urlEqualTo("/s2s/details"))); + WireMock.verify(0, getRequestedFor(urlEqualTo("/o/userinfo"))); + } + + private String signedJwt(String issuer) throws JOSEException, ParseException { + Instant now = Instant.now(); + + SignedJWT signedJwt = new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(JOSEObjectType.JWT) + .keyID(getRsaJWK().getKeyID()) + .build(), + new JWTClaimsSet.Builder() + .jwtID(UUID.randomUUID().toString()) + .issuer(issuer) + .subject("123") + .claim("tokenName", "access_token") + .issueTime(Date.from(now.minusSeconds(60))) + .expirationTime(Date.from(now.plusSeconds(300))) + .build() + ); + signedJwt.sign(new RSASSASigner(getRsaJWK().toPrivateKey())); + return signedJwt.serialize(); + } +} diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties index 9e0d588bc6..928796d1be 100644 --- a/src/test/resources/test.properties +++ b/src/test/resources/test.properties @@ -13,6 +13,7 @@ idam.api.url=http://localhost:${wiremock.server.port:5000} idam.s2s-auth.url=http://localhost:${wiremock.server.port:4502}/s2s spring.security.oauth2.client.provider.oidc.issuer-uri=${idam.api.url}/o +oidc.issuer=${OIDC_ISSUER:http://localhost:${wiremock.server.port:5000}/o} spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.url=jdbc:tc:postgresql:15:///localhost?stringtype=unspecified