From 71e72aa24b208e5d9597351c45da312cddc04e0a Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 30 Jul 2024 17:11:02 +0200 Subject: [PATCH] fix: scope and par fixes Signed-off-by: Timo Glastra --- .../client/lib/AuthorizationCodeClient.ts | 23 ++++++++--------- .../lib/__tests__/OpenID4VCIClient.spec.ts | 25 ++----------------- .../__tests__/OpenID4VCIClientV1_0_13.spec.ts | 25 ++----------------- 3 files changed, 15 insertions(+), 58 deletions(-) diff --git a/packages/client/lib/AuthorizationCodeClient.ts b/packages/client/lib/AuthorizationCodeClient.ts index f247a739..f3b9d129 100644 --- a/packages/client/lib/AuthorizationCodeClient.ts +++ b/packages/client/lib/AuthorizationCodeClient.ts @@ -113,13 +113,16 @@ export const createAuthorizationRequestUrl = async ({ const { redirectUri, requestObjectOpts = { requestObjectMode: CreateRequestObjectMode.NONE } } = authorizationRequest; const client_id = clientId ?? authorizationRequest.clientId; - let { scope, authorizationDetails } = authorizationRequest; - const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests + // Authorization server metadata takes precedence + const authorizationMetadata = endpointMetadata.authorizationServerMetadata ?? endpointMetadata.credentialIssuerMetadata + + let { authorizationDetails } = authorizationRequest; + const parMode = authorizationMetadata?.require_pushed_authorization_requests ? PARMode.REQUIRE - : authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER); + : (authorizationRequest.parMode ?? (client_id ? PARMode.AUTO : PARMode.NEVER)); // Scope and authorization_details can be used in the same authorization request // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param - if (!scope && !authorizationDetails) { + if (!authorizationRequest.scope && !authorizationDetails) { if (!credentialOffer) { throw Error('Please provide a scope or authorization_details if no credential offer is present'); } @@ -177,12 +180,8 @@ export const createAuthorizationRequestUrl = async ({ if (!endpointMetadata?.authorization_endpoint) { throw Error('Server metadata does not contain authorization endpoint'); } - const parEndpoint = endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint; + const parEndpoint = authorizationMetadata?.pushed_authorization_request_endpoint; - // add 'openid' scope if not present - if (!scope?.includes('openid')) { - scope = ['openid', scope].filter((s) => !!s).join(' '); - } let queryObj: Record | PushedAuthorizationResponse = { response_type: ResponseType.AUTH_CODE, @@ -194,7 +193,7 @@ export const createAuthorizationRequestUrl = async ({ ...(redirectUri && { redirect_uri: redirectUri }), ...(client_id && { client_id }), ...(credentialOffer?.issuerState && { issuer_state: credentialOffer.issuerState }), - scope, + scope: authorizationRequest.scope, }; if (!parEndpoint && parMode === PARMode.REQUIRE) { @@ -210,11 +209,11 @@ export const createAuthorizationRequestUrl = async ({ { contentType: 'application/x-www-form-urlencoded', accept: 'application/json' }, ); if (parResponse.errorBody || !parResponse.successBody) { - console.log(JSON.stringify(parResponse.errorBody)); - console.log('Falling back to regular request URI, since PAR failed'); if (parMode === PARMode.REQUIRE) { throw Error(`PAR error: ${parResponse.origResponse.statusText}`); } + + debug('Falling back to regular request URI, since PAR failed', JSON.stringify(parResponse.errorBody)); } else { debug(`PAR response: ${JSON.stringify(parResponse.successBody, null, 2)}`); queryObj = { /*response_type: ResponseType.AUTH_CODE,*/ client_id, request_uri: parResponse.successBody.request_uri }; diff --git a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts index c53d1e02..53c5a7f3 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts @@ -59,27 +59,6 @@ describe('OpenID4VCIClient should', () => { }), ).rejects.toThrow(Error('Server metadata does not contain authorization endpoint')); }); - it("injects 'openid' as the first scope if not provided", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; - - const url = await client.createAuthorizationRequestUrl({ - pkce: { - codeChallengeMethod: CodeChallengeMethod.S256, - codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', - }, - authorizationRequest: { - scope: 'TestCredential', - redirectUri: 'http://localhost:8881/cb', - }, - }); - - const urlSearchParams = new URLSearchParams(url.split('?')[1]); - const scope = urlSearchParams.get('scope')?.split(' '); - - expect(scope?.[0]).toBe('openid'); - }); it('throw an error if no scope and no authorization_details is provided', async () => { nock(MOCK_URL).get(/.*/).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(200, {}); @@ -149,7 +128,7 @@ describe('OpenID4VCIClient should', () => { }, }), ).resolves.toEqual( - 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', + 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client', ); }); it('create an authorization request url with authorization_details object property', async () => { @@ -176,7 +155,7 @@ describe('OpenID4VCIClient should', () => { }, }), ).resolves.toEqual( - 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', + 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client', ); }); diff --git a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts index 4ef5a1a6..9f7fbc3e 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts @@ -51,27 +51,6 @@ describe('OpenID4VCIClientV1_0_13 should', () => { }), ).rejects.toThrow(Error('Server metadata does not contain authorization endpoint')); }); - it("injects 'openid' as the first scope if not provided", async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; - - const url = await client.createAuthorizationRequestUrl({ - pkce: { - codeChallengeMethod: CodeChallengeMethod.S256, - codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', - }, - authorizationRequest: { - scope: 'TestCredential', - redirectUri: 'http://localhost:8881/cb', - }, - }); - - const urlSearchParams = new URLSearchParams(url.split('?')[1]); - const scope = urlSearchParams.get('scope')?.split(' '); - - expect(scope?.[0]).toBe('openid'); - }); it('throw an error if no scope and no authorization_details is provided', async () => { nock(MOCK_URL).get(/.*/).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(200, {}); @@ -141,7 +120,7 @@ describe('OpenID4VCIClientV1_0_13 should', () => { }, }), ).resolves.toEqual( - 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', + 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%2C%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22mso_mdoc%22%2C%22doctype%22%3A%22org%2Eiso%2E18013%2E5%2E1%2EmDL%22%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D%5D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client', ); }); it('create an authorization request url with authorization_details object property', async () => { @@ -168,7 +147,7 @@ describe('OpenID4VCIClientV1_0_13 should', () => { }, }), ).resolves.toEqual( - 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', + 'https://server.example.com/v1/auth/authorize?response_type=code&code_challenge_method=S256&code_challenge=mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs&authorization_details=%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22ldp_vc%22%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fwww%2Ew3%2Eorg%2F2018%2Fcredentials%2Fexamples%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%2C%22locations%22%3A%5B%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client', ); });