From 68341a36744eb046454820d8f2533f5e2378fec1 Mon Sep 17 00:00:00 2001 From: Antony Leons Date: Thu, 16 Apr 2026 14:04:40 +0100 Subject: [PATCH 1/5] CME-971: Add redirect URI allowlist support for OAuth2 authentication --- config/custom-environment-variables.yaml | 1 + config/default.yaml | 1 + src/main/oauth2/access-token-request.ts | 25 ++++++++++-- src/test/oauth2/access-token-request.spec.ts | 42 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/config/custom-environment-variables.yaml b/config/custom-environment-variables.yaml index 2b4dcae30..8ba14283d 100644 --- a/config/custom-environment-variables.yaml +++ b/config/custom-environment-variables.yaml @@ -14,6 +14,7 @@ idam: token_endpoint: IDAM_OAUTH2_TOKEN_ENDPOINT logout_endpoint: IDAM_OAUTH2_LOGOUT_ENDPOINT client_id: IDAM_OAUTH2_CLIENT_ID + redirect_uri_allowlist: IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST adminWeb: login_url: ADMINWEB_LOGIN_URL import_url: ADMINWEB_IMPORT_URL diff --git a/config/default.yaml b/config/default.yaml index 8c11b47e7..f5fba68a7 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -23,6 +23,7 @@ idam: token_endpoint: http://localhost:5000/oauth2/token logout_endpoint: http://localhost:5000/session/:token client_id: ccd_admin + redirect_uri_allowlist: "localhost,127.0.0.1" adminWeb: login_url: https://localhost:3501/login import_url: http://localhost:4451/import diff --git a/src/main/oauth2/access-token-request.ts b/src/main/oauth2/access-token-request.ts index 1a1060ba6..e3a0998e4 100644 --- a/src/main/oauth2/access-token-request.ts +++ b/src/main/oauth2/access-token-request.ts @@ -1,13 +1,30 @@ import * as fetch from "node-fetch"; -import { format } from "url"; +import { format, URL } from "url"; import { get } from "config"; import { Logger } from "@hmcts/nodejs-logging"; +const ERROR_INVALID_REDIRECT_URI = { + code: "INVALID_REDIRECT_URI", + error: "Bad Request", + message: "Redirect URI is not permitted", + status: 400, +}; + const completeRedirectURI = (uri) => { - if (!uri.startsWith("http")) { - return `https://${uri}`; + let parsedUrl; + try { + const fullUri = uri.startsWith("http") ? uri : `https://${uri}`; + parsedUrl = new URL(fullUri); + } catch (e) { + throw ERROR_INVALID_REDIRECT_URI; + } + const allowedHosts = (get("idam.oauth2.redirect_uri_allowlist") as string) + .split(",") + .map((h) => h.trim()); + if (allowedHosts.indexOf(parsedUrl.hostname) === -1) { + throw ERROR_INVALID_REDIRECT_URI; } - return uri; + return parsedUrl.href; }; export function accessTokenRequest(request) { diff --git a/src/test/oauth2/access-token-request.spec.ts b/src/test/oauth2/access-token-request.spec.ts index 93d0bf542..5ef4cfaba 100644 --- a/src/test/oauth2/access-token-request.spec.ts +++ b/src/test/oauth2/access-token-request.spec.ts @@ -15,7 +15,10 @@ describe("Access Token Request", () => { const TOKEN_ENDPOINT = "http://localhost:1234/oauth2/token"; const REDIRECT_URN = "localhost/redirect/to"; const REDIRECT_URL = "https://localhost/redirect/to"; + const REDIRECT_URL_WITH_PORT = "https://localhost:3501/redirect/to"; + const DISALLOWED_URI = "https://attacker.com/steal"; const AUTH_CODE = "xyz789"; + const REDIRECT_ALLOWLIST = "localhost,127.0.0.1"; const REQUEST = sinonExpressMock.mockReq({ query: { @@ -29,6 +32,12 @@ describe("Access Token Request", () => { redirect_uri: REDIRECT_URL, }, }); + const REQUEST_WITH_PORT = sinonExpressMock.mockReq({ + query: { + code: AUTH_CODE, + redirect_uri: REDIRECT_URL_WITH_PORT, + }, + }); const RESPONSE = { body: { access_token: "q1w2e3r4t5y6", @@ -46,6 +55,7 @@ describe("Access Token Request", () => { config = { get: sinon.stub(), }; + config.get.withArgs("idam.oauth2.redirect_uri_allowlist").returns(REDIRECT_ALLOWLIST); fetch = fetchMock.sandbox().post(`begin:${TOKEN_ENDPOINT}`, RESPONSE); accessTokenRequest = proxyquire("../../main/oauth2/access-token-request", { @@ -89,4 +99,36 @@ describe("Access Token Request", () => { }) .catch((error) => done(new Error(error))); }); + + it("should allow redirect URIs with an allowed hostname and port", (done) => { + config.get.withArgs("idam.oauth2.client_id").returns(CLIENT_ID); + config.get.withArgs("secrets.ccd.ccd-admin-web-oauth2-client-secret").returns(CLIENT_SECRET); + config.get.withArgs("idam.oauth2.token_endpoint").returns(TOKEN_ENDPOINT); + + accessTokenRequest(REQUEST_WITH_PORT) + .then(() => { + expect(fetch.called()).to.be.true; + const requestedUrl = url.parse(fetch.lastUrl(), true); + expect(requestedUrl.query.redirect_uri).to.equal(REDIRECT_URL_WITH_PORT); + done(); + }) + .catch((error) => done(new Error(error))); + }); + + it("should reject redirect URIs with disallowed hosts", async () => { + config.get.withArgs("idam.oauth2.client_id").returns(CLIENT_ID); + config.get.withArgs("secrets.ccd.ccd-admin-web-oauth2-client-secret").returns(CLIENT_SECRET); + config.get.withArgs("idam.oauth2.token_endpoint").returns(TOKEN_ENDPOINT); + const REQUEST_DISALLOWED = sinonExpressMock.mockReq({ + query: { code: AUTH_CODE, redirect_uri: DISALLOWED_URI }, + }); + try { + await accessTokenRequest(REQUEST_DISALLOWED); + expect.fail("Expected error to be thrown"); + } catch (error) { + expect(error.status).to.equal(400); + expect(error.error).to.equal("Bad Request"); + expect(error.message).to.equal("Redirect URI is not permitted"); + } + }); }); From 7be1eb8e2db34e3c707c20ed3e429a54da61c5a7 Mon Sep 17 00:00:00 2001 From: Antony Leons Date: Wed, 29 Apr 2026 14:47:27 +0100 Subject: [PATCH 2/5] Enhance error handling in completeRedirectURI function and update yarn audit known issues --- src/main/oauth2/access-token-request.ts | 5 +++-- yarn-audit-known-issues | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/oauth2/access-token-request.ts b/src/main/oauth2/access-token-request.ts index e3a0998e4..30ccbd01a 100644 --- a/src/main/oauth2/access-token-request.ts +++ b/src/main/oauth2/access-token-request.ts @@ -16,12 +16,13 @@ const completeRedirectURI = (uri) => { const fullUri = uri.startsWith("http") ? uri : `https://${uri}`; parsedUrl = new URL(fullUri); } catch (e) { - throw ERROR_INVALID_REDIRECT_URI; + const error = Object.assign({}, ERROR_INVALID_REDIRECT_URI, { cause: e }); + throw error; } const allowedHosts = (get("idam.oauth2.redirect_uri_allowlist") as string) .split(",") .map((h) => h.trim()); - if (allowedHosts.indexOf(parsedUrl.hostname) === -1) { + if (!allowedHosts.includes(parsedUrl.hostname)) { throw ERROR_INVALID_REDIRECT_URI; } return parsedUrl.href; diff --git a/yarn-audit-known-issues b/yarn-audit-known-issues index 8450fbc8a..ccd4a0303 100644 --- a/yarn-audit-known-issues +++ b/yarn-audit-known-issues @@ -8,4 +8,6 @@ {"value":"govuk_template_mustache","children":{"ID":"govuk_template_mustache (deprecation)","Issue":"GOV.UK Template is no longer maintained. Use the GOV.UK Design System instead: https://frontend.design-system.service.gov.uk/v4/migrating-from-legacy-products/","Severity":"moderate","Vulnerable Versions":"0.26.0","Tree Versions":["0.26.0"],"Dependents":["ccd-admin-web@workspace:."]}} {"value":"lodash.isequal","children":{"ID":"lodash.isequal (deprecation)","Issue":"This package is deprecated. Use require('node:util').isDeepStrictEqual instead.","Severity":"moderate","Vulnerable Versions":"4.5.0","Tree Versions":["4.5.0"],"Dependents":["@fast-csv/format@npm:4.3.5"]}} {"value":"multer","children":{"ID":"multer (deprecation)","Issue":"Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.","Severity":"moderate","Vulnerable Versions":"1.4.5-lts.2","Tree Versions":["1.4.5-lts.2"],"Dependents":["ccd-admin-web@workspace:."]}} +{"value":"protobufjs","children":{"ID":1117042,"Issue":"Arbitrary code execution in protobufjs","URL":"https://github.com/advisories/GHSA-xq3m-2v4x-88gg","Severity":"critical","Vulnerable Versions":"<7.5.5","Tree Versions":["7.5.4"],"Dependents":["@opentelemetry/otlp-transformer@virtual:e5e738d5f8fd0ff82fd4a132fa0a52b7cd5ef9973a73f79167e24d95e4e4fcd3d3eec44802acf4c68bc02a289d56ee19dbc37a8dae5b1c48fa8b5380189af860#npm:0.208.0"]}} +{"value":"uuid","children":{"ID":1116970,"Issue":"uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided","URL":"https://github.com/advisories/GHSA-w5hq-g745-h8pq","Severity":"moderate","Vulnerable Versions":"<14.0.0","Tree Versions":["8.3.2"],"Dependents":["@azure/functions@npm:3.5.1"]}} {"value":"whatwg-encoding","children":{"ID":"whatwg-encoding (deprecation)","Issue":"Use @exodus/bytes instead for a more spec-conformant and faster implementation","Severity":"moderate","Vulnerable Versions":"2.0.0","Tree Versions":["2.0.0"],"Dependents":["jsdom@virtual:765dd21400b9887d1cda8410e14996ece3abd2d473a1afb27695f43d295da769ea8bf3ebcf77d15b6687aeeeff789a6f299e6aeede434e237808bef39343fe75#npm:20.0.3"]}} From f55cda2239b7b2349ed60cd24071bb96644f32b7 Mon Sep 17 00:00:00 2001 From: Antony Leons Date: Thu, 30 Apr 2026 09:05:17 +0100 Subject: [PATCH 3/5] Add IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST to environment variables in values files --- charts/ccd-admin-web/values.aat.template.yaml | 4 +++- charts/ccd-admin-web/values.preview.template.yaml | 1 + charts/ccd-admin-web/values.yaml | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/charts/ccd-admin-web/values.aat.template.yaml b/charts/ccd-admin-web/values.aat.template.yaml index fdcf5f66d..217052e86 100644 --- a/charts/ccd-admin-web/values.aat.template.yaml +++ b/charts/ccd-admin-web/values.aat.template.yaml @@ -1,3 +1,5 @@ nodejs: image: ${IMAGE_NAME} - ingressHost: ${SERVICE_FQDN} \ No newline at end of file + ingressHost: ${SERVICE_FQDN} + environment: + IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST: "${SERVICE_FQDN}" \ No newline at end of file diff --git a/charts/ccd-admin-web/values.preview.template.yaml b/charts/ccd-admin-web/values.preview.template.yaml index 6d155517a..407f14a70 100644 --- a/charts/ccd-admin-web/values.preview.template.yaml +++ b/charts/ccd-admin-web/values.preview.template.yaml @@ -2,6 +2,7 @@ nodejs: image: ${IMAGE_NAME} ingressHost: ${SERVICE_FQDN} environment: + IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST: "${SERVICE_FQDN}" ADMINWEB_LOGIN_URL: https://idam-web-public.aat.platform.hmcts.net ADMINWEB_IMPORT_URL: http://ccd-definition-store-api-aat.service.core-compute-aat.internal/import ADMINWEB_JURISDICTIONS_URL: http://ccd-definition-store-api-aat.service.core-compute-aat.internal/api/data/jurisdictions diff --git a/charts/ccd-admin-web/values.yaml b/charts/ccd-admin-web/values.yaml index 159761f42..ca0f36025 100644 --- a/charts/ccd-admin-web/values.yaml +++ b/charts/ccd-admin-web/values.yaml @@ -30,6 +30,7 @@ nodejs: IDAM_BASE_URL: https://idam-api.{{ .Values.global.environment }}.platform.hmcts.net IDAM_OAUTH2_TOKEN_ENDPOINT: https://idam-api.{{ .Values.global.environment }}.platform.hmcts.net/oauth2/token IDAM_OAUTH2_LOGOUT_ENDPOINT: https://idam-api.{{ .Values.global.environment }}.platform.hmcts.net/session/:token + IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST: "ccd-admin-web.{{ .Values.global.environment }}.platform.hmcts.net" ADMINWEB_LOGIN_URL: "https://idam-web-public.{{ .Values.global.environment }}.platform.hmcts.net/login" ADMINWEB_IMPORT_URL: "http://ccd-definition-store-api-{{ .Values.global.environment }}.service.core-compute-{{ .Values.global.environment }}.internal/import" From f2d65cc3b73aed193bdad69734a29e2755f6db6d Mon Sep 17 00:00:00 2001 From: hmcts-jenkins-a-to-c <62422075+hmcts-jenkins-a-to-c[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 08:43:40 +0000 Subject: [PATCH 4/5] Bumping chart version/ fixing aliases --- charts/ccd-admin-web/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/ccd-admin-web/Chart.yaml b/charts/ccd-admin-web/Chart.yaml index 57c34079b..164088319 100644 --- a/charts/ccd-admin-web/Chart.yaml +++ b/charts/ccd-admin-web/Chart.yaml @@ -2,7 +2,7 @@ description: Helm chart for the HMCTS CCD Admin Web name: ccd-admin-web apiVersion: v2 home: https://github.com/hmcts/ccd-admin-web -version: 2.2.14 +version: 2.2.15 maintainers: - name: HMCTS CCD Dev Team email: ccd-devops@HMCTS.NET From 351c5b839fa37ffd1c808e3934a5b56cf9b38cdb Mon Sep 17 00:00:00 2001 From: hmcts-jenkins-a-to-c <62422075+hmcts-jenkins-a-to-c[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 16:13:29 +0000 Subject: [PATCH 5/5] Bumping chart version/ fixing aliases --- charts/ccd-admin-web/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/ccd-admin-web/Chart.yaml b/charts/ccd-admin-web/Chart.yaml index 164088319..88c447454 100644 --- a/charts/ccd-admin-web/Chart.yaml +++ b/charts/ccd-admin-web/Chart.yaml @@ -2,7 +2,7 @@ description: Helm chart for the HMCTS CCD Admin Web name: ccd-admin-web apiVersion: v2 home: https://github.com/hmcts/ccd-admin-web -version: 2.2.15 +version: 2.2.16 maintainers: - name: HMCTS CCD Dev Team email: ccd-devops@HMCTS.NET