From d28bb17035e1c27beeb54f40ff9894fc5cc41f77 Mon Sep 17 00:00:00 2001 From: Craig Earl Date: Wed, 8 Jan 2025 14:10:16 +0000 Subject: [PATCH] ATO-1291: Update controller to support request objects Uses new validation in controller, which enables support for request objects. Also throws MethodNotAllowedError for non GET requests --- src/app.ts | 4 +- .../authorise/authorise-get-controller.ts | 75 +- src/errors/method-not-allowed-error.ts | 5 + .../authorise-get-controller.test.ts | 1638 ++++++++++++----- 4 files changed, 1256 insertions(+), 466 deletions(-) create mode 100644 src/errors/method-not-allowed-error.ts diff --git a/src/app.ts b/src/app.ts index e9fb2ff3..935b2da9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,7 +1,7 @@ import express, { Application, Express, Request, Response } from "express"; import { configController } from "./components/config/config-controller"; import { tokenController } from "./components/token/token-controller"; -import { authoriseGetController } from "./components/authorise/authorise-get-controller"; +import { authoriseController } from "./components/authorise/authorise-get-controller"; import { dedupeQueryParams } from "./middleware/dedupe-query-params"; import { userInfoController } from "./components/user-info/user-info-controller"; import { generateJWKS } from "./components/token/helper/key-helpers"; @@ -23,7 +23,7 @@ const createApp = (): Application => { app.get("/", (req: Request, res: Response) => { res.send("Express + TypeScript Server"); }); - app.get("/authorize", authoriseGetController); + app.use("/authorize", authoriseController); app.post( "/config", diff --git a/src/components/authorise/authorise-get-controller.ts b/src/components/authorise/authorise-get-controller.ts index 4d001497..69dabb99 100644 --- a/src/components/authorise/authorise-get-controller.ts +++ b/src/components/authorise/authorise-get-controller.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import { logger } from "../../logger"; -import { parseAuthQueryParams } from "../../parse/parse-auth-request-query-params"; +import { parseAuthRequest } from "../../parse/parse-auth-request"; import { AuthoriseRequestError } from "../../errors/authorise-request-error"; import { BadRequestError } from "../../errors/bad-request-error"; import { ParseAuthRequestError } from "../../errors/parse-auth-request-error"; @@ -8,17 +8,54 @@ import { Config } from "../../config"; import { MissingParameterError } from "../../errors/missing-parameter-error"; import { base64url } from "jose"; import { randomBytes } from "crypto"; +import { validateAuthRequestQueryParams } from "../../validators/validate-auth-request-query-params"; +import { MethodNotAllowedError } from "../../errors/method-not-allowed-error"; +import { VectorOfTrust } from "../../types/vector-of-trust"; +import { validateAuthRequestObject } from "../../validators/validate-auth-request-object"; +import { transformRequestObject } from "../../utils/utils"; +import { TrustChainValidationError } from "../../errors/trust-chain-validation-error"; -export const authoriseGetController = (req: Request, res: Response): void => { +export const authoriseController = async ( + req: Request, + res: Response +): Promise => { const config = Config.getInstance(); try { - const parsedAuthRequestParams = parseAuthQueryParams( - //We can safely cast this type as our middleware will handle - //any duplicate query params - req.query as Record, - config - ); + let parsedAuthRequest; + + switch (req.method) { + case "GET": + parsedAuthRequest = parseAuthRequest( + //We can safely cast this type as our middleware will handle + //any duplicate query params + req.query as Record + ); + break; + default: + throw new MethodNotAllowedError(req.method); + } + + if (!parsedAuthRequest.requestObject) { + logger.info("Validating request query params"); + validateAuthRequestQueryParams(parsedAuthRequest, config); + } else { + logger.info("Validating request object"); + await validateAuthRequestObject(parsedAuthRequest, config); + parsedAuthRequest = transformRequestObject( + parsedAuthRequest.requestObject + ); + } + + if (parsedAuthRequest.prompt.includes("select_account")) { + throw new AuthoriseRequestError({ + errorCode: "unmet_authentication_requirements", + errorDescription: "Unmet authentication requirements", + httpStatusCode: 302, + state: parsedAuthRequest.state, + redirectUri: parsedAuthRequest.redirect_uri, + }); + } if (config.getAuthoriseErrors().includes("ACCESS_DENIED")) { logger.warn("Client configured to return access_denied error response"); @@ -27,23 +64,22 @@ export const authoriseGetController = (req: Request, res: Response): void => { errorDescription: "Access denied by resource owner or authorization server", httpStatusCode: 302, - redirectUri: parsedAuthRequestParams.redirect_uri, - state: parsedAuthRequestParams.state, + redirectUri: parsedAuthRequest.redirect_uri, + state: parsedAuthRequest.state, }); } const authCode = generateAuthCode(); - config.addToAuthCodeRequestParamsStore(authCode, { - claims: parsedAuthRequestParams.claims, - nonce: parsedAuthRequestParams.nonce, - redirectUri: parsedAuthRequestParams.redirect_uri, - scopes: parsedAuthRequestParams.scope, - vtr: parsedAuthRequestParams.vtr[0], + claims: parsedAuthRequest.claims, + nonce: parsedAuthRequest.nonce, + redirectUri: parsedAuthRequest.redirect_uri, + scopes: parsedAuthRequest.scope, + vtr: (parsedAuthRequest.vtr as VectorOfTrust[])[0], }); res.redirect( - `${parsedAuthRequestParams.redirect_uri}?code=${authCode}&state=${parsedAuthRequestParams.state}` + `${parsedAuthRequest.redirect_uri}?code=${authCode}&state=${parsedAuthRequest.state}` ); return; } catch (error) { @@ -82,8 +118,13 @@ const handleRequestError = ( } } else if (error instanceof BadRequestError) { res.status(400).send("Invalid Request"); + } else if (error instanceof MethodNotAllowedError) { + res.status(405).send(error.message); + } else if (error instanceof TrustChainValidationError) { + res.status(400).send(error.message); } else { logger.error("Unknown error occurred: " + (error as Error).message); + logger.error(error); res.status(500).json({ message: "Internal Server Error", }); diff --git a/src/errors/method-not-allowed-error.ts b/src/errors/method-not-allowed-error.ts new file mode 100644 index 00000000..c7b7e4ba --- /dev/null +++ b/src/errors/method-not-allowed-error.ts @@ -0,0 +1,5 @@ +export class MethodNotAllowedError extends Error { + constructor(httpMethod: string) { + super(`Method "${httpMethod} not allowed"`); + } +} diff --git a/tests/integration/authorise-get-controller.test.ts b/tests/integration/authorise-get-controller.test.ts index 3a05e38a..6b8699d3 100644 --- a/tests/integration/authorise-get-controller.test.ts +++ b/tests/integration/authorise-get-controller.test.ts @@ -1,7 +1,8 @@ -import crypto from "crypto"; +import crypto, { generateKeyPairSync } from "crypto"; import { createApp } from "./../../src/app"; import request from "supertest"; import { Config } from "./../../src/config"; +import { exportSPKI, JWTPayload, SignJWT } from "jose"; const authoriseEndpoint = "/authorize"; const knownClientId = "43c729a8f8a8bed3441a872039d45180"; @@ -17,358 +18,212 @@ const createRequestParams = (params: Record): string => { .join("&"); }; -describe("/authorize GET controller: invalid request non-redirecting errors", () => { - beforeAll(() => { - process.env.CLIENT_ID = knownClientId; - process.env.REDIRECT_URLS = knownRedirectUri; - }); - - it("returns a Missing Parameters response for no query strings", async () => { - const app = createApp(); - const response = await request(app).get(authoriseEndpoint); - expect(response.status).toBe(400); - expect(response.text).toBe("Request is missing parameters"); - }); - - it("returns an Missing Parameters response for an no client_id", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - state: "51994f8e382a5c6ffa19d2518b190696", - }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Request is missing parameters"); - }); - - it("returns an Missing Parameters response for no response_type", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Request is missing parameters"); - }); - - it("returns invalid request for a non-oidc prompt", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - response_type: "code", - prompt: "login unknown", - }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Invalid Request"); - }); - - it("returns an Missing Parameters response for a no redirect uri", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - response_type: "code", - }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Request is missing parameters"); - }); - - it("returns an Missing Parameters response for a badly formatted redirect uri", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: ".notaredirectURI..", - response_type: "code", +describe("Auth requests using query params", () => { + describe("/authorize GET controller: invalid request non-redirecting errors", () => { + beforeAll(() => { + process.env.CLIENT_ID = knownClientId; + process.env.REDIRECT_URLS = knownRedirectUri; }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Request is missing parameters"); - }); - it("returns an Invalid request response for non-matching redirect_uri", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: unknownRedirectUri, - response_type: "code", + it("returns a Missing Parameters response for no query strings", async () => { + const app = createApp(); + const response = await request(app).get(authoriseEndpoint); + expect(response.status).toBe(400); + expect(response.text).toBe("Request is missing parameters"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Invalid Request"); - }); - it("returns an Invalid request response for an unknown client_id", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: crypto.randomUUID(), - redirect_uri: knownRedirectUri, - response_type: "code", + it("returns an Missing Parameters response for an no client_id", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + state: "51994f8e382a5c6ffa19d2518b190696", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Request is missing parameters"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(400); - expect(response.text).toBe("Invalid Request"); - }); -}); - -describe("/authorize GET controller: Invalid request, redirecting errors", () => { - beforeEach(() => { - process.env.CLIENT_ID = knownClientId; - process.env.REDIRECT_URLS = knownRedirectUri; - process.env.SCOPES = "openid,email"; - process.env.CLAIMS = "https://vocab.account.gov.uk/v1/coreIdentityJWT"; - process.env.CLIENT_LOCS = "P2"; - Config.resetInstance(); - }); - it("redirects with an invalid request error response for no scope parameter", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", + it("returns an invalid request response for no response_type", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Invalid Request"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` - ); - }); - it("redirects with an invalid request error for a scope which does not include openid ", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "email", + it("returns invalid request for a non-oidc prompt", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + response_type: "code", + prompt: "login unknown", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Invalid Request"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` - ); - }); - it("returns 302 and redirects with an error for invalid JSON in claims", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - claims: "{{}", + it("returns an Missing Parameters response for a no redirect uri", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + response_type: "code", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Request is missing parameters"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` - ); - }); - it("returns 302 and redirects with an error for unknown prompt", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - prompt: "login unknown", + it("returns an Missing Parameters response for a badly formatted redirect uri", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: ".notaredirectURI..", + response_type: "code", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Request is missing parameters"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` - ); - }); - it("returns 302 and redirects with an error for no state", async () => { - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - prompt: "login", + it("returns an Invalid request response for non-matching redirect_uri", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: unknownRedirectUri, + response_type: "code", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Invalid Request"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request is missing state parameter")}` - ); - }); - it("returns 302 and redirects if the request includes a request_uri", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - prompt: "login", - state, - request_uri: "https:/example.com/request-uri/12345", + it("returns an Invalid request response for an unknown client_id", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: crypto.randomUUID(), + redirect_uri: knownRedirectUri, + response_type: "code", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Invalid Request"); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=request_uri_not_supported&error_description=${encodeURIComponent("Request URI parameter not supported")}&state=${state}` - ); }); - it("returns 302 and redirects if the response_type is not code", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "id_token", - scope: "openid email", - prompt: "login", - state, + describe("/authorize GET controller: Invalid request, redirecting errors", () => { + beforeEach(() => { + process.env.CLIENT_ID = knownClientId; + process.env.REDIRECT_URLS = knownRedirectUri; + process.env.SCOPES = "openid,email"; + process.env.CLAIMS = "https://vocab.account.gov.uk/v1/coreIdentityJWT"; + process.env.CLIENT_LOCS = "P2"; + Config.resetInstance(); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=unsupported_response_type&error_description=${encodeURIComponent("Unsupported response type")}&state=${state}` - ); - }); - it("returns 302 and redirects with an error for invalid scope", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid unknown", - prompt: "login", - state, + it("redirects with an invalid request error response for no scope parameter", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` - ); - }); - it("returns 302 and redirects with an error for unsupported scope", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid phone", - prompt: "login", - state, + it("redirects with an invalid request error for a scope which does not include openid ", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "email", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` - ); - }); - it("returns 302 and redirects with an error for unknown claim", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - prompt: "login", - state, - claims: '{"userinfo":{"unknownClaim":null}}', + it("returns 302 and redirects with an error for invalid JSON in claims", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + claims: "{{}", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` - ); - }); - it("returns 302 and redirects with an error for an unsupported claim", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - prompt: "login", - state, - claims: '{"userinfo":{"https://vocab.account.gov.uk/v1/passport":null}}', + it("returns 302 and redirects with an error for unknown prompt", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + prompt: "login unknown", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid Request")}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` - ); - }); - it("returns 302 and redirects with an error for no nonce value", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - prompt: "login", - state, - claims: - '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + it("returns 302 and redirects with an error for no state", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + prompt: "login", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request is missing state parameter")}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request is missing nonce parameter")}&state=${state}` - ); - }); - it.each(["[]]", '["Ch"]', '["Cl.P2"]', '["Cl.Cm.P2","Cl.Cm"]'])( - "returns 302 and redirects with an error for invalid Vtr value %s", - async (vtrValue) => { + it("returns 302 and redirects if the request includes a request_uri", async () => { const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; const app = createApp(); const requestParams = createRequestParams({ @@ -378,154 +233,1043 @@ describe("/authorize GET controller: Invalid request, redirecting errors", () => scope: "openid email", prompt: "login", state, - claims: - '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', - nonce: "3eb5b04ca8e1baf7dea15b7fb7ac05a6", - vtr: vtrValue, + request_uri: "https:/example.com/request-uri/12345", }); const response = await request(app).get( authoriseEndpoint + "?" + requestParams ); expect(response.status).toBe(302); expect(response.header.location).toBe( - `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request vtr not valid")}&state=${state}` + `${knownRedirectUri}?error=request_uri_not_supported&error_description=${encodeURIComponent("Request URI parameter not supported")}&state=${state}` ); - } - ); -}); - -describe("valid auth request", () => { - jest - .spyOn(crypto, "randomBytes") - .mockImplementation(() => - Buffer.from( - "6e4195129066135da2c81745247bb0edf82e00da5a8925d1a1289629ad8633" - ) - ); - beforeEach(() => { - process.env.CLIENT_ID = knownClientId; - process.env.REDIRECT_URLS = knownRedirectUri; - process.env.SCOPES = "openid,email"; - process.env.CLAIMS = "https://vocab.account.gov.uk/v1/coreIdentityJWT"; - process.env.CLIENT_LOCS = "P2"; - Config.resetInstance(); - }); + }); - it("returns 302 and redirects with an access_denied error when the client has enabled ACCESS_DENIED", async () => { + it("returns 302 and redirects if the response_type is not code", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "id_token", + scope: "openid email", + prompt: "login", + state, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=unsupported_response_type&error_description=${encodeURIComponent("Unsupported response type")}&state=${state}` + ); + }); + + it("returns 302 and redirects with an error for invalid scope", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid unknown", + prompt: "login", + state, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` + ); + }); + + it("returns 302 and redirects with an error for unsupported scope", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid phone", + prompt: "login", + state, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` + ); + }); + + it("returns 302 and redirects with an error for unknown claim", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + prompt: "login", + state, + claims: '{"userinfo":{"unknownClaim":null}}', + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` + ); + }); + + it("returns 302 and redirects with an error for an unsupported claim", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + prompt: "login", + state, + claims: + '{"userinfo":{"https://vocab.account.gov.uk/v1/passport":null}}', + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` + ); + }); + + it("returns 302 and redirects with an error for no nonce value", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + prompt: "login", + state, + claims: + '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request is missing nonce parameter")}&state=${state}` + ); + }); + + it.each(["[]]", '["Ch"]', '["Cl.P2"]', '["Cl.Cm.P2","Cl.Cm"]'])( + "returns 302 and redirects with an error for invalid Vtr value %s", + async (vtrValue) => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + prompt: "login", + state, + claims: + '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + nonce: "3eb5b04ca8e1baf7dea15b7fb7ac05a6", + vtr: vtrValue, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request vtr not valid")}&state=${state}` + ); + } + ); + + it("returns 302 and redirects with an error code and description", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + state, + nonce, + prompt: "select_account", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=unmet_authentication_requirements&error_description=${encodeURIComponent("Unmet authentication requirements")}&state=${state}` + ); + }); + }); + + describe("valid auth request", () => { jest - .spyOn(Config.getInstance(), "getAuthoriseErrors") - .mockReturnValue(["ACCESS_DENIED"]); + .spyOn(crypto, "randomBytes") + .mockImplementation(() => + Buffer.from( + "6e4195129066135da2c81745247bb0edf82e00da5a8925d1a1289629ad8633" + ) + ); + beforeEach(() => { + process.env.CLIENT_ID = knownClientId; + process.env.REDIRECT_URLS = knownRedirectUri; + process.env.SCOPES = "openid,email"; + process.env.CLAIMS = "https://vocab.account.gov.uk/v1/coreIdentityJWT"; + process.env.CLIENT_LOCS = "P2"; + Config.resetInstance(); + }); + + it("returns 302 and redirects with an access_denied error when the client has enabled ACCESS_DENIED", async () => { + jest + .spyOn(Config.getInstance(), "getAuthoriseErrors") + .mockReturnValue(["ACCESS_DENIED"]); + + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + state, + nonce, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?error=access_denied&error_description=${encodeURIComponent("Access denied by resource owner or authorization server")}&state=${state}` + ); + }); + + it("returns 302 and redirect with an auth code for a valid minimal auth request", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + state, + nonce, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + ); + }); + + it("returns 302 and redirect with an auth code for a valid auth request with claims", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + state, + claims: + '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + nonce, + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + ); + }); + + it("returns 302 and redirect with an auth code for a valid auth request with claims and prompt", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + state, + claims: + '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + nonce, + prompt: "login", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + ); + }); + + it("returns 302 and redirect with an auth code for a valid auth request with claims, prompt and valid vtr", async () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + state, + claims: + '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + nonce, + prompt: "login", + vtr: '["Cl.Cm.P2"]', + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + ); + }); + }); +}); + +describe("Auth requests using request objects", () => { + const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; + const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; + const rsaKeyPair = generateKeyPairSync("rsa", { + modulusLength: 2048, + }); + + beforeEach(async () => { + process.env.CLIENT_ID = knownClientId; + process.env.REDIRECT_URLS = knownRedirectUri; + process.env.SCOPES = "openid,email"; + process.env.CLAIMS = "https://vocab.account.gov.uk/v1/coreIdentityJWT"; + process.env.CLIENT_LOCS = "P2"; + process.env.PUBLIC_KEY = await exportSPKI(rsaKeyPair.publicKey); + process.env.SIMULATOR_URL = "http://localhost:8080"; + Config.resetInstance(); + }); + + describe("/authorize GET controller: invalid request non-redirecting errors", () => { + beforeEach(async () => { + process.env.CLIENT_ID = knownClientId; + process.env.REDIRECT_URLS = knownRedirectUri; + process.env.SCOPES = "openid,email"; + process.env.CLAIMS = "https://vocab.account.gov.uk/v1/coreIdentityJWT"; + process.env.CLIENT_LOCS = "P2"; + process.env.PUBLIC_KEY = await exportSPKI(rsaKeyPair.publicKey); + process.env.SIMULATOR_URL = "http://localhost:8080"; + Config.resetInstance(); + }); + + it("returns a Bad Request response for invalid client ID", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: "unknown-client-id", + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Invalid Request"); + }); + + it("returns a validation failed response for invalid JWT signature", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: (await encodedJwtWithParams({})) + "a", + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Trust chain validation failed"); + }); + + it("returns a Invalid request response for a redirect_uri that does not match the client config", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + redirect_uri: "https://example.com/auth-callback", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(400); + expect(response.text).toBe("Invalid Request"); + }); + + it("returns a 500 response for badly formatted redirect_uri", async () => { + const badUri = "http^^^^://"; + Config.getInstance().setRedirectUrls([knownRedirectUri, badUri]); + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + redirect_uri: "http^^^^://", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(500); + expect(response.text).toBe( + JSON.stringify({ + message: "Internal Server Error", + }) + ); + }); + }); + + describe("/authorize GET controller: invalid request redirecting errors", () => { + beforeEach(() => { + Config.resetInstance(); + }); + + it("returns 302 with invalid request for no state", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ state: undefined }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request is missing state parameter")}` + ); + }); + + it("returns 302 with unsupported response type for non code response type at top level (not request object)", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "id_token", + scope: "openid email", + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unsupported_response_type&error_description=${encodeURIComponent("Unsupported response type")}&state=${state}` + ); + }); + + it("returns 302 with invalid scopes for request with unknown scopes at top level", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid profile", + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` + ); + }); + + it("returns 302 with invalid scopes for request with scopes the client is not configured for", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid phone", + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` + ); + }); + + //ATO-1329: Validation will match this after this ticket is complete + it("returns 302 with invalid request for unknown userinfo claims at the top level", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + claims: JSON.stringify({ + userinfo: { + exampleClaim: { essential: true }, + }, + }), + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` + ); + }); + + it("returns 302 with invalid request for claims they aren't configured for at the top level", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + claims: JSON.stringify({ + userinfo: { + "https://vocab.account.gov.uk/v1/passport": { essential: true }, + }, + }), + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` + ); + }); + + it("returns 302 with unauthorized client for no client id in request object", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ client_id: undefined }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unauthorized_client&error_description=${encodeURIComponent("Unauthorized client")}&state=${state}` + ); + }); + + it("returns 302 with unauthorized client when client id in request object does not match top level client id", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + client_id: "different-client-id", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unauthorized_client&error_description=${encodeURIComponent("Unauthorized client")}&state=${state}` + ); + }); + + it("returns 302 with invalid request when request present in request object", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + request: "another-request-object", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid request")}&state=${state}` + ); + }); + + it("returns 302 with invalid request when request uri present in request object", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + request_uri: "another-request-uri", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Invalid request")}&state=${state}` + ); + }); + + it("returns 302 with access denied for no aud in request object", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + aud: undefined, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=access_denied&error_description=${encodeURIComponent("Access denied by resource owner or authorization server")}&state=${state}` + ); + }); + + it("returns 302 with access denied when aud in request object does not match the authorise endpoint", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + aud: "http://not-authorise-endpoint.com/authorize", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=access_denied&error_description=${encodeURIComponent("Access denied by resource owner or authorization server")}&state=${state}` + ); + }); - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - state, - nonce, + it("returns 302 with unauthorized client error when no issuer is in the request object", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + iss: undefined, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unauthorized_client&error_description=${encodeURIComponent("Unauthorized client")}&state=${state}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?error=access_denied&error_description=${encodeURIComponent("Access denied by resource owner or authorization server")}&state=${state}` - ); - }); - it("returns 302 and redirect with an auth code for a valid minimal auth request", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - state, - nonce, + it("returns 302 with unauthorized client error when issuer is in the request object does not match client_id", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + iss: "not-client-id", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unauthorized_client&error_description=${encodeURIComponent("Unauthorized client")}&state=${state}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` - ); - }); - it("returns 302 and redirect with an auth code for a valid auth request with claims", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - state, - claims: - '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', - nonce, + it('returns 302 with unsupported response type when response type in request object is not "code"', async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + response_type: "id_token", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unsupported_response_type&error_description=${encodeURIComponent("Unsupported response type")}&state=${state}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + + it("returns a 302 with invalid scope error if the request object contains unknown scopes", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + scope: "openid profile", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` + ); + }); + + it("returns a 302 with invalid scope error if the request object contains scopes the client is not configured for", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + scope: "openid phone", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_scope&error_description=${encodeURIComponent("Invalid, unknown or malformed scope")}&state=${state}` + ); + }); + + it("returns a 500 with Internal Server Error if the claims cannot be parsed", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + claims: ["invalid claims"], + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(500); + expect(response.text).toBe( + JSON.stringify({ + message: "Internal Server Error", + }) + ); + }); + + it("returns a 302 with invalid request if the request object claims contain unknown claims", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + claims: { + userinfo: { + invalidClaim: { essential: true }, + }, + }, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` + ); + }); + + it("returns a 302 with invalid request if the request object claims contain claims the client is not configured for", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + claims: { + userinfo: { + "https://vocab.account.gov.uk/v1/passport": { essential: true }, + }, + }, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request contains invalid claims")}&state=${state}` + ); + }); + + it("returns a 302 with invalid request if the request object has no nonce parameter", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + nonce: undefined, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request is missing nonce parameter")}&state=${state}` + ); + }); + + it.each(["[]]", '["Ch"]', '["Cl.P2"]', '["Cl.Cm.P2","Cl.Cm"]'])( + "returns 302 and redirects with an error for invalid Vtr value %s", + async (vtrValue) => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + vtr: vtrValue, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Request vtr not valid")}&state=${state}` + ); + } ); + + it("returns a 302 with invalid request inf the max_age parameter is negative in the request object", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + max_age: -1000, + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Max age is negative in request object")}&state=${state}` + ); + }); + + it("returns a 302 with invalid request if the max_age parameter in the request object fails to be parsed to an integer", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + max_age: "notANumber", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=invalid_request&error_description=${encodeURIComponent("Max age could not be parsed to an integer")}&state=${state}` + ); + }); + + it("returns a 302 with unmet authentication requirements if the prompt includes select_account", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + prompt: "select_account", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=unmet_authentication_requirements&error_description=${encodeURIComponent("Unmet authentication requirements")}&state=${state}` + ); + }); + + it("returns a 302 with access denied if error configuration includes ACCESS_DENIED", async () => { + Config.getInstance().setAuthoriseErrors(["ACCESS_DENIED"]); + + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.headers.location).toBe( + `${knownRedirectUri}?error=access_denied&error_description=${encodeURIComponent("Access denied by resource owner or authorization server")}&state=${state}` + ); + }); }); - it("returns 302 and redirect with an auth code for a valid auth request with claims and prompt", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, - redirect_uri: knownRedirectUri, - response_type: "code", - scope: "openid email", - state, - claims: - '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', - nonce, - prompt: "login", + describe("valid auth request", () => { + jest + .spyOn(crypto, "randomBytes") + .mockImplementation(() => + Buffer.from( + "6e4195129066135da2c81745247bb0edf82e00da5a8925d1a1289629ad8633" + ) + ); + + it("returns 302 and redirect with an auth code for a valid minimal auth request", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({}), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + ); + }); + + it("returns 302 and redirect with an auth code for a valid auth request with all parameters", async () => { + const app = createApp(); + const requestParams = createRequestParams({ + client_id: knownClientId, + redirect_uri: knownRedirectUri, + response_type: "code", + scope: "openid email", + request: await encodedJwtWithParams({ + max_age: 123, + prompt: "login", + claims: + '{"userinfo": { "https://vocab.account.gov.uk/v1/coreIdentityJWT": { "essential": true }}}', + ui_locales: "en", + }), + }); + const response = await request(app).get( + authoriseEndpoint + "?" + requestParams + ); + expect(response.status).toBe(302); + expect(response.header.location).toBe( + `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` + ); }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` - ); }); - it("returns 302 and redirect with an auth code for a valid auth request with claims, prompt and valid vtr", async () => { - const state = "a7e4bfd39d3eaa57c27775744f22c5a2"; - const nonce = "3eb5b04ca8e1baf7dea15b7fb7ac05a6"; - const app = createApp(); - const requestParams = createRequestParams({ - client_id: knownClientId, + const encodedJwtWithParams = async ( + params: Record + ): Promise => { + const payload = { redirect_uri: knownRedirectUri, + client_id: knownClientId, response_type: "code", - scope: "openid email", state, - claims: - '{"userinfo":{"https://vocab.account.gov.uk/v1/coreIdentityJWT":null}}', + iss: knownClientId, + scope: "openid", + aud: "http://localhost:8080/authorize", nonce, - prompt: "login", - vtr: '["Cl.Cm.P2"]', - }); - const response = await request(app).get( - authoriseEndpoint + "?" + requestParams - ); - expect(response.status).toBe(302); - expect(response.header.location).toBe( - `${knownRedirectUri}?code=${knownAuthCode}&state=${state}` - ); - }); + ...params, + }; + return await signPayload(payload); + }; + const signPayload = async (payload: JWTPayload): Promise => { + return await new SignJWT(payload) + .setProtectedHeader({ + alg: "RS256", + }) + .sign(rsaKeyPair.privateKey); + }; });