From 06117c0fd9a06170284ce5a89075d5b12fcd7d7b Mon Sep 17 00:00:00 2001 From: sksadjad Date: Mon, 13 May 2024 09:41:04 +0200 Subject: [PATCH] fix: (WIP) refactored and fixed parts of the logic for v1_0_13. --- .../lib/__tests__/issuerCallback.spec.ts | 9 +-- packages/client/lib/AccessTokenClient.ts | 58 +++++++++------ .../client/lib/CredentialRequestClient.ts | 19 ++--- .../CredentialRequestClientBuilderV1_0_11.ts | 4 +- .../lib/CredentialRequestClientV1_0_11.ts | 4 +- packages/client/lib/OpenID4VCIClient.ts | 1 - .../client/lib/OpenID4VCIClientV1_0_11.ts | 4 +- .../lib/__tests__/AccessTokenClient.spec.ts | 44 ++++++++---- .../__tests__/CredentialRequestClient.spec.ts | 10 ++- .../CredentialRequestClientV1_0_11.spec.ts | 6 +- packages/client/lib/__tests__/IT.spec.ts | 72 ++++++++++--------- .../lib/__tests__/IssuanceInitiation.spec.ts | 4 +- .../lib/__tests__/MetadataClient.spec.ts | 2 +- .../client/lib/__tests__/MetadataMocks.ts | 6 +- .../OpenID4VCIClientPARV1_0_11.spec.ts | 4 +- packages/client/lib/__tests__/SdJwt.spec.ts | 26 ++++--- .../lib/functions/IssuerMetadataUtils.ts | 33 ++------- .../common/lib/types/Authorization.types.ts | 5 +- packages/common/lib/types/Generic.types.ts | 2 +- .../lib/__tests__/ClientIssuerIT.spec.ts | 10 ++- 20 files changed, 171 insertions(+), 152 deletions(-) diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index 560ab0d2..ef5613ab 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -4,13 +4,14 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess import { Alg, CNonceState, - CredentialConfigurationSupported, CredentialIssuerMetadata, + CredentialConfigurationSupported, + CredentialIssuerMetadata, IssuerCredentialSubjectDisplay, IssueStatus, Jwt, JwtVerifyResult, OpenId4VCIVersion, - ProofOfPossession + ProofOfPossession, } from '@sphereon/oid4vci-common' import { CredentialOfferSession } from '@sphereon/oid4vci-common/dist' import { CredentialSupportedBuilderV1_13, VcIssuer, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' @@ -215,9 +216,9 @@ describe('issuerCallback', () => { const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI })) .withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential') .withCredentialEndpointFromMetadata({ - credential_configurations_supported: {"VeriCred":{format: 'jwt_vc_json' } as CredentialConfigurationSupported} + credential_configurations_supported: { VeriCred: { format: 'jwt_vc_json' } as CredentialConfigurationSupported }, } as unknown as CredentialIssuerMetadata) - .withFormat('jwt_vc_json') + .withFormat('jwt_vc_json') .withCredentialType('credentialType') .withToken('token') diff --git a/packages/client/lib/AccessTokenClient.ts b/packages/client/lib/AccessTokenClient.ts index cd7cb04a..f1014238 100644 --- a/packages/client/lib/AccessTokenClient.ts +++ b/packages/client/lib/AccessTokenClient.ts @@ -18,7 +18,7 @@ import { PRE_AUTH_CODE_LITERAL, TokenErrorResponse, toUniformCredentialOfferRequest, - TxCode, + TxCodeAndPinRequired, UniformCredentialOfferPayload, } from '@sphereon/oid4vci-common'; import { ObjectUtils } from '@sphereon/ssi-types'; @@ -34,8 +34,7 @@ export class AccessTokenClient { const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts; const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined; - const txCode: TxCode | undefined = - credentialOffer?.credential_offer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code ?? undefined; + const pinMetadata: TxCodeAndPinRequired | undefined = credentialOffer && this.getPinMetadata(credentialOffer.credential_offer); const issuer = opts.credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : (metadata?.issuer as string)); @@ -54,9 +53,9 @@ export class AccessTokenClient { code, redirectUri, pin, - txCode, + pinMetadata, }), - txCode, + pinMetadata, metadata, asOpts, issuerOpts, @@ -65,18 +64,18 @@ export class AccessTokenClient { public async acquireAccessTokenUsingRequest({ accessTokenRequest, - txCode, + pinMetadata, metadata, asOpts, issuerOpts, }: { accessTokenRequest: AccessTokenRequest; - txCode?: TxCode; + pinMetadata?: TxCodeAndPinRequired; metadata?: EndpointMetadata; asOpts?: AuthorizationServerOpts; issuerOpts?: IssuerOpts; }): Promise> { - this.validate(accessTokenRequest, txCode); + this.validate(accessTokenRequest, pinMetadata); const requestTokenURL = AccessTokenClient.determineTokenURL({ asOpts, @@ -105,7 +104,7 @@ export class AccessTokenClient { } if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) { - this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin); + this.assertAlphanumericPin(opts.pinMetadata, pin); request.user_pin = pin; request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE; @@ -143,24 +142,39 @@ export class AccessTokenClient { } } - private isPinRequiredValue(requestPayload: UniformCredentialOfferPayload): boolean { - let isPinRequired = false; + private getPinMetadata(requestPayload: UniformCredentialOfferPayload): TxCodeAndPinRequired { if (!requestPayload) { throw new Error(TokenErrorResponse.invalid_request); } const issuer = getIssuerFromCredentialOfferPayload(requestPayload); - if (requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code) { - isPinRequired = true; - } + + const grantDetails = requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']; + const isPinRequired = !!grantDetails?.tx_code || !!grantDetails?.['pre-authorized_code']; + debug(`Pin required for issuer ${issuer}: ${isPinRequired}`); - return isPinRequired; + return { + txCode: grantDetails?.tx_code, + isPinRequired, + }; } - private assertNumericPin(isPinRequired: boolean, pin?: string): void { - if (isPinRequired) { - if (!pin || !/^\d{1,8}$/.test(pin)) { - debug(`Pin is not 1 to 8 digits long`); - throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.'); + private assertAlphanumericPin(pinMeta?: TxCodeAndPinRequired, pin?: string): void { + if (pinMeta && pinMeta.isPinRequired) { + let regex; + if (pinMeta.txCode && pinMeta.txCode.input_mode === 'numeric') { + regex = new RegExp(`^\\d{1,${pinMeta.txCode.length || 8}}$`); + } else if (pinMeta.txCode && pinMeta.txCode.input_mode === 'text') { + regex = new RegExp(`^[a-zA-Z0-9]{1,${pinMeta.txCode.length || 8}}$`); + } else { + // default regex that limits the length to 8 + regex = /^[a-zA-Z0-9]{1,8}$/; + } + + if (!pin || !regex.test(pin)) { + debug( + `Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || 'alphanumeric'}, Length: up to ${pinMeta?.txCode?.length || 8} characters`, + ); + throw new Error('A valid pin must be present according to the specified transaction code requirements.'); } } else if (pin) { debug(`Pin set, whilst not required`); @@ -188,11 +202,11 @@ export class AccessTokenClient { throw new Error('Authorization flow requires the code to be present'); } } - private validate(accessTokenRequest: AccessTokenRequest, txCode?: TxCode): void { + private validate(accessTokenRequest: AccessTokenRequest, pinMeta?: TxCodeAndPinRequired): void { if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) { this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type); this.assertNonEmptyPreAuthorizedCode(accessTokenRequest); - this.assertNumericPin(!!txCode, accessTokenRequest.user_pin); + this.assertAlphanumericPin(pinMeta, accessTokenRequest['pre-authorized_code']); } else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) { this.assertAuthorizationGrantType(accessTokenRequest.grant_type); this.assertNonEmptyCodeVerifier(accessTokenRequest); diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index cd24ba4a..5ee49a87 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -1,5 +1,6 @@ import { - acquireDeferredCredential, CredentialRequestV1_0_13, + acquireDeferredCredential, + CredentialRequestV1_0_13, CredentialResponse, getCredentialRequestForVersion, getUniformFormat, @@ -9,14 +10,14 @@ import { OpenIDResponse, ProofOfPossession, UniformCredentialRequest, - URL_NOT_VALID -} from '@sphereon/oid4vci-common' -import { CredentialFormat } from '@sphereon/ssi-types' -import Debug from 'debug' + URL_NOT_VALID, +} from '@sphereon/oid4vci-common'; +import { CredentialFormat } from '@sphereon/ssi-types'; +import Debug from 'debug'; -import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder' -import { isValidURL, post } from './functions' -import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder' +import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'; +import { isValidURL, post } from './functions'; +import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; const debug = Debug('sphereon:oid4vci:credential'); @@ -89,7 +90,7 @@ export class CredentialRequestClient { public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise> { if (this.version() < OpenId4VCIVersion.VER_1_0_13) { - throw new Error('Versions below v1.0.13 (draft 13) are not supported.') + throw new Error('Versions below v1.0.13 (draft 13) are not supported.'); } const request: CredentialRequestV1_0_13 = getCredentialRequestForVersion(uniformRequest, this.version()) as CredentialRequestV1_0_13; const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint; diff --git a/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts b/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts index 4019e64c..c830de14 100644 --- a/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts +++ b/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts @@ -14,8 +14,8 @@ import { } from '@sphereon/oid4vci-common'; import { CredentialFormat } from '@sphereon/ssi-types'; -import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11' -import { CredentialRequestClientV1_0_11 } from './CredentialRequestClientV1_0_11' +import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11'; +import { CredentialRequestClientV1_0_11 } from './CredentialRequestClientV1_0_11'; export class CredentialRequestClientBuilderV1_0_11 { credentialEndpoint?: string; diff --git a/packages/client/lib/CredentialRequestClientV1_0_11.ts b/packages/client/lib/CredentialRequestClientV1_0_11.ts index 1ac06aed..848c6ce8 100644 --- a/packages/client/lib/CredentialRequestClientV1_0_11.ts +++ b/packages/client/lib/CredentialRequestClientV1_0_11.ts @@ -14,8 +14,8 @@ import { import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; -import { buildProof } from './CredentialRequestClient' -import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11' +import { buildProof } from './CredentialRequestClient'; +import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { isValidURL, post } from './functions'; diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 3dfe86ab..b6398267 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -283,7 +283,6 @@ export class OpenID4VCIClient { if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) { redirectUri = this._state.authorizationRequestOpts.redirectUri; } - const response = await accessTokenClient.acquireAccessToken({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 77117fff..293b5849 100644 --- a/packages/client/lib/OpenID4VCIClientV1_0_11.ts +++ b/packages/client/lib/OpenID4VCIClientV1_0_11.ts @@ -27,10 +27,10 @@ import { import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; -import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11' +import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11'; import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11'; -import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11' +import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; import { MetadataClient } from './MetadataClient'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts } from './functions/AuthorizationUtil'; diff --git a/packages/client/lib/__tests__/AccessTokenClient.spec.ts b/packages/client/lib/__tests__/AccessTokenClient.spec.ts index 66171cbf..268ce3d4 100644 --- a/packages/client/lib/__tests__/AccessTokenClient.spec.ts +++ b/packages/client/lib/__tests__/AccessTokenClient.spec.ts @@ -44,6 +44,13 @@ describe('AccessTokenClient should', () => { const accessTokenResponse: OpenIDResponse = await accessTokenClient.acquireAccessTokenUsingRequest({ accessTokenRequest, + pinMetadata: { + isPinRequired: true, + txCode: { + length: accessTokenRequest['pre-authorized_code'].length, + input_mode: 'numeric', + }, + }, asOpts: { as: MOCK_URL }, }); @@ -121,13 +128,16 @@ describe('AccessTokenClient should', () => { await expect( accessTokenClient.acquireAccessTokenUsingRequest({ accessTokenRequest, - txCode: { - length: 8, - input_mode: 'text', + pinMetadata: { + isPinRequired: true, + txCode: { + length: 6, + input_mode: 'text', + }, }, asOpts: { as: MOCK_URL }, }), - ).rejects.toThrow('A valid pin consisting of maximal 8 numeric characters must be present.'); + ).rejects.toThrow('A valid pin must be present according to the specified transaction code requirements.'); }, UNIT_TEST_TIMEOUT, ); @@ -149,13 +159,16 @@ describe('AccessTokenClient should', () => { await expect( accessTokenClient.acquireAccessTokenUsingRequest({ accessTokenRequest, - txCode: { - length: 9, - input_mode: 'text', + pinMetadata: { + isPinRequired: true, + txCode: { + length: 6, + input_mode: 'text', + }, }, asOpts: { as: MOCK_URL }, }), - ).rejects.toThrow(Error('A valid pin consisting of maximal 8 numeric characters must be present.')); + ).rejects.toThrow(Error('A valid pin must be present according to the specified transaction code requirements.')); }, UNIT_TEST_TIMEOUT, ); @@ -184,9 +197,12 @@ describe('AccessTokenClient should', () => { const response = await accessTokenClient.acquireAccessTokenUsingRequest({ accessTokenRequest, - txCode: { - length: 8, - input_mode: 'text', + pinMetadata: { + isPinRequired: true, + txCode: { + length: 8, + input_mode: 'text', + }, }, asOpts: { as: MOCK_URL }, }); @@ -199,14 +215,16 @@ describe('AccessTokenClient should', () => { const accessTokenClient: AccessTokenClient = new AccessTokenClient(); nock(MOCK_URL).post(/.*/).reply(200, {}); - nock(INITIATION_TEST.credential_offer.credential_issuer+'token').post(/.*/).reply(200, {}); + nock(INITIATION_TEST.credential_offer.credential_issuer + 'token') + .post(/.*/) + .reply(200, {}); await expect(() => accessTokenClient.acquireAccessToken({ credentialOffer: INITIATION_TEST, pin: '1234', }), - ).rejects.toThrow(Error('Cannot set a pin, when the pin is not required.')); + ).rejects.toThrow(Error('A valid pin must be present according to the specified transaction code requirements.')); }); it('get error if no as, issuer and metadata values are present', async () => { diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index c2d73463..d5d5e8c6 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -21,9 +21,8 @@ import { CredentialRequestClientBuilder, CredentialRequestClientBuilderV1_0_11, MetadataClient, - ProofOfPossessionBuilder -} from '..' -import { CredentialOfferClient } from '../CredentialOfferClient'; + ProofOfPossessionBuilder, +} from '..'; import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST, WALT_OID4VCI_METADATA } from './MetadataMocks'; import { getMockData } from './data/VciDataFixtures'; @@ -73,7 +72,6 @@ afterEach(async () => { nock.cleanAll(); }); describe('Credential Request Client ', () => { - it('should get success credential response', async function () { const mockedVC = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA'; @@ -170,7 +168,7 @@ describe('Credential Request Client with different issuers ', () => { }); it('should create correct CredentialRequest for Spruce', async () => { const IRR_URI = - 'openid-initiate-issuance://?issuer=https%3A%2F%2Fngi%2Doidc4vci%2Dtest%2Espruceid%2Exyz&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJFUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOlsiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJleHAiOiIyMDIzLTA0LTIwVDA5OjA0OjM2WiIsIm5vbmNlIjoibWFibmVpT0VSZVB3V3BuRFFweEt3UnRsVVRFRlhGUEwifQ.qOZRPN8sTv_knhp7WaWte2-aDULaPZX--2i9unF6QDQNUllqDhvxgIHMDCYHCV8O2_Gj-T2x1J84fDMajE3asg&user_pin_required=false'; + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credential_configuration_ids%22:%5B%22OpenBadgeCredential%22%5D,%22grants%22:%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%7B%22pre-authorized_code%22:%22oaKazRN8I0IbtZ0C7JuMn5%22,%22tx_code%22:%7B%22input_mode%22:%22text%22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D'; const credentialRequest = await ( await CredentialRequestClientBuilder.fromURI({ uri: IRR_URI, @@ -185,7 +183,7 @@ describe('Credential Request Client with different issuers ', () => { }, credentialTypes: ['OpenBadgeCredential'], format: 'jwt_vc', - version: OpenId4VCIVersion.VER_1_0_08, + version: OpenId4VCIVersion.VER_1_0_13, }); const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08); expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request); diff --git a/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts b/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts index eaa5d4af..0c2063af 100644 --- a/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts @@ -16,15 +16,15 @@ import * as jose from 'jose'; // @ts-ignore import nock from 'nock'; -import { CredentialOfferClientV1_0_11, CredentialRequestClientBuilderV1_0_11, MetadataClient, ProofOfPossessionBuilder } from '..' +import { CredentialOfferClientV1_0_11, CredentialRequestClientBuilderV1_0_11, MetadataClient, ProofOfPossessionBuilder } from '..'; import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST, INITIATION_TEST_V1_0_08, - WALT_OID4VCI_METADATA -} from './MetadataMocks' + WALT_OID4VCI_METADATA, +} from './MetadataMocks'; import { getMockData } from './data/VciDataFixtures'; const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN'; diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index 4b4056d6..e0c2ed68 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -1,24 +1,26 @@ import { AccessTokenResponse, - Alg, CredentialOfferPayloadV1_0_13, + Alg, + CredentialOfferPayloadV1_0_13, CredentialOfferRequestWithBaseUrl, Jwt, OpenId4VCIVersion, ProofOfPossession, - WellKnownEndpoints -} from '@sphereon/oid4vci-common' + WellKnownEndpoints, +} from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; import { - AccessTokenClient, AccessTokenClientV1_0_11, + AccessTokenClient, + AccessTokenClientV1_0_11, CredentialOfferClientV1_0_11, CredentialRequestClientBuilder, CredentialRequestClientBuilderV1_0_11, OpenID4VCIClientV1_0_11, - ProofOfPossessionBuilder -} from '..' + ProofOfPossessionBuilder, +} from '..'; import { CredentialOfferClient } from '../CredentialOfferClient'; import { IDENTIPROOF_AS_METADATA, IDENTIPROOF_AS_URL, IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA } from './MetadataMocks'; @@ -60,7 +62,7 @@ describe('OID4VCI-Client should', () => { 'openid-credential-offer://credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credentials%22:%5B%7B%22format%22:%22jwt_vc_json%22,%22types%22:%5B%22VerifiableCredential%22,%22UniversityDegreeCredential%22%5D%7D%5D,%22issuer_state%22:%22eyJhbGciOiJSU0Et...FYUaBy%22%7D'; const INITIATE_QR_V1_0_13 = - 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://issuer.research.identiproof.io%22,%22credential_configuration_ids%22:%5B%22OpenBadgeCredentialUrl%22%5D,%22grants%22:%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%7B%22pre-authorized_code%22:%22oaKazRN8I0IbtZ0C7JuMn5%22,%22tx_code%22:%7B%22input_mode%22:%22text%22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D'; + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://issuer.research.identiproof.io%22,%22credential_configuration_ids%22:%5B%22OpenBadgeCredentialUrl%22%5D,%22grants%22:%7B%22urn:ietf:params:oauth:grant-type:pre-authorized_code%22:%7B%22pre-authorized_code%22:%22oaKazRN8I0IbtZ0C7JuMn5%22,%22tx_code%22:%7B%22input_mode%22:%22text%22,%22length%22:22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D'; function succeedWithAFullFlowWithClientSetup() { nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA)); @@ -135,8 +137,8 @@ describe('OID4VCI-Client should', () => { }); nock(ISSUER_URL) - .post(/token.*/) - .reply(200, JSON.stringify(mockedAccessTokenResponse)); + .post(/token.*/) + .reply(200, JSON.stringify(mockedAccessTokenResponse)); /* The actual access token calls */ const accessTokenClient: AccessTokenClientV1_0_11 = new AccessTokenClientV1_0_11(); @@ -144,16 +146,16 @@ describe('OID4VCI-Client should', () => { expect(accessTokenResponse.successBody).toEqual(mockedAccessTokenResponse); // Get the credential nock(ISSUER_URL) - .post(/credential/) - .reply(200, { - format: 'jwt-vc', - credential: mockedVC, - }); + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: credentialOffer }) - .withFormat('jwt_vc') + .withFormat('jwt_vc') - .withTokenFromResponse(accessTokenResponse.successBody!) - .build(); + .withTokenFromResponse(accessTokenResponse.successBody!) + .build(); //TS2322: Type '(args: ProofOfPossessionCallbackArgs) => Promise' // is not assignable to type 'ProofOfPossessionCallback'. @@ -166,13 +168,13 @@ describe('OID4VCI-Client should', () => { }, version: OpenId4VCIVersion.VER_1_0_11, }) - .withEndpointMetadata({ - issuer: 'https://issuer.research.identiproof.io', - credential_endpoint: 'https://issuer.research.identiproof.io/credential', - token_endpoint: 'https://issuer.research.identiproof.io/token', - }) - .withKid('did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1') - .build(); + .withEndpointMetadata({ + issuer: 'https://issuer.research.identiproof.io', + credential_endpoint: 'https://issuer.research.identiproof.io/credential', + token_endpoint: 'https://issuer.research.identiproof.io/token', + }) + .withKid('did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1') + .build(); const credResponse = await credReqClient.acquireCredentialsUsingProof({ proofInput: proof }); expect(credResponse.successBody?.credential).toEqual(mockedVC); }, @@ -184,17 +186,18 @@ describe('OID4VCI-Client should', () => { async () => { /* Convert the URI into an object */ const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_13); - + const preAuthorizedCode = 'oaKazRN8I0IbtZ0C7JuMn5'; expect(credentialOffer.baseUrl).toEqual('openid-credential-offer://'); expect((credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids).toEqual(['OpenBadgeCredentialUrl']); expect(credentialOffer.original_credential_offer.grants).toEqual({ - "urn:ietf:params:oauth:grant-type:pre-authorized_code":{ - "pre-authorized_code":"oaKazRN8I0IbtZ0C7JuMn5", - "tx_code":{ - "input_mode":"text", - "description":"Please enter the serial number of your physical drivers license" - } - } + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': preAuthorizedCode, + tx_code: { + input_mode: 'text', + description: 'Please enter the serial number of your physical drivers license', + length: preAuthorizedCode.length, + }, + }, }); nock(ISSUER_URL) @@ -236,7 +239,10 @@ describe('OID4VCI-Client should', () => { }) .withKid('did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1') .build(); - const credResponse = await credReqClient.acquireCredentialsUsingProof({ proofInput: proof, credentialTypes: credentialOffer.original_credential_offer.credential_configuration_ids }); + const credResponse = await credReqClient.acquireCredentialsUsingProof({ + proofInput: proof, + credentialTypes: credentialOffer.original_credential_offer.credential_configuration_ids, + }); expect(credResponse.successBody?.credential).toEqual(mockedVC); }, UNIT_TEST_TIMEOUT, diff --git a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts index 17b0d659..8d9fa67c 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -10,7 +10,9 @@ describe('Issuance Initiation', () => { }); it('Should return Issuance Initiation URI from request', async () => { - expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual('openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F%22%2C%22credential_configuration_ids%22%3A%5B%22OpenBadgeCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE%22%2C%22tx_code%22%3A%7B%22description%22%3A%22Pleaseprovide%20the%20one-time%20code%20that%20was%20sent%20via%20e-mail%22%2C%22input_mode%22%3A%22numeric%22%2C%22length%22%3A4%7D%7D%7D%7D'); + expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual( + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F%22%2C%22credential_configuration_ids%22%3A%5B%22OpenBadgeCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE%22%2C%22tx_code%22%3A%7B%22description%22%3A%22Pleaseprovide%20the%20one-time%20code%20that%20was%20sent%20via%20e-mail%22%2C%22input_mode%22%3A%22numeric%22%2C%22length%22%3A4%7D%7D%7D%7D', + ); }); it('Should throw error on invalid URI', async () => { diff --git a/packages/client/lib/__tests__/MetadataClient.spec.ts b/packages/client/lib/__tests__/MetadataClient.spec.ts index 51af2bf8..1a0ce12b 100644 --- a/packages/client/lib/__tests__/MetadataClient.spec.ts +++ b/packages/client/lib/__tests__/MetadataClient.spec.ts @@ -3,7 +3,7 @@ import { getIssuerFromCredentialOfferPayload, WellKnownEndpoints } from '@sphere // @ts-ignore import nock from 'nock'; -import { CredentialOfferClientV1_0_11 } from '../CredentialOfferClientV1_0_11' +import { CredentialOfferClientV1_0_11 } from '../CredentialOfferClientV1_0_11'; import { MetadataClient } from '../MetadataClient'; import { diff --git a/packages/client/lib/__tests__/MetadataMocks.ts b/packages/client/lib/__tests__/MetadataMocks.ts index 2ca57095..4ce7d598 100644 --- a/packages/client/lib/__tests__/MetadataMocks.ts +++ b/packages/client/lib/__tests__/MetadataMocks.ts @@ -1,8 +1,4 @@ -import { - AuthzFlowType, - CredentialOfferPayloadV1_0_13, - CredentialOfferRequestWithBaseUrl, -} from '@sphereon/oid4vci-common'; +import { AuthzFlowType, CredentialOfferPayloadV1_0_13, CredentialOfferRequestWithBaseUrl } from '@sphereon/oid4vci-common'; export const IDENTIPROOF_ISSUER_URL = 'https://issuer.research.identiproof.io'; export const IDENTIPROOF_AS_URL = 'https://auth.research.identiproof.io'; diff --git a/packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts index b8c7a756..d28ac897 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts @@ -1,4 +1,4 @@ -import { BAD_PARAMS, PARMode, WellKnownEndpoints } from '@sphereon/oid4vci-common'; +import { PARMode, WellKnownEndpoints } from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; @@ -59,7 +59,7 @@ describe('OpenID4VCIClientV1_0_11', () => { redirectUri: 'http://localhost:8881/cb', }, }), - ).rejects.toThrow(Error(BAD_PARAMS)); + ).rejects.toThrow('Could not create authorization details from credential offer. Please pass in explicit details'); }); it('should not fail when only authorization_details is present', async () => { diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index f959a6da..4e90364c 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -1,4 +1,4 @@ -import { AccessTokenRequest, CredentialOfferPayloadV1_0_13, CredentialRequestV1_0_11, CredentialSupportedSdJwtVc } from '@sphereon/oid4vci-common'; +import { AccessTokenRequest, CredentialRequestV1_0_11, CredentialSupportedSdJwtVc } from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; @@ -63,7 +63,6 @@ describe('sd-jwt vc', () => { 'succeed with a full flow', async () => { const offerUri = await vcIssuer.createCredentialOfferURI({ - credential_issuer: 'https://example.com', grants: { 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { tx_code: { @@ -73,15 +72,21 @@ describe('sd-jwt vc', () => { 'pre-authorized_code': '123', }, }, - credential_configuration_ids: ['SdJwtCredentialId'], - } as CredentialOfferPayloadV1_0_13); + credentials: { + SdJwtCredentialId: { + format: 'vc+sd-jwt', + vct: 'SdJwtCredential', + id: 'SdJwtCredentialId', + }, + }, + }); nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(issuerMetadata)); nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/openid-configuration').reply(404); nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/oauth-authorization-server').reply(404); expect(offerUri.uri).toEqual( - 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22123%22%7D%7D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredentialId%22%5D%7D', + 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22123%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredentialId%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%7D', ); const client = await OpenID4VCIClient.fromURI({ @@ -90,23 +95,16 @@ describe('sd-jwt vc', () => { expect(client.credentialOffer?.credential_offer).toEqual({ credential_issuer: 'https://example.com', - credentials: ['SdJwtCredentialId'], + credential_configuration_ids: ['SdJwtCredentialId'], grants: { 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { 'pre-authorized_code': '123', - user_pin_required: false, }, }, }); const supported = client.getCredentialsSupported('vc+sd-jwt'); - expect(supported).toEqual([ - { - vct: 'SdJwtCredential', - format: 'vc+sd-jwt', - id: 'SdJwtCredentialId', - }, - ]); + expect(supported).toEqual({ SdJwtCredentialId: { format: 'vc+sd-jwt', id: 'SdJwtCredentialId', vct: 'SdJwtCredential' } }); const offered = supported[0] as CredentialSupportedSdJwtVc; diff --git a/packages/common/lib/functions/IssuerMetadataUtils.ts b/packages/common/lib/functions/IssuerMetadataUtils.ts index 77ddd351..693b6451 100644 --- a/packages/common/lib/functions/IssuerMetadataUtils.ts +++ b/packages/common/lib/functions/IssuerMetadataUtils.ts @@ -9,7 +9,7 @@ import { OID4VCICredentialFormat, OpenId4VCIVersion, } from '../types'; -import { CredentialIssuerMetadataOptsV1_0_13, IssuerMetadataV1_0_13 } from '../types/v1_0_13.types'; +import { IssuerMetadataV1_0_13 } from '../types'; export function getSupportedCredentials(options?: { issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08 | IssuerMetadataV1_0_13; @@ -40,41 +40,18 @@ export function getSupportedCredential(opts?: { types?: string | string[]; format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[]; }): Record { - // Safely extract properties from opts with default values const { issuerMetadata, types, format } = opts ?? {}; - // Default to an empty object if no metadata is provided - if (!issuerMetadata) { + if (!issuerMetadata || !issuerMetadata.credential_configurations_supported) { return {}; } - // Handle formats array - let formats: (OID4VCICredentialFormat | string)[] = []; - if (Array.isArray(format)) { - formats = format; - } else if (format) { - formats = [format]; - } - - // Define and fill credential configuration based on version and metadata - let credentialConfigsSupported: Record = {}; - if (Object.keys(credentialConfigsSupported).length === 0) { - return {}; - } else if ('credential_configurations_supported' in issuerMetadata) { - credentialConfigsSupported = (issuerMetadata as CredentialIssuerMetadataOptsV1_0_13).credential_configurations_supported!; - } - - // Exit early if no credentials are supported or no filtering is required - if (!types && Object.keys(credentialConfigsSupported).length > 0) { - return credentialConfigsSupported; - } - - // Normalize types to an array + const configurations = issuerMetadata.credential_configurations_supported; + const formats = Array.isArray(format) ? format : format ? [format] : []; const normalizedTypes = Array.isArray(types) ? types : types ? [types] : []; - // Filter configurations based on types and formats const filteredConfigs: Record = {}; - Object.entries(credentialConfigsSupported).forEach(([key, value]) => { + Object.entries(configurations).forEach(([key, value]) => { const isTypeMatch = normalizedTypes.length === 0 || normalizedTypes.includes(key); const isFormatMatch = formats.length === 0 || formats.includes(value.format); diff --git a/packages/common/lib/types/Authorization.types.ts b/packages/common/lib/types/Authorization.types.ts index 26ed88c5..1cf29f03 100644 --- a/packages/common/lib/types/Authorization.types.ts +++ b/packages/common/lib/types/Authorization.types.ts @@ -198,6 +198,9 @@ export interface IssuerOpts { export interface AccessTokenFromAuthorizationResponseOpts extends AccessTokenRequestOpts { authorizationResponse: AuthorizationResponse; } + +export type TxCodeAndPinRequired = { isPinRequired?: boolean; txCode?: TxCode }; + export interface AccessTokenRequestOpts { credentialOffer?: UniformCredentialOffer; credentialIssuer?: string; @@ -207,7 +210,7 @@ export interface AccessTokenRequestOpts { code?: string; // only required for authorization flow redirectUri?: string; // only required for authorization flow pin?: string; // Pin-number. Only used when required - txCode?: TxCode; // OPTIONAL. String value containing a Transaction Code. This value MUST be present if a tx_code object was present in the Credential Offer (including if the object was empty). This parameter MUST only be used if the grant_type is urn:ietf:params:oauth:grant-type:pre-authorized_code. + pinMetadata?: TxCodeAndPinRequired; // OPTIONAL. String value containing a Transaction Code. This value MUST be present if a tx_code object was present in the Credential Offer (including if the object was empty). This parameter MUST only be used if the grant_type is urn:ietf:params:oauth:grant-type:pre-authorized_code. } /*export interface AuthorizationRequestOpts { diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index defe23a4..a2a72fff 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -4,7 +4,7 @@ import { ProofOfPossession } from './CredentialIssuance.types'; import { AuthorizationServerMetadata } from './ServerMetadata'; import { CredentialOfferSession } from './StateManager.types'; import { CredentialRequestV1_0_11 } from './v1_0_11.types'; -import { CredentialRequestV1_0_13 } from './v1_0_13.types' +import { CredentialRequestV1_0_13 } from './v1_0_13.types'; export type InputCharSet = 'numeric' | 'text'; export type KeyProofType = 'jwt' | 'cwt' | 'ldp_vp'; diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index bab73fca..8eccf424 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -196,7 +196,7 @@ describe('VcIssuer', () => { }, }, }, - credentials: { 'UniversityDegree_JWT': {format: 'ldp_vc', id: 'UniversityDegree_JWT'} as CredentialConfigurationSupported }, + credentials: { UniversityDegree_JWT: { format: 'ldp_vc', id: 'UniversityDegree_JWT' } as CredentialConfigurationSupported }, scheme: 'http', }) .then((response) => response.uri) @@ -307,7 +307,13 @@ describe('VcIssuer', () => { }) it('should acquire access token', async () => { - accessToken = await client.acquireAccessToken({ pin: credOfferSession.userPin }) + client = await OpenID4VCIClient.fromURI({ + uri: `http://localhost:3456/test?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22previously-created-state%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22testcode%22%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegree_JWT%22%5D%2C%22credential_issuer%22%3A%22http%3A%2F%2Flocalhost%3A3456%2Ftest%22%7D`, + kid: subjectDIDKey.didDocument.authentication[0], + alg: 'ES256', + createAuthorizationRequestURL: false, + }) + accessToken = await client.acquireAccessToken({ pin: 'testcode' }) expect(accessToken).toBeDefined() })