diff --git a/app/oauth2/access-token-request.js b/app/oauth2/access-token-request.js index ba15d71f0..a4e37fc24 100644 --- a/app/oauth2/access-token-request.js +++ b/app/oauth2/access-token-request.js @@ -5,22 +5,31 @@ const { Logger } = require('@hmcts/nodejs-logging'); const logger = Logger.getLogger('accessTokenRequest'); -const completeRedirectURI = (uri) => { - if (uri.startsWith('undefined')){ - throw ERROR_INVALID_REDIRECT_URI; - } else if (!uri.startsWith('http')) { - return `https://${uri}`; - } - return uri; -}; - const ERROR_INVALID_REDIRECT_URI = { code: 'INVALID_REDIRECT_URI', error: 'Bad Request', - message: 'Redirect URI cannot start with undefined', + message: 'Redirect URI is not permitted', status: 400 }; +const completeRedirectURI = (uri) => { + let parsedUrl; + try { + const fullUri = uri.startsWith('http') ? uri : `https://${uri}`; + parsedUrl = new URL(fullUri); + } catch (e) { + logger.error('Invalid redirect URI:', e.message); + throw ERROR_INVALID_REDIRECT_URI; + } + const allowedHosts = config.get('idam.oauth2.redirect_uri_allowlist') + .split(',') + .map(h => h.trim()); + if (!allowedHosts.includes(parsedUrl.hostname)) { + throw ERROR_INVALID_REDIRECT_URI; + } + return parsedUrl.href; +}; + function accessTokenRequest(request) { const options = { method: 'POST', diff --git a/charts/ccd-api-gateway-web/Chart.yaml b/charts/ccd-api-gateway-web/Chart.yaml index 9d71f6852..c925debe0 100644 --- a/charts/ccd-api-gateway-web/Chart.yaml +++ b/charts/ccd-api-gateway-web/Chart.yaml @@ -2,7 +2,7 @@ description: Helm chart for the HMCTS CCD API Gateway apiVersion: v2 name: ccd-api-gateway-web home: https://github.com/hmcts/ccd-api-gateway -version: 1.2.11 +version: 1.2.12 maintainers: - name: HMCTS CCD Dev Team email: ccd-devops@HMCTS.NET diff --git a/charts/ccd-api-gateway-web/values.aat.template.yaml b/charts/ccd-api-gateway-web/values.aat.template.yaml index f22bd538f..e4e07da66 100644 --- a/charts/ccd-api-gateway-web/values.aat.template.yaml +++ b/charts/ccd-api-gateway-web/values.aat.template.yaml @@ -2,4 +2,5 @@ nodejs: image: ${IMAGE_NAME} ingressHost: ${SERVICE_FQDN} environment: + IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST: "${SERVICE_FQDN}" CORS_ORIGIN_WHITELIST: "*" diff --git a/charts/ccd-api-gateway-web/values.preview.template.yaml b/charts/ccd-api-gateway-web/values.preview.template.yaml index 96fb639f3..82a47f029 100644 --- a/charts/ccd-api-gateway-web/values.preview.template.yaml +++ b/charts/ccd-api-gateway-web/values.preview.template.yaml @@ -2,6 +2,7 @@ nodejs: image: ${IMAGE_NAME} ingressHost: ${SERVICE_FQDN} environment: + IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST: "${SERVICE_FQDN}" IDAM_OAUTH2_TOKEN_ENDPOINT: https://idam-api.aat.platform.hmcts.net/oauth2/token IDAM_OAUTH2_LOGOUT_ENDPOINT: https://idam-api.aat.platform.hmcts.net/session/:token IDAM_BASE_URL: https://idam-api.aat.platform.hmcts.net diff --git a/charts/ccd-api-gateway-web/values.yaml b/charts/ccd-api-gateway-web/values.yaml index c61ea1c25..76bbf6b76 100644 --- a/charts/ccd-api-gateway-web/values.yaml +++ b/charts/ccd-api-gateway-web/values.yaml @@ -10,6 +10,7 @@ nodejs: minReplicas: 8 environment: IDAM_OAUTH2_CLIENT_ID: ccd_gateway + IDAM_OAUTH2_REDIRECT_URI_ALLOWLIST: "gateway-ccd.{{ .Values.global.environment }}.platform.hmcts.net" CORS_ORIGIN_METHODS: GET,POST,OPTIONS,PUT,DELETE IDAM_SERVICE_NAME: ccd_gw SECURE_AUTH_COOKIE_ENABLED: true diff --git a/config/custom-environment-variables.yaml b/config/custom-environment-variables.yaml index a79ad84dd..41f7eed1c 100644 --- a/config/custom-environment-variables.yaml +++ b/config/custom-environment-variables.yaml @@ -17,6 +17,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 address_lookup: detect_proxy: ADDRESS_LOOKUP_DETECT_PROXY url: ADDRESS_LOOKUP_URL diff --git a/config/default.yaml b/config/default.yaml index 4099e42a3..15eab3798 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -17,6 +17,7 @@ idam: token_endpoint: http://localhost:5000/oauth2/token logout_endpoint: http://localhost:5000/session/:token client_id: ccd_gateway + redirect_uri_allowlist: "localhost,127.0.0.1" address_lookup: detect_proxy: false url: https://api.os.uk/search/places/v1/postcode?postcode=${postcode}&key=${key} diff --git a/config/test.yaml b/config/test.yaml index 4c179b46b..3cd62231c 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -2,6 +2,8 @@ appInsights: enabled: false idam: base_url: http://test-idam:1234 + oauth2: + redirect_uri_allowlist: "localhost" secrets: ccd: ccd-api-gateway-oauth2-client-secret: ccd_gateway_secret diff --git a/test/oauth2/access-token-request.spec.js b/test/oauth2/access-token-request.spec.js index 84c805390..07576d23e 100644 --- a/test/oauth2/access-token-request.spec.js +++ b/test/oauth2/access-token-request.spec.js @@ -15,7 +15,9 @@ describe('Access Token Request', () => { const REDIRECT_URN = 'localhost/redirect/to'; const REDIRECT_URL = 'https://localhost/redirect/to'; const UNDEFINED_URI = 'undefined:///oauth2redirect'; + const DISALLOWED_URI = 'https://attacker.com/steal'; const AUTH_CODE = 'xyz789'; + const REDIRECT_ALLOWLIST = 'localhost'; const REQUEST = sinonExpressMock.mockReq({ query: { @@ -60,6 +62,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}`, SUCCESSFUL_RESPONSE); accessTokenRequest = proxyquire('../../app/oauth2/access-token-request', { @@ -134,10 +137,28 @@ describe('Access Token Request', () => { config.get.withArgs('idam.oauth2.token_endpoint').returns(TOKEN_ENDPOINT); try { await accessTokenRequest(REQUEST_UNDEFINED_URI); + expect.fail('Expected error to be thrown'); } catch (error) { expect(error.status).to.deep.equal(400); expect(error.error).to.deep.equal('Bad Request'); - expect(error.message).to.deep.equal('Redirect URI cannot start with undefined'); + expect(error.message).to.deep.equal('Redirect URI is not permitted'); + } + }); + + 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-api-gateway-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.deep.equal(400); + expect(error.error).to.deep.equal('Bad Request'); + expect(error.message).to.deep.equal('Redirect URI is not permitted'); } }); });