From 57ca0203bb9f90bb9e9b21e22aa5bc492bfcff4c Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Thu, 25 Jul 2024 18:38:53 +0200 Subject: [PATCH 1/4] fix: remove bug for txCode Signed-off-by: Mirko Mollik --- packages/issuer/lib/VcIssuer.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 3cd70b08..a5cfa435 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -192,7 +192,6 @@ export class VcIssuer { status, notification_id: v4(), ...(userPin && { txCode: userPin }), // We used to use userPin according to older specs. We map these onto txCode now. If both are used, txCode in the end wins, even if they are different - ...(txCode && { txCode }), ...(opts.credentialDataSupplierInput && { credentialDataSupplierInput: opts.credentialDataSupplierInput }), credentialOffer, } From 7e06eb3a7527df839b3e42e0c8218f96592f2da8 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Fri, 26 Jul 2024 13:47:08 +0200 Subject: [PATCH 2/4] set tx_code in request Signed-off-by: Mirko Mollik --- packages/client/lib/AccessTokenClient.ts | 2 +- packages/common/lib/types/Authorization.types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index adc0afcc..064da005 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -104,7 +104,7 @@ export class AccessTokenClient { if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) { this.assertAlphanumericPin(opts.pinMetadata, pin); - request.user_pin = pin; + request.tx_code = pin; request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE; // we actually know it is there because of the isPreAuthCode call diff --git a/packages/common/lib/types/Authorization.types.ts b/packages/common/lib/types/Authorization.types.ts index d52e9753..f2bf12f5 100644 --- a/packages/common/lib/types/Authorization.types.ts +++ b/packages/common/lib/types/Authorization.types.ts @@ -312,7 +312,7 @@ export interface AccessTokenRequest { 'pre-authorized_code': string; redirect_uri?: string; scope?: string; - user_pin?: string; //pre draft 13 + user_pin?: string; //this is for v11, not required in v13 anymore tx_code?: string; //draft 13 [s: string]: unknown; } From 5ca1eda0f55e5b777c01ab99edbd53108b2f3767 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Mon, 29 Jul 2024 15:57:27 +0200 Subject: [PATCH 3/4] set both variables to be compliant Signed-off-by: Mirko Mollik --- packages/client/lib/AccessTokenClient.ts | 1 + packages/issuer/lib/tokens/index.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index 064da005..f7c0d44d 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -104,6 +104,7 @@ export class AccessTokenClient { if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) { this.assertAlphanumericPin(opts.pinMetadata, pin); + request.user_pin = pin; request.tx_code = pin; request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE; diff --git a/packages/issuer/lib/tokens/index.ts b/packages/issuer/lib/tokens/index.ts index be5ef9f1..dfe5d568 100644 --- a/packages/issuer/lib/tokens/index.ts +++ b/packages/issuer/lib/tokens/index.ts @@ -102,12 +102,17 @@ export const assertValidAccessTokenRequest = async ( invalid_request: the Authorization Server does not expect a PIN in the pre-authorized flow but the client provides a PIN */ - if (!credentialOfferSession.credentialOffer.credential_offer?.grants?.[GrantTypes.PRE_AUTHORIZED_CODE]?.tx_code && request.tx_code) { + if ( + !credentialOfferSession.credentialOffer.credential_offer?.grants?.[GrantTypes.PRE_AUTHORIZED_CODE]?.tx_code && + request.tx_code && + !request.user_pin + ) { // >= v13 throw new TokenError(400, TokenErrorResponse.invalid_request, USER_PIN_NOT_REQUIRED_ERROR) } else if ( !credentialOfferSession.credentialOffer.credential_offer?.grants?.[GrantTypes.PRE_AUTHORIZED_CODE]?.user_pin_required && - request.user_pin + request.user_pin && + !request.tx_code ) { // <= v12 throw new TokenError(400, TokenErrorResponse.invalid_request, USER_PIN_NOT_REQUIRED_ERROR) From 71e72aa24b208e5d9597351c45da312cddc04e0a Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 30 Jul 2024 17:11:02 +0200 Subject: [PATCH 4/4] 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', ); });