From 0a115fef610c2ce5dd54cb2b02d70096fe51db6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= <3857362+corrideat@users.noreply.github.com> Date: Tue, 23 May 2023 23:18:47 +0200 Subject: [PATCH] Refactoring and better RFC support and validation --- README.md | 8 +- package-lock.json | 4 +- package.json | 2 +- src/exchangeTokenEndpoint.ts | 125 ++-- src/hydraSessionConstructorFactory.ts | 534 ++++++++++-------- src/index.ts | 1 + src/lib/authenticatedFetch.ts | 7 +- .../{ResponseError.ts => errorResponse.ts} | 22 +- test/index.ts | 4 +- 9 files changed, 395 insertions(+), 312 deletions(-) rename src/lib/{ResponseError.ts => errorResponse.ts} (67%) diff --git a/README.md b/README.md index 6ff7289..b934b4c 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,6 @@ const exchangeTokenEndpointHandler = exchangeTokenEndpoint( 'about:invalid', // hydraClientRedirectUri 'http://localhost:4444', // hydraPublicUri 'http://localhost:4445', // hydraAdminUri - [], // hydraScope - [], // hydraAudience { ['clientAuthMethod']: 'none' }, // hydraPublicAuthParams // NB! Remember to use authentication in production { ['clientAuthMethod']: 'none' }, // hydraAdminAuthParams @@ -75,6 +73,12 @@ const exchangeTokenEndpointHandler = exchangeTokenEndpoint( name: 'Alice', } }), + [], // scope. Optional list of lowercase scopes + [], // audience. Optional list of audiences + [], // subjectTokenType. Optional list of acceptable token types; + // null or undefined defaults to access tokens + [], // actorTokenType. Optional list of acceptable token types + // null or undefined defaults to none ); server(listeners.node) diff --git a/package-lock.json b/package-lock.json index 3c25d89..f128185 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@exact-realty/hydra-rfc8693", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@exact-realty/hydra-rfc8693", - "version": "1.1.1", + "version": "1.1.2", "hasInstallScript": true, "license": "Apache-2.0 WITH LLVM-exception", "devDependencies": { diff --git a/package.json b/package.json index fc47af7..e2826f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@exact-realty/hydra-rfc8693", - "version": "1.1.1", + "version": "1.1.2", "description": "An implementation of RFC 8693 for Ory Hydra, providing powerful capabilities for token exchange in OAuth 2.0 and OpenID Connect servers.", "main": "dist/index.js", "module": "./dist/index.mjs", diff --git a/src/exchangeTokenEndpoint.ts b/src/exchangeTokenEndpoint.ts index 6c73aa2..17558b5 100644 --- a/src/exchangeTokenEndpoint.ts +++ b/src/exchangeTokenEndpoint.ts @@ -14,7 +14,7 @@ */ import hydraSessionConstructorFactory from './hydraSessionConstructorFactory.js'; -import ResponseError from './lib/ResponseError.js'; +import errorResponse from './lib/errorResponse.js'; import type { TAuthenticatedFetchParams } from './lib/authenticatedFetch.js'; import authenticatedFetch from './lib/authenticatedFetch.js'; import bodyParser from './lib/bodyParser.js'; @@ -35,11 +35,13 @@ const exchangeTokenEndpoint = ( hydraClientRedirectUri: Readonly, hydraPublicUri: Readonly, hydraAdminUri: Readonly, - hydraScope: Readonly, - hydraAudience: Readonly, hydraPublicAuthParams: Readonly, hydraAdminAuthParams: Readonly, userinfo: (body: URLSearchParams) => Promise | TSessionInfo, + scope?: Readonly | undefined | null, + audience?: Readonly | undefined | null, + subjectTokenType?: Readonly | undefined | null, + actorTokenType?: Readonly | undefined | null, ) => { if (!hydraPublicUri || !getOrigin(hydraPublicUri)) { throw new Error('Invalid Hydra public URI: ' + hydraPublicUri); @@ -82,10 +84,20 @@ const exchangeTokenEndpoint = ( } if ( - !Array.isArray(hydraAudience) || - !hydraAudience.reduce((acc, cv) => acc && typeof cv === 'string', true) + !Array.isArray(audience) || + !audience.reduce((acc, cv) => acc && typeof cv === 'string', true) ) { - throw new Error('Invalid Hydra audience: ' + hydraAudience); + throw new Error('Invalid Hydra audience: ' + audience); + } + + // Default subject token type to accept is access token + if (!subjectTokenType) { + subjectTokenType = ['urn:ietf:params:oauth:token-type:access_token']; + } + + // Default actor token type to accept is none + if (!actorTokenType) { + actorTokenType = []; } const hydraSessionConstructor = hydraSessionConstructorFactory( @@ -99,49 +111,53 @@ const exchangeTokenEndpoint = ( authenticatedFetch(hydraAdminAuthParams), ); + // Redefinitions to keep TypeScript happy + const subjectTokenType_ = subjectTokenType; + const actorTokenType_ = actorTokenType; + return async (req: Request) => { try { const body = await bodyParser(req); // REQUIRED if (!body.has('subject_token')) { - throw new ResponseError({ - ['error']: 'invalid_request', - ['error_description']: 'missing subject_token', - }); + return errorResponse( + 'invalid_request', + 'missing subject_token', + ); } // REQUIRED if ( - body.get('subject_token_type') !== - 'urn:ietf:params:oauth:token-type:access_token' + !body.has('subject_token_type') || + !subjectTokenType_.includes( + String(body.get('subject_token_type')).toLowerCase(), + ) ) { - throw new ResponseError({ - ['error']: 'invalid_request', - ['error_description']: 'invalid subject_token_type', - }); + return errorResponse( + 'invalid_request', + 'invalid subject_token_type', + ); } - // OPTIONAL + // REQUIRED if ( - body.get('grant_type') !== + String(body.get('grant_type')).toLowerCase() !== 'urn:ietf:params:oauth:grant-type:token-exchange' ) { - throw new ResponseError({ - ['error']: 'unsupported_grant_type', - }); + return errorResponse('unsupported_grant_type'); } // OPTIONAL but only access_token supported if ( - body.get('requested_token_type') && - body.get('requested_token_type') !== + body.has('requested_token_type') && + String(body.get('requested_token_type')).toLowerCase() !== 'urn:ietf:params:oauth:token-type:access_token' ) { - throw new ResponseError({ - ['error']: 'invalid_request', - ['error_description']: 'invalid requested_token_type', - }); + return errorResponse( + 'invalid_request', + 'invalid requested_token_type', + ); } const requestedScope = String(body.get('scope') ?? '') @@ -151,13 +167,11 @@ const exchangeTokenEndpoint = ( // OPTIONAL if ( !requestedScope.reduce( - (acc, cv) => acc && hydraScope.includes(cv), + (acc, cv) => acc && !!scope?.includes(cv.toLowerCase()), true, ) ) { - throw new ResponseError({ - ['error']: 'invalid_scope', - }); + return errorResponse('invalid_scope'); } const requestedAudience = body.getAll('audience'); @@ -165,32 +179,38 @@ const exchangeTokenEndpoint = ( // OPTIONAL if ( !requestedAudience.reduce( - (acc, cv) => acc && hydraAudience.includes(cv), + (acc, cv) => acc && audience.includes(cv), true, ) ) { - throw new ResponseError({ - ['error']: 'invalid_request', - ['error_description']: 'invalid audience', - }); + return errorResponse('invalid_request', 'invalid audience'); } const resource = body.get('resource'); // OPTIONAL, but must be a valid URL if (resource && getOrigin(resource) === null) { - throw new ResponseError({ - ['error']: 'invalid_request', - ['error_description']: 'invalid resource', - }); + return errorResponse('invalid_request', 'invalid resource'); } // OPTIONAL - if (body.has('actor_token') && !body.has('actor_token_type')) { - throw new ResponseError({ - ['error']: 'invalid_request', - ['error_description']: 'missing actor_token_type', - }); + if (body.has('actor_token') !== body.has('actor_token_type')) { + return errorResponse( + 'invalid_request', + 'missing actor_token or actor_token_type', + ); + } + + if ( + body.has('actor_token_type') && + !actorTokenType_.includes( + String(body.get('actor_token_type')).toLowerCase(), + ) + ) { + return errorResponse( + 'invalid_request', + 'invalid subject_token_type', + ); } const sessionInformation = await userinfo(body); @@ -216,25 +236,12 @@ const exchangeTokenEndpoint = ( }), { status: 200, - headers: { ['content-type']: 'application/json' }, + headers: [['content-type', 'application/json']], }, ); } catch (e) { if (typeof e === 'number') { return new Response(null, { status: e }); - } else if (e instanceof ResponseError) { - return new Response( - JSON.stringify({ - ['error']: e.information.error, - ['error_description']: e.information.error_description, - }), - { - status: e.information.status ?? 400, - headers: { - ['content-type']: 'application/json', - }, - }, - ); } } return new Response(null, { status: 500 }); diff --git a/src/hydraSessionConstructorFactory.ts b/src/hydraSessionConstructorFactory.ts index 93902a2..e1a5b66 100644 --- a/src/hydraSessionConstructorFactory.ts +++ b/src/hydraSessionConstructorFactory.ts @@ -85,248 +85,320 @@ const hydraSessionConstructorFactory = // 4. Intercept the redirect to the consent frotend and use the // parameters to accept the loggin request by making a request to // ${hydraAdminUri}/admin/oauth2/auth/requests/consent/accept - // 5. Make a request to the public endpoint at `${hydraPublicUri}/ - // oauth2/token` to obtain the token + // 5. Make a request to the public Hydra endpoint to obtain the + // token parameters + // 6. Intercept the redirect and make a request to the token endpoint + // at `${hydraPublicUri}/oauth2/token` to obtain the token // Since this approach requires access to the admin API with the ability // to accept login and consent requests, and any arbitrary information // could be inserted into the resulting session, this function is only // meant to be used in the context of a trusted setup. + const state = generateState(); const [code_verifier, code_challenge] = await generateChallenge(); - const initiatorParams = new URLSearchParams([ - ...((requestedAudiences && - Array.from(requestedAudiences) - .sort() - .map((aud) => ['audience', aud])) || - []), - ['client_id', hydraClientId], - ['code_challenge', code_challenge], - ['code_challenge_method', 'S256'], - ['redirect_uri', hydraClientRedirectUri], - ['response_type', 'code'], - ...((requestedScopes && [ - ['scope', Array.from(requestedScopes).sort().join(' ')], - ]) || - []), - ['state', state], - ]); - - const initiator = await publicFetch( - `${hydraPublicUri}/oauth2/auth?${initiatorParams}`, - { - redirect: 'manual', - }, - ); - - if ( - initiator.status < 300 || - initiator.status > 399 || - !initiator.headers.has('location') - ) { - throw new Error('Redirect expected while initiating login request'); - } - - const loginCookies = initiator.headers - .getSetCookie() - .map((cookie: string) => cookie.split(';')[0]) - .join('; '); - - const loginChallenge = new URL( - String(initiator.headers.get('location')), - ).searchParams.get('login_challenge'); - - if (!loginChallenge) { - throw new Error('Invalid login challenge'); - } - - const acceptedLoginRequest = await adminFetch( - `${hydraAdminUri}/admin/oauth2/auth/requests/login/accept?login_challenge=${encodeURIComponent( - loginChallenge, - )}`, - { - method: 'PUT', - body: JSON.stringify({ - ...(acr && { ['acr']: acr }), - ...(Array.isArray(amr) && !!amr.length && { ['amr']: amr }), - ['subject']: subject, - }), - redirect: 'error', - }, - ); - - if ( - acceptedLoginRequest.status !== 200 || - !acceptedLoginRequest.headers - .get('content-type') - ?.toLowerCase() - .startsWith('application/json') - ) { - // TODO - throw new Error( - 'Unexpected response code or content type while accepting login request', - ); - } - - const acceptedLoginRequestData = await acceptedLoginRequest.json(); - - const consentDestination = new URL( - acceptedLoginRequestData['redirect_to'], - ); - - const hydraLoginRequest = await publicFetch( - `${hydraPublicUri}${consentDestination.pathname}${consentDestination.search}`, - { - headers: loginCookies ? [['cookie', loginCookies]] : [], - redirect: 'manual', - }, - ); - - if ( - hydraLoginRequest.status < 300 || - hydraLoginRequest.status > 399 || - !hydraLoginRequest.headers.has('location') - ) { - throw new Error( - 'Redirect expected while consenting to login request', - ); - } - - const consentCookies = hydraLoginRequest.headers - .getSetCookie() - .map((cookie: string) => cookie.split(';')[0]) - .join('; '); - - const consentChallenge = new URL( - String(hydraLoginRequest.headers.get('location')), - ).searchParams.get('consent_challenge'); - - if (!consentChallenge) { - throw new Error('Invalid consent challenge'); - } - - const acceptedConsentRequest = await adminFetch( - `${hydraAdminUri}/admin/oauth2/auth/requests/consent/accept?consent_challenge=${encodeURIComponent( - consentChallenge, - )}`, - { - method: 'PUT', - body: JSON.stringify({ - ['grant_access_token_audience']: requestedAudiences, - ['grant_scope']: requestedScopes, - ...((sessionAccessTokenClaims || sessionIdTokenClaims) && { - ['session']: { - ...(sessionAccessTokenClaims && { - ['access_token']: sessionAccessTokenClaims, + return Promise.resolve() + .then( + /** 1. Initiate an authorization flow */ + async () => { + const initiatorParams = new URLSearchParams([ + ...((requestedAudiences && + Array.from(requestedAudiences) + .sort() + .map((aud) => ['audience', aud])) || + []), + ['client_id', hydraClientId], + ['code_challenge', code_challenge], + ['code_challenge_method', 'S256'], + ['redirect_uri', hydraClientRedirectUri], + ['response_type', 'code'], + ...((requestedScopes && [ + [ + 'scope', + Array.from(requestedScopes).sort().join(' '), + ], + ]) || + []), + ['state', state], + ]); + + const initiator = await publicFetch( + `${hydraPublicUri}/oauth2/auth?${initiatorParams}`, + { + redirect: 'manual', + }, + ); + + if ( + initiator.status < 300 || + initiator.status > 399 || + !initiator.headers.has('location') + ) { + throw new Error( + 'Redirect expected while initiating login request', + ); + } + + const loginCookies = initiator.headers + .getSetCookie() + .map((cookie: string) => cookie.split(';')[0]) + .join('; '); + + const loginChallenge = new URL( + String(initiator.headers.get('location')), + ).searchParams.get('login_challenge'); + + if (!loginChallenge) { + throw new Error('Invalid login challenge'); + } + + return [loginChallenge, loginCookies]; + }, + ) + .then( + /** 2. Intercept redirect to the authentication frontend and + * accept the login request + */ + async ([loginChallenge, loginCookies]) => { + const acceptedLoginRequest = await adminFetch( + `${hydraAdminUri}/admin/oauth2/auth/requests/login/accept?login_challenge=${encodeURIComponent( + loginChallenge, + )}`, + { + method: 'PUT', + body: JSON.stringify({ + ...(acr && { ['acr']: acr }), + ...(Array.isArray(amr) && + !!amr.length && { ['amr']: amr }), + ['subject']: subject, }), - ...(sessionIdTokenClaims && { - ['id_token']: sessionIdTokenClaims, + redirect: 'error', + }, + ); + + if ( + acceptedLoginRequest.status !== 200 || + !acceptedLoginRequest.headers + .get('content-type') + ?.toLowerCase() + .startsWith('application/json') + ) { + throw new Error( + 'Unexpected response code or content type while accepting login request', + ); + } + + const acceptedLoginRequestData = + await acceptedLoginRequest.json(); + + const consentDestination = new URL( + acceptedLoginRequestData['redirect_to'], + ); + + return [ + consentDestination.pathname + consentDestination.search, + loginCookies, + ]; + }, + ) + .then( + /** 3. Make a request to the public Hydra endpoint to obtain the + * consent parameters */ + async ([consentDestination, loginCookies]) => { + const hydraLoginRequest = await publicFetch( + `${hydraPublicUri}${consentDestination}`, + { + headers: loginCookies + ? [['cookie', loginCookies]] + : [], + redirect: 'manual', + }, + ); + + if ( + hydraLoginRequest.status < 300 || + hydraLoginRequest.status > 399 || + !hydraLoginRequest.headers.has('location') + ) { + throw new Error( + 'Redirect expected while accepting consent request', + ); + } + + const consentCookies = hydraLoginRequest.headers + .getSetCookie() + .map((cookie: string) => cookie.split(';')[0]) + .join('; '); + + const consentChallenge = new URL( + String(hydraLoginRequest.headers.get('location')), + ).searchParams.get('consent_challenge'); + + if (!consentChallenge) { + throw new Error('Invalid consent challenge'); + } + + return [consentChallenge, consentCookies]; + }, + ) + .then( + /** 4. Intercept redirect to the consent frontend and accept + * the consent request + */ + async ([consentChallenge, consentCookies]) => { + const acceptedConsentRequest = await adminFetch( + `${hydraAdminUri}/admin/oauth2/auth/requests/consent/accept?consent_challenge=${encodeURIComponent( + consentChallenge, + )}`, + { + method: 'PUT', + body: JSON.stringify({ + ['grant_access_token_audience']: + requestedAudiences, + ['grant_scope']: requestedScopes, + ...((sessionAccessTokenClaims || + sessionIdTokenClaims) && { + ['session']: { + ...(sessionAccessTokenClaims && { + ['access_token']: + sessionAccessTokenClaims, + }), + ...(sessionIdTokenClaims && { + ['id_token']: sessionIdTokenClaims, + }), + }, + }), }), + redirect: 'error', }, - }), - }), - redirect: 'error', - }, - ); - - if ( - acceptedConsentRequest.status !== 200 || - !acceptedConsentRequest.headers - .get('content-type') - ?.toLowerCase() - .startsWith('application/json') - ) { - throw new Error( - 'Unexpected response code or content type while accepting consent request', - ); - } - - const acceptedConsentRequestData = await acceptedConsentRequest.json(); - - const finalDestination = new URL( - acceptedConsentRequestData['redirect_to'], - ); - - const hydraConsentRequest = await publicFetch( - [ - `${hydraPublicUri}`, - finalDestination.pathname, - finalDestination.search, - ].join(''), - { - headers: consentCookies ? [['cookie', consentCookies]] : [], - redirect: 'manual', - }, - ); - - if ( - hydraConsentRequest.status < 300 || - hydraConsentRequest.status > 399 || - !hydraConsentRequest.headers.has('location') - ) { - throw new Error('Redirect expected'); - } - - const clientRedirect = new URL( - String(hydraConsentRequest.headers.get('location')), - ).searchParams; - const redirectState = clientRedirect.get('state'); - const code = clientRedirect.get('code'); - - if (redirectState !== state) { - throw new Error('Invalid state'); - } - - if (!code) { - throw new Error('Invalid code'); - } - - const tokenRequest = await publicFetch( - `${hydraPublicUri}/oauth2/token`, - { - method: 'POST', - body: new URLSearchParams([ - ...(hydraTokenAuthMethod === 'client_secret_basic' - ? [] - : hydraTokenAuthMethod === 'client_secret_post' - ? [ - ['client_id', hydraClientId], - ['client_secret', String(hydraClientSecret)], - ] - : [['client_id', hydraClientId]]), - ['code', code], - ['code_verifier', code_verifier], - ['grant_type', 'authorization_code'], - ['redirect_uri', hydraClientRedirectUri], - ]).toString(), - redirect: 'error', - headers: [ - ['content-type', 'application/x-www-form-urlencoded'], - ], - ...(hydraTokenAuthMethod === 'client_secret_basic' - ? { - auth: { - username: hydraClientId, - password: hydraClientSecret, - }, - } - : {}), - }, - ); - - if ( - tokenRequest.status !== 200 || - !tokenRequest.headers - .get('content-type') - ?.toLowerCase() - .startsWith('application/json') - ) { - throw new Error( - 'Unexpected response code or content type while fetching token', - ); - } + ); + + if ( + acceptedConsentRequest.status !== 200 || + !acceptedConsentRequest.headers + .get('content-type') + ?.toLowerCase() + .startsWith('application/json') + ) { + throw new Error( + 'Unexpected response code or content type while accepting consent request', + ); + } + + const acceptedConsentRequestData = + await acceptedConsentRequest.json(); + + const finalDestination = new URL( + acceptedConsentRequestData['redirect_to'], + ); + + return [ + finalDestination.pathname + finalDestination.search, + consentCookies, + ]; + }, + ) + .then( + /** 5. Make a request to the public Hydra endpoint to obtain the + * token parameters */ + async ([finalDestination, consentCookies]) => { + const hydraConsentRequest = await publicFetch( + `${hydraPublicUri}${finalDestination}`, + { + headers: consentCookies + ? [['cookie', consentCookies]] + : [], + redirect: 'manual', + }, + ); + + if ( + hydraConsentRequest.status < 300 || + hydraConsentRequest.status > 399 || + !hydraConsentRequest.headers.has('location') + ) { + throw new Error( + 'Redirect expected while accepting consent', + ); + } + + const clientRedirect = new URL( + String(hydraConsentRequest.headers.get('location')), + ).searchParams; + const redirectState = clientRedirect.get('state'); + const code = clientRedirect.get('code'); + + if (redirectState !== state) { + throw new Error('Invalid state'); + } + + if (!code) { + throw new Error('Invalid code'); + } - return tokenRequest.json(); + return [code]; + }, + ) + .then( + /** 6. Intercept the redirect and make a request to the token + * endpoint */ + async ([code]) => { + const tokenRequest = await publicFetch( + `${hydraPublicUri}/oauth2/token`, + { + method: 'POST', + body: new URLSearchParams([ + ...(hydraTokenAuthMethod === + 'client_secret_basic' + ? [] + : hydraTokenAuthMethod === + 'client_secret_post' + ? [ + ['client_id', hydraClientId], + [ + 'client_secret', + String(hydraClientSecret), + ], + ] + : [['client_id', hydraClientId]]), + ['code', code], + ['code_verifier', code_verifier], + ['grant_type', 'authorization_code'], + ['redirect_uri', hydraClientRedirectUri], + ]).toString(), + redirect: 'error', + headers: [ + [ + 'content-type', + 'application/x-www-form-urlencoded', + ], + ], + ...(hydraTokenAuthMethod === 'client_secret_basic' + ? { + auth: { + username: hydraClientId, + password: hydraClientSecret, + }, + } + : {}), + }, + ); + + if ( + tokenRequest.status !== 200 || + !tokenRequest.headers + .get('content-type') + ?.toLowerCase() + .startsWith('application/json') + ) { + throw new Error( + 'Unexpected response code or content type while fetching token', + ); + } + + return tokenRequest.json(); + }, + ); }; export default hydraSessionConstructorFactory; diff --git a/src/index.ts b/src/index.ts index 2f486a2..7eb71fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,4 +15,5 @@ export { default } from './exchangeTokenEndpoint.js'; export type { TSessionInfo } from './exchangeTokenEndpoint.js'; +export { default as errorResponse } from './lib/errorResponse.js'; export { default as authenticatedFetch } from './lib/authenticatedFetch.js'; diff --git a/src/lib/authenticatedFetch.ts b/src/lib/authenticatedFetch.ts index e4114b6..d62db75 100644 --- a/src/lib/authenticatedFetch.ts +++ b/src/lib/authenticatedFetch.ts @@ -16,6 +16,8 @@ const accessTokenSymbol = Symbol(); const expiresSymbol = Symbol(); +// TODO: Add RFC 9068 support + export type TAuthenticatedFetchParams = | { ['tokenEndpointUri']?: string; @@ -129,11 +131,6 @@ const authenticatedFetch = ( }; for (;;) { - console.log([ - token[expiresSymbol], - process.hrtime.bigint(), - token[expiresSymbol] >= process.hrtime.bigint(), - ]); if (token[expiresSymbol] >= process.hrtime.bigint()) { if ( token[expiresSymbol] - process.hrtime.bigint() < diff --git a/src/lib/ResponseError.ts b/src/lib/errorResponse.ts similarity index 67% rename from src/lib/ResponseError.ts rename to src/lib/errorResponse.ts index 0d92578..eb057a8 100644 --- a/src/lib/ResponseError.ts +++ b/src/lib/errorResponse.ts @@ -13,14 +13,16 @@ * limitations under the License. */ -class ResponseError extends Error { - information: { status?: number; error: string; error_description?: string }; +const errorResponse = (error: string, description?: string) => + new Response( + JSON.stringify({ + ['error']: error, + ['error_description']: description, + }), + { + status: 400, + headers: [['content-type', 'application/json']], + }, + ); - constructor(information: { error: string; error_description?: string }) { - super(); - this.name = this.constructor.name; - this.information = information; - } -} - -export default ResponseError; +export default errorResponse; diff --git a/test/index.ts b/test/index.ts index d54eb5a..5ea2ca5 100644 --- a/test/index.ts +++ b/test/index.ts @@ -23,8 +23,6 @@ const exchangeTokenEndpointHandler = exchangeTokenEndpoint( 'about:invalid', // hydraClientRedirectUri 'http://localhost:8846', // hydraPublicUri 'http://localhost:8847', // hydraAdminUri - [], // hydraScope - [], // hydraAudience { ['clientAuthMethod']: 'none' }, // hydraPublicAuthParams { ['clientAuthMethod']: 'none' }, // hydraAdminAuthParams (body) => ({ @@ -33,6 +31,8 @@ const exchangeTokenEndpointHandler = exchangeTokenEndpoint( ['body']: body.toString(), }, }), + [], // scope + [], // audience ); server(listeners.node)