From d8c2c4fa8d73ea14a0faa823a394cde23733db8f Mon Sep 17 00:00:00 2001 From: sksadjad Date: Sat, 11 May 2024 11:16:39 +0200 Subject: [PATCH] fix: (WIP) refactored and fixed build. still have to fix 8 test cases that are failing --- .../lib/__tests__/issuerCallback.spec.ts | 58 ++-- .../client/lib/AccessTokenClientV1_0_11.ts | 4 +- packages/client/lib/CredentialOfferClient.ts | 7 +- .../client/lib/CredentialRequestClient.ts | 31 +- .../CredentialRequestClientBuilderV1_0_11.ts | 156 +++++++++ .../lib/CredentialRequestClientV1_0_11.ts | 190 +++++++++++ .../client/lib/OpenID4VCIClientV1_0_11.ts | 16 +- .../lib/__tests__/AccessTokenClient.spec.ts | 1 + .../__tests__/CredentialRequestClient.spec.ts | 47 +-- .../CredentialRequestClientV1_0_11.spec.ts | 316 ++++++++++++++++++ packages/client/lib/__tests__/IT.spec.ts | 95 +++++- .../lib/__tests__/IssuanceInitiation.spec.ts | 35 +- .../IssuanceInitiationV1_0_11.spec.ts | 62 ++++ .../lib/__tests__/MetadataClient.spec.ts | 4 +- .../client/lib/__tests__/MetadataMocks.ts | 38 ++- .../lib/__tests__/OpenID4VCIClient.spec.ts | 10 +- ....ts => OpenID4VCIClientPARV1_0_11.spec.ts} | 2 +- .../__tests__/OpenID4VCIClientV1_0_11.spec.ts | 202 +++++++++++ packages/client/lib/__tests__/SdJwt.spec.ts | 8 +- packages/client/lib/index.ts | 4 +- .../lib/functions/CredentialOfferUtil.ts | 2 +- packages/common/lib/types/Generic.types.ts | 3 +- .../lib/__tests__/ClientIssuerIT.spec.ts | 7 +- packages/issuer/lib/VcIssuer.ts | 5 +- .../issuer/lib/__tests__/VcIssuer.spec.ts | 6 +- pnpm-lock.yaml | 34 +- 26 files changed, 1168 insertions(+), 175 deletions(-) create mode 100644 packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts create mode 100644 packages/client/lib/CredentialRequestClientV1_0_11.ts create mode 100644 packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts create mode 100644 packages/client/lib/__tests__/IssuanceInitiationV1_0_11.spec.ts rename packages/client/lib/__tests__/{OpenID4VCIClientPAR.spec.ts => OpenID4VCIClientPARV1_0_11.spec.ts} (99%) create mode 100644 packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index 1eea7397..560ab0d2 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -4,13 +4,13 @@ import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossess import { Alg, CNonceState, - CredentialConfigurationSupported, + 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' @@ -23,7 +23,7 @@ import * as jose from 'jose' import { generateDid, getIssuerCallback, verifyCredential } from '../IssuerCallback' const INITIATION_TEST_URI = - 'openid-initiate-issuance://?credential_type=OpenBadgeCredential&issuer=https%3A%2F%2Fjff%2Ewalt%2Eid%2Fissuer-api%2Foidc%2F&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false' + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://credential-issuer.example.com%22,%22credential_configuration_ids%22:%5B%22UniversityDegreeCredential%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 IDENTIPROOF_ISSUER_URL = 'https://example.com/credential' const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21#keys-1' let keypair: KeyPair // Proof of Possession JWT @@ -85,28 +85,27 @@ describe('issuerCallback', () => { const clientId = 'sphereon:wallet' beforeAll(async () => { - const credentialsSupported: Record = - new CredentialSupportedBuilderV1_13() - .withCryptographicSuitesSupported('ES256K') - .withCryptographicBindingMethod('did') - .withFormat('jwt_vc_json') - .withTypes('VerifiableCredential') - .withId('UniversityDegree_JWT') - .withCredentialSupportedDisplay({ - name: 'University Credential', - locale: 'en-US', - logo: { - url: 'https://exampleuniversity.com/public/logo.png', - alt_text: 'a square logo of a university', - }, - background_color: '#12107c', - text_color: '#FFFFFF', - }) - .addCredentialSubjectPropertyDisplay('given_name', { - name: 'given name', - locale: 'en-US', - } as IssuerCredentialSubjectDisplay) - .build() + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCryptographicSuitesSupported('ES256K') + .withCryptographicBindingMethod('did') + .withFormat('jwt_vc_json') + .withTypes('VerifiableCredential') + .withId('UniversityDegree_JWT') + .withCredentialSupportedDisplay({ + name: 'University Credential', + locale: 'en-US', + logo: { + url: 'https://exampleuniversity.com/public/logo.png', + alt_text: 'a square logo of a university', + }, + background_color: '#12107c', + text_color: '#FFFFFF', + }) + .addCredentialSubjectPropertyDisplay('given_name', { + name: 'given name', + locale: 'en-US', + } as IssuerCredentialSubjectDisplay) + .build() const stateManager = new MemoryStates() await stateManager.set('existing-state', { issuerState: 'existing-state', @@ -215,7 +214,10 @@ describe('issuerCallback', () => { it('Should pass requesting a verifiable credential using the client', async () => { const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI })) .withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential') - .withFormat('jwt_vc_json') + .withCredentialEndpointFromMetadata({ + credential_configurations_supported: {"VeriCred":{format: 'jwt_vc_json' } as CredentialConfigurationSupported} + } as unknown as CredentialIssuerMetadata) + .withFormat('jwt_vc_json') .withCredentialType('credentialType') .withToken('token') @@ -237,7 +239,7 @@ describe('issuerCallback', () => { callbacks: { signCallback: proofOfPossessionCallbackFunction, }, - version: OpenId4VCIVersion.VER_1_0_11, + version: OpenId4VCIVersion.VER_1_0_13, }) .withClientId(clientId) .withKid(kid) @@ -248,7 +250,7 @@ describe('issuerCallback', () => { credentialTypes: ['VerifiableCredential'], format: 'jwt_vc_json', proofInput: proof, - version: OpenId4VCIVersion.VER_1_0_11, + version: OpenId4VCIVersion.VER_1_0_13, }) expect(credentialRequest).toEqual({ format: 'jwt_vc_json', diff --git a/packages/client/lib/AccessTokenClientV1_0_11.ts b/packages/client/lib/AccessTokenClientV1_0_11.ts index 6357a84f..a7781d79 100644 --- a/packages/client/lib/AccessTokenClientV1_0_11.ts +++ b/packages/client/lib/AccessTokenClientV1_0_11.ts @@ -30,7 +30,7 @@ import { convertJsonToURI, formPost } from './functions'; const debug = Debug('sphereon:oid4vci:token'); -export class AccessTokenClient { +export class AccessTokenClientV1_0_11 { public async acquireAccessToken(opts: AccessTokenRequestOpts): Promise> { const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts; @@ -77,7 +77,7 @@ export class AccessTokenClient { }): Promise> { this.validate(accessTokenRequest, isPinRequired); - const requestTokenURL = AccessTokenClient.determineTokenURL({ + const requestTokenURL = AccessTokenClientV1_0_11.determineTokenURL({ asOpts, issuerOpts, metadata: metadata diff --git a/packages/client/lib/CredentialOfferClient.ts b/packages/client/lib/CredentialOfferClient.ts index 71fb7995..b1d5d145 100644 --- a/packages/client/lib/CredentialOfferClient.ts +++ b/packages/client/lib/CredentialOfferClient.ts @@ -46,9 +46,10 @@ export class CredentialOfferClient { ...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && { preAuthorizedCode: grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']['pre-authorized_code'], }), - ...(request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code && { - // txCode: request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code, - }), + ...(request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code && + { + // txCode: request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code, + }), }; } diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index cbdd7827..cd24ba4a 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -1,5 +1,5 @@ import { - acquireDeferredCredential, + acquireDeferredCredential, CredentialRequestV1_0_13, CredentialResponse, getCredentialRequestForVersion, getUniformFormat, @@ -9,14 +9,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 { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; -import { isValidURL, post } from './functions'; +import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder' +import { isValidURL, post } from './functions' +import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder' const debug = Debug('sphereon:oid4vci:credential'); @@ -41,7 +41,7 @@ export async function buildProof( ) { if ('proof_type' in proofInput) { if (opts.cNonce) { - throw Error(`Cnonce param is only supported when using a Proof of Posession builder`); + throw Error(`Cnonce param is only supported when using a Proof of possession builder`); } return await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build(); } @@ -88,7 +88,10 @@ export class CredentialRequestClient { } public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise> { - const request = getCredentialRequestForVersion(uniformRequest, this.version()); + if (this.version() < OpenId4VCIVersion.VER_1_0_13) { + 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; if (!isValidURL(credentialEndpoint)) { debug(`Invalid credential endpoint: ${credentialEndpoint}`); @@ -154,7 +157,7 @@ export class CredentialRequestClient { throw Error(`Credential type(s) need to be provided`); } // FIXME: this is mixing up the type (as id) from v8/v9 and the types (from the vc.type) from v11 - else if (!this.isV11OrHigher() && types.length !== 1) { + else if (!this.isV13OrHigher() && types.length !== 1) { throw Error('Only a single credential type is supported for V8/V9'); } const proof = await buildProof(proofInput, opts); @@ -199,10 +202,10 @@ export class CredentialRequestClient { } private version(): OpenId4VCIVersion { - return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11; + return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_13; } - private isV11OrHigher(): boolean { - return this.version() >= OpenId4VCIVersion.VER_1_0_11; + private isV13OrHigher(): boolean { + return this.version() >= OpenId4VCIVersion.VER_1_0_13; } } diff --git a/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts b/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts new file mode 100644 index 00000000..4019e64c --- /dev/null +++ b/packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts @@ -0,0 +1,156 @@ +import { + AccessTokenResponse, + CredentialIssuerMetadata, + CredentialOfferPayloadV1_0_08, + CredentialOfferPayloadV1_0_11, + CredentialOfferRequestWithBaseUrl, + determineSpecVersionFromOffer, + EndpointMetadata, + getIssuerFromCredentialOfferPayload, + getTypesFromOfferV1_0_11, + OID4VCICredentialFormat, + OpenId4VCIVersion, + UniformCredentialOfferRequest, +} 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' + +export class CredentialRequestClientBuilderV1_0_11 { + credentialEndpoint?: string; + deferredCredentialEndpoint?: string; + deferredCredentialAwait = false; + deferredCredentialIntervalInMS = 5000; + credentialTypes: string[] = []; + format?: CredentialFormat | OID4VCICredentialFormat; + token?: string; + version?: OpenId4VCIVersion; + + public static fromCredentialIssuer({ + credentialIssuer, + metadata, + version, + credentialTypes, + }: { + credentialIssuer: string; + metadata?: EndpointMetadata; + version?: OpenId4VCIVersion; + credentialTypes: string | string[]; + }): CredentialRequestClientBuilderV1_0_11 { + const issuer = credentialIssuer; + const builder = new CredentialRequestClientBuilderV1_0_11(); + builder.withVersion(version ?? OpenId4VCIVersion.VER_1_0_11); + builder.withCredentialEndpoint(metadata?.credential_endpoint ?? (issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`)); + if (metadata?.deferred_credential_endpoint) { + builder.withDeferredCredentialEndpoint(metadata.deferred_credential_endpoint); + } + builder.withCredentialType(credentialTypes); + return builder; + } + + public static async fromURI({ uri, metadata }: { uri: string; metadata?: EndpointMetadata }): Promise { + const offer = await CredentialOfferClientV1_0_11.fromURI(uri); + return CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ request: offer, ...offer, metadata, version: offer.version }); + } + + public static fromCredentialOfferRequest(opts: { + request: UniformCredentialOfferRequest; + scheme?: string; + baseUrl?: string; + version?: OpenId4VCIVersion; + metadata?: EndpointMetadata; + }): CredentialRequestClientBuilderV1_0_11 { + const { request, metadata } = opts; + const version = opts.version ?? request.version ?? determineSpecVersionFromOffer(request.original_credential_offer); + const builder = new CredentialRequestClientBuilderV1_0_11(); + const issuer = getIssuerFromCredentialOfferPayload(request.credential_offer) ?? (metadata?.issuer as string); + builder.withVersion(version); + builder.withCredentialEndpoint(metadata?.credential_endpoint ?? (issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`)); + if (metadata?.deferred_credential_endpoint) { + builder.withDeferredCredentialEndpoint(metadata.deferred_credential_endpoint); + } + + if (version <= OpenId4VCIVersion.VER_1_0_08) { + //todo: This basically sets all types available during initiation. Probably the user only wants a subset. So do we want to do this? + builder.withCredentialType((request.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type); + } else if (version <= OpenId4VCIVersion.VER_1_0_11) { + // todo: look whether this is correct + builder.withCredentialType(getTypesFromOfferV1_0_11(request.credential_offer as CredentialOfferPayloadV1_0_11)); + } + + return builder; + } + + public static fromCredentialOffer({ + credentialOffer, + metadata, + }: { + credentialOffer: CredentialOfferRequestWithBaseUrl; + metadata?: EndpointMetadata; + }): CredentialRequestClientBuilderV1_0_11 { + return CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ + request: credentialOffer, + metadata, + version: credentialOffer.version, + }); + } + + public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata): this { + this.credentialEndpoint = metadata.credential_endpoint; + return this; + } + + public withCredentialEndpoint(credentialEndpoint: string): this { + this.credentialEndpoint = credentialEndpoint; + return this; + } + + public withDeferredCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata): this { + this.deferredCredentialEndpoint = metadata.deferred_credential_endpoint; + return this; + } + + public withDeferredCredentialEndpoint(deferredCredentialEndpoint: string): this { + this.deferredCredentialEndpoint = deferredCredentialEndpoint; + return this; + } + + public withDeferredCredentialAwait(deferredCredentialAwait: boolean, deferredCredentialIntervalInMS?: number): this { + this.deferredCredentialAwait = deferredCredentialAwait; + this.deferredCredentialIntervalInMS = deferredCredentialIntervalInMS ?? 5000; + return this; + } + + public withCredentialType(credentialTypes: string | string[]): this { + this.credentialTypes = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes]; + return this; + } + + public withFormat(format: CredentialFormat | OID4VCICredentialFormat): this { + this.format = format; + return this; + } + + public withToken(accessToken: string): this { + this.token = accessToken; + return this; + } + + public withTokenFromResponse(response: AccessTokenResponse): this { + this.token = response.access_token; + return this; + } + + public withVersion(version: OpenId4VCIVersion): this { + this.version = version; + return this; + } + + public build(): CredentialRequestClientV1_0_11 { + if (!this.version) { + this.withVersion(OpenId4VCIVersion.VER_1_0_11); + } + return new CredentialRequestClientV1_0_11(this); + } +} diff --git a/packages/client/lib/CredentialRequestClientV1_0_11.ts b/packages/client/lib/CredentialRequestClientV1_0_11.ts new file mode 100644 index 00000000..1ac06aed --- /dev/null +++ b/packages/client/lib/CredentialRequestClientV1_0_11.ts @@ -0,0 +1,190 @@ +import { + acquireDeferredCredential, + CredentialResponse, + getCredentialRequestForVersion, + getUniformFormat, + isDeferredCredentialResponse, + OID4VCICredentialFormat, + OpenId4VCIVersion, + OpenIDResponse, + ProofOfPossession, + UniformCredentialRequest, + URL_NOT_VALID, +} from '@sphereon/oid4vci-common'; +import { CredentialFormat } from '@sphereon/ssi-types'; +import Debug from 'debug'; + +import { buildProof } from './CredentialRequestClient' +import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11' +import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; +import { isValidURL, post } from './functions'; + +const debug = Debug('sphereon:oid4vci:credential'); + +export interface CredentialRequestOptsV1_0_11 { + deferredCredentialAwait?: boolean; + deferredCredentialIntervalInMS?: number; + credentialEndpoint: string; + deferredCredentialEndpoint?: string; + credentialTypes: string[]; + format?: CredentialFormat | OID4VCICredentialFormat; + proof: ProofOfPossession; + token: string; + version: OpenId4VCIVersion; +} + +export class CredentialRequestClientV1_0_11 { + private readonly _credentialRequestOpts: Partial; + private _isDeferred = false; + + get credentialRequestOpts(): CredentialRequestOptsV1_0_11 { + return this._credentialRequestOpts as CredentialRequestOptsV1_0_11; + } + + public isDeferred(): boolean { + return this._isDeferred; + } + + public getCredentialEndpoint(): string { + return this.credentialRequestOpts.credentialEndpoint; + } + + public getDeferredCredentialEndpoint(): string | undefined { + return this.credentialRequestOpts.deferredCredentialEndpoint; + } + + public constructor(builder: CredentialRequestClientBuilderV1_0_11) { + this._credentialRequestOpts = { ...builder }; + } + + public async acquireCredentialsUsingProof(opts: { + proofInput: ProofOfPossessionBuilder | ProofOfPossession; + credentialTypes?: string | string[]; + context?: string[]; + format?: CredentialFormat | OID4VCICredentialFormat; + }): Promise> { + const { credentialTypes, proofInput, format, context } = opts; + + const request = await this.createCredentialRequest({ proofInput, credentialTypes, context, format, version: this.version() }); + return await this.acquireCredentialsUsingRequest(request); + } + + public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise> { + const request = getCredentialRequestForVersion(uniformRequest, this.version()); + const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint; + if (!isValidURL(credentialEndpoint)) { + debug(`Invalid credential endpoint: ${credentialEndpoint}`); + throw new Error(URL_NOT_VALID); + } + debug(`Acquiring credential(s) from: ${credentialEndpoint}`); + debug(`request\n: ${JSON.stringify(request, null, 2)}`); + const requestToken: string = this.credentialRequestOpts.token; + let response: OpenIDResponse = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken }); + this._isDeferred = isDeferredCredentialResponse(response); + if (this.isDeferred() && this.credentialRequestOpts.deferredCredentialAwait && response.successBody) { + response = await this.acquireDeferredCredential(response.successBody, { bearerToken: this.credentialRequestOpts.token }); + } + + debug(`Credential endpoint ${credentialEndpoint} response:\r\n${JSON.stringify(response, null, 2)}`); + return response; + } + + public async acquireDeferredCredential( + response: Pick, + opts?: { + bearerToken?: string; + }, + ): Promise> { + const transactionId = response.transaction_id; + const bearerToken = response.acceptance_token ?? opts?.bearerToken; + const deferredCredentialEndpoint = this.getDeferredCredentialEndpoint(); + if (!deferredCredentialEndpoint) { + throw Error(`No deferred credential endpoint supplied.`); + } else if (!bearerToken) { + throw Error(`No bearer token present and refresh for defered endpoint not supported yet`); + // todo updated bearer token with new c_nonce + } + return await acquireDeferredCredential({ + bearerToken, + transactionId, + deferredCredentialEndpoint, + deferredCredentialAwait: this.credentialRequestOpts.deferredCredentialAwait, + deferredCredentialIntervalInMS: this.credentialRequestOpts.deferredCredentialIntervalInMS, + }); + } + + public async createCredentialRequest(opts: { + proofInput: ProofOfPossessionBuilder | ProofOfPossession; + credentialTypes?: string | string[]; + context?: string[]; + format?: CredentialFormat | OID4VCICredentialFormat; + version: OpenId4VCIVersion; + }): Promise { + const { proofInput } = opts; + const formatSelection = opts.format ?? this.credentialRequestOpts.format; + + if (!formatSelection) { + throw Error(`Format of credential to be issued is missing`); + } + const format = getUniformFormat(formatSelection); + const typesSelection = + opts?.credentialTypes && (typeof opts.credentialTypes === 'string' || opts.credentialTypes.length > 0) + ? opts.credentialTypes + : this.credentialRequestOpts.credentialTypes; + const types = Array.isArray(typesSelection) ? typesSelection : [typesSelection]; + if (types.length === 0) { + throw Error(`Credential type(s) need to be provided`); + } + // FIXME: this is mixing up the type (as id) from v8/v9 and the types (from the vc.type) from v11 + else if (!this.isV11OrHigher() && types.length !== 1) { + throw Error('Only a single credential type is supported for V8/V9'); + } + const proof = await buildProof(proofInput, opts); + + // TODO: we should move format specific logic + if (format === 'jwt_vc_json' || format === 'jwt_vc') { + return { + types, + format, + proof, + }; + } else if (format === 'jwt_vc_json-ld' || format === 'ldp_vc') { + if (this.version() >= OpenId4VCIVersion.VER_1_0_12 && !opts.context) { + throw Error('No @context value present, but it is required'); + } + + return { + format, + proof, + + // Ignored because v11 does not have the context value, but it is required in v12 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + credential_definition: { + types, + ...(opts.context && { '@context': opts.context }), + }, + }; + } else if (format === 'vc+sd-jwt') { + if (types.length > 1) { + throw Error(`Only a single credential type is supported for ${format}`); + } + + return { + format, + proof, + vct: types[0], + }; + } + + throw new Error(`Unsupported format: ${format}`); + } + + private version(): OpenId4VCIVersion { + return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11; + } + + private isV11OrHigher(): boolean { + return this.version() >= OpenId4VCIVersion.VER_1_0_11; + } +} diff --git a/packages/client/lib/OpenID4VCIClientV1_0_11.ts b/packages/client/lib/OpenID4VCIClientV1_0_11.ts index 14884e63..77117fff 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 { AccessTokenClient } from './AccessTokenClient'; +import { AccessTokenClientV1_0_11 } from './AccessTokenClientV1_0_11' import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; -import { CredentialOfferClient } from './CredentialOfferClient'; -import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'; +import { CredentialOfferClientV1_0_11 } from './CredentialOfferClientV1_0_11'; +import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11' import { MetadataClient } from './MetadataClient'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts } from './functions/AuthorizationUtil'; @@ -173,7 +173,7 @@ export class OpenID4VCIClientV1_0_11 { clientId?: string; authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl }): Promise { - const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }); + const credentialOfferClient = await CredentialOfferClientV1_0_11.fromURI(uri, { resolve: resolveOfferUri }); const client = new OpenID4VCIClientV1_0_11({ credentialOffer: credentialOfferClient, kid, @@ -275,7 +275,7 @@ export class OpenID4VCIClientV1_0_11 { this._state.clientId = clientId; } if (!this._state.accessTokenResponse) { - const accessTokenClient = new AccessTokenClient(); + const accessTokenClient = new AccessTokenClientV1_0_11(); if (redirectUri && redirectUri !== this._state.authorizationRequestOpts?.redirectUri) { console.log( @@ -350,11 +350,11 @@ export class OpenID4VCIClientV1_0_11 { if (kid) this._state.kid = kid; const requestBuilder = this.credentialOffer - ? CredentialRequestClientBuilder.fromCredentialOffer({ + ? CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, }) - : CredentialRequestClientBuilder.fromCredentialIssuer({ + : CredentialRequestClientBuilderV1_0_11.fromCredentialIssuer({ credentialIssuer: this.getIssuer(), credentialTypes, metadata: this.endpointMetadata, @@ -571,7 +571,7 @@ export class OpenID4VCIClientV1_0_11 { this.assertIssuerData(); return this.endpointMetadata ? this.endpointMetadata.token_endpoint - : AccessTokenClient.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } }); + : AccessTokenClientV1_0_11.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } }); } public getCredentialEndpoint(): string { diff --git a/packages/client/lib/__tests__/AccessTokenClient.spec.ts b/packages/client/lib/__tests__/AccessTokenClient.spec.ts index 9e1d1dcc..66171cbf 100644 --- a/packages/client/lib/__tests__/AccessTokenClient.spec.ts +++ b/packages/client/lib/__tests__/AccessTokenClient.spec.ts @@ -199,6 +199,7 @@ 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, {}); await expect(() => accessTokenClient.acquireAccessToken({ diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index 84dc9689..c2d73463 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -16,7 +16,13 @@ import * as jose from 'jose'; // @ts-ignore import nock from 'nock'; -import { CredentialRequestClientBuilder, MetadataClient, ProofOfPossessionBuilder } from '..'; +import { + CredentialOfferClientV1_0_11, + CredentialRequestClientBuilder, + CredentialRequestClientBuilderV1_0_11, + MetadataClient, + ProofOfPossessionBuilder +} from '..' import { CredentialOfferClient } from '../CredentialOfferClient'; import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST, WALT_OID4VCI_METADATA } from './MetadataMocks'; @@ -67,35 +73,6 @@ afterEach(async () => { nock.cleanAll(); }); describe('Credential Request Client ', () => { - it('should get a failed credential response with an unsupported format', async function () { - const basePath = 'https://sphereonjunit2022101301.com/'; - nock(basePath).post(/.*/).reply(500, { - error: 'unsupported_format', - error_description: 'This is a mock error message', - }); - - const credReqClient = CredentialRequestClientBuilder.fromCredentialOffer({ credentialOffer: INITIATION_TEST }) - .withCredentialEndpoint(basePath + '/credential') - .withFormat('ldp_vc') - .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential') - .build(); - const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ - jwt, - callbacks: { - signCallback: proofOfPossessionCallbackFunction, - }, - version: OpenId4VCIVersion.VER_1_0_08, - }) - // .withEndpointMetadata(metadata) - .withClientId('sphereon:wallet') - .withKid(kid) - .build(); - expect(credReqClient.getCredentialEndpoint()).toEqual(basePath + '/credential'); - const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, version: OpenId4VCIVersion.VER_1_0_08 }); - expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy(); - const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest); - expect(result?.errorBody?.error).toBe('unsupported_format'); - }); it('should get success credential response', async function () { const mockedVC = @@ -168,14 +145,14 @@ describe('Credential Request Client with Walt.id ', () => { nock.cleanAll(); const WALT_IRR_URI = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false'; - const credentialOffer = await CredentialOfferClient.fromURI(WALT_IRR_URI); + const credentialOffer = await CredentialOfferClientV1_0_11.fromURI(WALT_IRR_URI); const request = credentialOffer.credential_offer; const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string); expect(metadata.credential_endpoint).toEqual(WALT_OID4VCI_METADATA.credential_endpoint); expect(metadata.token_endpoint).toEqual(WALT_OID4VCI_METADATA.token_endpoint); - const credReqClient = CredentialRequestClientBuilder.fromCredentialOffer({ + const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer, metadata, }).build(); @@ -219,7 +196,7 @@ describe('Credential Request Client with different issuers ', () => { const IRR_URI = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMTc4OTNjYy04ZTY3LTQxNzItYWZlOS1lODcyYmYxNDBlNWMiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.ODfq2AIhOcB61dAb3zMrXBJjPJaf53zkeHh_AssYyYA&user_pin_required=false'; const credentialOffer = await ( - await CredentialRequestClientBuilder.fromURI({ + await CredentialRequestClientBuilderV1_0_11.fromURI({ uri: IRR_URI, metadata: getMockData('walt')?.metadata as unknown as EndpointMetadata, }) @@ -264,7 +241,7 @@ describe('Credential Request Client with different issuers ', () => { const IRR_URI = 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=g0UCOj6RAN5AwHU6gczm_GzB4_lH6GW39Z0Dl2DOOiO'; const credentialOffer = await ( - await CredentialRequestClientBuilder.fromURI({ + await CredentialRequestClientBuilderV1_0_11.fromURI({ uri: IRR_URI, metadata: getMockData('mattr')?.metadata as unknown as EndpointMetadata, }) @@ -287,7 +264,7 @@ describe('Credential Request Client with different issuers ', () => { const IRR_URI = 'openid-initiate-issuance://?issuer=https://oidc4vc.diwala.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJIUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZXhwIjoxNjgxOTg0NDY3fQ.fEAHKz2nuWfiYHw406iNxr-81pWkNkbi31bWsYSf6Ng'; const credentialOffer = await ( - await CredentialRequestClientBuilder.fromURI({ + await CredentialRequestClientBuilderV1_0_11.fromURI({ uri: IRR_URI, metadata: getMockData('diwala')?.metadata as unknown as EndpointMetadata, }) diff --git a/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts b/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts new file mode 100644 index 00000000..eaa5d4af --- /dev/null +++ b/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts @@ -0,0 +1,316 @@ +import { KeyObject } from 'crypto'; + +import { + Alg, + EndpointMetadata, + getCredentialRequestForVersion, + getIssuerFromCredentialOfferPayload, + Jwt, + OpenId4VCIVersion, + ProofOfPossession, + URL_NOT_VALID, + WellKnownEndpoints, +} from '@sphereon/oid4vci-common'; +import * as jose from 'jose'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import nock from 'nock'; + +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' +import { getMockData } from './data/VciDataFixtures'; + +const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN'; + +const jwt: Jwt = { + header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' }, + payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL }, +}; + +const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1'; + +let keypair: KeyPair; + +async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { + if (!args.payload.aud) { + throw Error('aud required'); + } else if (!kid) { + throw Error('kid required'); + } + return await new jose.SignJWT({ ...args.payload }) + .setProtectedHeader({ alg: 'ES256' }) + .setIssuedAt() + .setIssuer(kid) + .setAudience(args.payload.aud) + .setExpirationTime('2h') + .sign(keypair.privateKey); +} + +interface KeyPair { + publicKey: KeyObject; + privateKey: KeyObject; +} + +beforeAll(async () => { + const { privateKey, publicKey } = await jose.generateKeyPair('ES256'); + keypair = { publicKey: publicKey as KeyObject, privateKey: privateKey as KeyObject }; +}); + +beforeEach(async () => { + nock.cleanAll(); + nock(IDENTIPROOF_ISSUER_URL).get(WellKnownEndpoints.OPENID4VCI_ISSUER).reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA)); +}); + +afterEach(async () => { + nock.cleanAll(); +}); +describe('Credential Request Client ', () => { + it('should get a failed credential response with an unsupported format', async function () { + const basePath = 'https://sphereonjunit2022101301.com/'; + nock(basePath).post(/.*/).reply(500, { + error: 'unsupported_format', + error_description: 'This is a mock error message', + }); + + const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: INITIATION_TEST_V1_0_08 }) + .withCredentialEndpoint(basePath + '/credential') + .withFormat('ldp_vc') + .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential') + .build(); + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + // .withEndpointMetadata(metadata) + .withClientId('sphereon:wallet') + .withKid(kid) + .build(); + expect(credReqClient.getCredentialEndpoint()).toEqual(basePath + '/credential'); + const credentialRequest = await credReqClient.createCredentialRequest({ proofInput: proof, version: OpenId4VCIVersion.VER_1_0_08 }); + expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy(); + const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest); + expect(result?.errorBody?.error).toBe('unsupported_format'); + }); + + it('should get success credential response', async function () { + const mockedVC = + 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA'; + nock('https://oidc4vci.demo.spruceid.com') + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); + const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ request: INITIATION_TEST }) + .withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential') + .withFormat('jwt_vc') + .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential') + .build(); + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + // .withEndpointMetadata(metadata) + .withKid(kid) + .withClientId('sphereon:wallet') + .build(); + const credentialRequest = await credReqClient.createCredentialRequest({ + proofInput: proof, + format: 'jwt', + version: OpenId4VCIVersion.VER_1_0_08, + }); + expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy(); + expect(credentialRequest.format).toEqual('jwt_vc'); + const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest); + expect(result?.successBody?.credential).toEqual(mockedVC); + }); + + it('should fail with invalid url', async () => { + const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest({ request: INITIATION_TEST }) + .withCredentialEndpoint('httpsf://oidc4vci.demo.spruceid.com/credential') + .withFormat('jwt_vc') + .withCredentialType('https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential') + .build(); + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + // .withEndpointMetadata(metadata) + .withKid(kid) + .withClientId('sphereon:wallet') + .build(); + await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json', types: ['random'], proof })).rejects.toThrow( + Error(URL_NOT_VALID), + ); + }); +}); + +describe('Credential Request Client with Walt.id ', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + it('should have correct metadata endpoints', async function () { + nock.cleanAll(); + const WALT_IRR_URI = + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false'; + const credentialOffer = await CredentialOfferClientV1_0_11.fromURI(WALT_IRR_URI); + + const request = credentialOffer.credential_offer; + const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string); + expect(metadata.credential_endpoint).toEqual(WALT_OID4VCI_METADATA.credential_endpoint); + expect(metadata.token_endpoint).toEqual(WALT_OID4VCI_METADATA.token_endpoint); + + const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ + credentialOffer, + metadata, + }).build(); + expect(credReqClient.credentialRequestOpts.credentialEndpoint).toBe(WALT_OID4VCI_METADATA.credential_endpoint); + }); +}); + +describe('Credential Request Client with different issuers ', () => { + beforeEach(() => { + nock.cleanAll(); + }); + + afterEach(() => { + nock.cleanAll(); + }); + 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'; + const credentialRequest = await ( + await CredentialRequestClientBuilderV1_0_11.fromURI({ + uri: IRR_URI, + metadata: getMockData('spruce')?.metadata as unknown as EndpointMetadata, + }) + ) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: 'jwt', + jwt: getMockData('spruce')?.credential.request.proof.jwt as string, + }, + credentialTypes: ['OpenBadgeCredential'], + format: 'jwt_vc', + version: OpenId4VCIVersion.VER_1_0_08, + }); + const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08); + expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request); + }); + + it('should create correct CredentialRequest for Walt', async () => { + nock.cleanAll(); + const IRR_URI = + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwMTc4OTNjYy04ZTY3LTQxNzItYWZlOS1lODcyYmYxNDBlNWMiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.ODfq2AIhOcB61dAb3zMrXBJjPJaf53zkeHh_AssYyYA&user_pin_required=false'; + const credentialOffer = await ( + await CredentialRequestClientBuilderV1_0_11.fromURI({ + uri: IRR_URI, + metadata: getMockData('walt')?.metadata as unknown as EndpointMetadata, + }) + ) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: 'jwt', + jwt: getMockData('walt')?.credential.request.proof.jwt as string, + }, + credentialTypes: ['OpenBadgeCredential'], + format: 'jwt_vc', + version: OpenId4VCIVersion.VER_1_0_08, + }); + expect(credentialOffer).toEqual(getMockData('walt')?.credential.request); + }); + + // Missing the issuer required property + xit('should create correct CredentialRequest for uniissuer', async () => { + const IRR_URI = + 'https://oidc4vc.uniissuer.io/?credential_type=OpenBadgeCredential&pre-authorized_code=0ApoI8rxVmdQ44RIpuDbFIURIIkOhyek&user_pin_required=false'; + const credentialOffer = await ( + await CredentialRequestClientBuilderV1_0_11.fromURI({ + uri: IRR_URI, + metadata: getMockData('uniissuer')?.metadata as unknown as EndpointMetadata, + }) + ) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: 'jwt', + jwt: getMockData('uniissuer')?.credential.request.proof.jwt as string, + }, + credentialTypes: ['OpenBadgeCredential'], + format: 'jwt_vc', + version: OpenId4VCIVersion.VER_1_0_08, + }); + expect(credentialOffer).toEqual(getMockData('uniissuer')?.credential.request); + }); + + it('should create correct CredentialRequest for mattr', async () => { + const IRR_URI = + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=g0UCOj6RAN5AwHU6gczm_GzB4_lH6GW39Z0Dl2DOOiO'; + const credentialOffer = await ( + await CredentialRequestClientBuilderV1_0_11.fromURI({ + uri: IRR_URI, + metadata: getMockData('mattr')?.metadata as unknown as EndpointMetadata, + }) + ) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: 'jwt', + jwt: getMockData('mattr')?.credential.request.proof.jwt as string, + }, + credentialTypes: ['OpenBadgeCredential'], + format: 'ldp_vc', + version: OpenId4VCIVersion.VER_1_0_08, + }); + const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08); + expect(credentialRequest).toEqual(getMockData('mattr')?.credential.request); + }); + + it('should create correct CredentialRequest for diwala', async () => { + const IRR_URI = + 'openid-initiate-issuance://?issuer=https://oidc4vc.diwala.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJhbGciOiJIUzI1NiJ9.eyJjcmVkZW50aWFsX3R5cGUiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZXhwIjoxNjgxOTg0NDY3fQ.fEAHKz2nuWfiYHw406iNxr-81pWkNkbi31bWsYSf6Ng'; + const credentialOffer = await ( + await CredentialRequestClientBuilderV1_0_11.fromURI({ + uri: IRR_URI, + metadata: getMockData('diwala')?.metadata as unknown as EndpointMetadata, + }) + ) + .build() + .createCredentialRequest({ + proofInput: { + proof_type: 'jwt', + jwt: getMockData('diwala')?.credential.request.proof.jwt as string, + }, + credentialTypes: ['OpenBadgeCredential'], + format: 'ldp_vc', + version: OpenId4VCIVersion.VER_1_0_08, + }); + + // createCredentialRequest returns uniform format in draft 11 + const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08); + + expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request); + }); +}); diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index bbcd68de..4b4056d6 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -1,17 +1,24 @@ import { AccessTokenResponse, - Alg, + 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, CredentialRequestClientBuilder, OpenID4VCIClientV1_0_11, ProofOfPossessionBuilder } from '..'; +import { + AccessTokenClient, AccessTokenClientV1_0_11, + CredentialOfferClientV1_0_11, + CredentialRequestClientBuilder, + CredentialRequestClientBuilderV1_0_11, + OpenID4VCIClientV1_0_11, + ProofOfPossessionBuilder +} from '..' import { CredentialOfferClient } from '../CredentialOfferClient'; import { IDENTIPROOF_AS_METADATA, IDENTIPROOF_AS_URL, IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA } from './MetadataMocks'; @@ -47,11 +54,14 @@ describe('OID4VCI-Client should', () => { }; const mockedVC = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA'; - const INITIATE_QR = + const INITIATE_QR_V1_0_08 = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredentialUrl&pre-authorized_code=4jLs9xZHEfqcoow0kHE7d1a8hUk6Sy-5bVSV2MqBUGUgiFFQi-ImL62T-FmLIo8hKA1UdMPH0lM1xAgcFkJfxIw9L-lI3mVs0hRT8YVwsEM1ma6N3wzuCdwtMU4bcwKp&user_pin_required=true'; - const OFFER_QR = + const OFFER_QR_V1_0_08 = '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'; + function succeedWithAFullFlowWithClientSetup() { nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA)); nock(IDENTIPROOF_AS_URL).get('/.well-known/oauth-authorization-server').reply(200, JSON.stringify(IDENTIPROOF_AS_METADATA)); @@ -70,7 +80,7 @@ describe('OID4VCI-Client should', () => { it('succeed with a full flow with the client using OpenID4VCI version 9', async () => { succeedWithAFullFlowWithClientSetup(); const client = await OpenID4VCIClientV1_0_11.fromURI({ - uri: INITIATE_QR, + uri: INITIATE_QR_V1_0_08, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, clientId: 'test-clientId', @@ -81,7 +91,7 @@ describe('OID4VCI-Client should', () => { test.skip('succeed with a full flow wit the client using OpenID4VCI version 11', async () => { succeedWithAFullFlowWithClientSetup(); const client = await OpenID4VCIClientV1_0_11.fromURI({ - uri: OFFER_QR, + uri: OFFER_QR_V1_0_08, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, clientId: 'test-clientId', @@ -110,10 +120,10 @@ describe('OID4VCI-Client should', () => { } it( - 'succeed with a full flow without the client', + 'succeed with a full flow without the client v1_0_11', async () => { /* Convert the URI into an object */ - const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR); + const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClientV1_0_11.fromURI(INITIATE_QR_V1_0_08); expect(credentialOffer.baseUrl).toEqual('openid-initiate-issuance://'); expect(credentialOffer.original_credential_offer).toEqual({ @@ -124,6 +134,69 @@ describe('OID4VCI-Client should', () => { user_pin_required: 'true', }); + nock(ISSUER_URL) + .post(/token.*/) + .reply(200, JSON.stringify(mockedAccessTokenResponse)); + + /* The actual access token calls */ + const accessTokenClient: AccessTokenClientV1_0_11 = new AccessTokenClientV1_0_11(); + const accessTokenResponse = await accessTokenClient.acquireAccessToken({ credentialOffer: credentialOffer, pin: '1234' }); + expect(accessTokenResponse.successBody).toEqual(mockedAccessTokenResponse); + // Get the credential + nock(ISSUER_URL) + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); + const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: credentialOffer }) + .withFormat('jwt_vc') + + .withTokenFromResponse(accessTokenResponse.successBody!) + .build(); + + //TS2322: Type '(args: ProofOfPossessionCallbackArgs) => Promise' + // is not assignable to type 'ProofOfPossessionCallback'. + // Types of parameters 'args' and 'args' are incompatible. + // Property 'kid' is missing in type '{ header: unknown; payload: unknown; }' but required in type 'ProofOfPossessionCallbackArgs'. + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + 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(); + const credResponse = await credReqClient.acquireCredentialsUsingProof({ proofInput: proof }); + expect(credResponse.successBody?.credential).toEqual(mockedVC); + }, + UNIT_TEST_TIMEOUT, + ); + + it( + 'succeed with a full flow without the client v1_0_13', + async () => { + /* Convert the URI into an object */ + const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_13); + + 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" + } + } + }); + nock(ISSUER_URL) .post(/token.*/) .reply(200, JSON.stringify(mockedAccessTokenResponse)); @@ -163,7 +236,7 @@ describe('OID4VCI-Client should', () => { }) .withKid('did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1') .build(); - const credResponse = await credReqClient.acquireCredentialsUsingProof({ proofInput: proof }); + 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 115beb3e..17b0d659 100644 --- a/packages/client/lib/__tests__/IssuanceInitiation.spec.ts +++ b/packages/client/lib/__tests__/IssuanceInitiation.spec.ts @@ -5,41 +5,12 @@ import { CredentialOfferClient } from '../CredentialOfferClient'; import { INITIATION_TEST, INITIATION_TEST_HTTPS_URI, INITIATION_TEST_URI } from './MetadataMocks'; describe('Issuance Initiation', () => { - it('Should return Issuance Initiation Request with base URL from https URI', async () => { - expect(await CredentialOfferClient.fromURI(INITIATION_TEST_HTTPS_URI)).toEqual({ - baseUrl: 'https://server.example.com', - credential_offer: { - credential_issuer: 'https://server.example.com', - credentials: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'], - grants: { - authorization_code: { - issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', - }, - }, - }, - issuerState: 'eyJhbGciOiJSU0Et...FYUaBy', - original_credential_offer: { - credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'], - issuer: 'https://server.example.com', - op_state: 'eyJhbGciOiJSU0Et...FYUaBy', - }, - scheme: 'https', - supportedFlows: ['Authorization Code Flow'], - version: 1008, - }); - }); - it('Should return Issuance Initiation Request with base URL from openid-initiate-issuance URI', async () => { expect(await CredentialOfferClient.fromURI(INITIATION_TEST_URI)).toEqual(INITIATION_TEST); }); it('Should return Issuance Initiation URI from request', async () => { - expect(CredentialOfferClient.toURI(INITIATION_TEST)).toEqual(INITIATION_TEST_URI); - }); - - it('Should return URI from Issuance Initiation Request', async () => { - const issuanceInitiationClient = await CredentialOfferClient.fromURI(INITIATION_TEST_HTTPS_URI); - expect(CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI); + 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 () => { @@ -49,9 +20,9 @@ describe('Issuance Initiation', () => { it('Should return Credential Offer', async () => { const client = await CredentialOfferClient.fromURI( - 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credential_configuration_ids%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', ); - expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11); + expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_13); expect(client.baseUrl).toEqual('openid-credential-offer://'); expect(client.scheme).toEqual('openid-credential-offer'); expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); diff --git a/packages/client/lib/__tests__/IssuanceInitiationV1_0_11.spec.ts b/packages/client/lib/__tests__/IssuanceInitiationV1_0_11.spec.ts new file mode 100644 index 00000000..d1e82e49 --- /dev/null +++ b/packages/client/lib/__tests__/IssuanceInitiationV1_0_11.spec.ts @@ -0,0 +1,62 @@ +import { OpenId4VCIVersion } from '@sphereon/oid4vci-common'; + +import { CredentialOfferClient } from '../CredentialOfferClient'; +import { CredentialOfferClientV1_0_11 } from '../CredentialOfferClientV1_0_11'; + +import { INITIATION_TEST_HTTPS_URI, INITIATION_TEST_HTTPS_URI_V1_0_11, INITIATION_TEST_URI_V1_0_08, INITIATION_TEST_V1_0_08 } from './MetadataMocks'; + +describe('Issuance Initiation V1_0_11', () => { + it('Should return Issuance Initiation Request with base URL from https URI', async () => { + expect(await CredentialOfferClientV1_0_11.fromURI(INITIATION_TEST_HTTPS_URI)).toEqual({ + baseUrl: 'https://server.example.com', + credential_offer: { + credential_issuer: 'https://server.example.com', + credentials: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'], + grants: { + authorization_code: { + issuer_state: 'eyJhbGciOiJSU0Et...FYUaBy', + }, + }, + }, + issuerState: 'eyJhbGciOiJSU0Et...FYUaBy', + original_credential_offer: { + credential_type: ['https://did.example.org/healthCard', 'https://did.example.org/driverLicense'], + issuer: 'https://server.example.com', + op_state: 'eyJhbGciOiJSU0Et...FYUaBy', + }, + scheme: 'https', + supportedFlows: ['Authorization Code Flow'], + userPinRequired: false, + version: 1008, + }); + }); + + it('Should return Issuance Initiation Request with base URL from openid-initiate-issuance URI', async () => { + expect(await CredentialOfferClientV1_0_11.fromURI(INITIATION_TEST_URI_V1_0_08)).toEqual(INITIATION_TEST_V1_0_08); + }); + + it('Should return Issuance Initiation URI from request', async () => { + expect(CredentialOfferClientV1_0_11.toURI(INITIATION_TEST_V1_0_08)).toEqual(INITIATION_TEST_URI_V1_0_08); + }); + + it('Should return URI from Issuance Initiation Request', async () => { + const issuanceInitiationClient = await CredentialOfferClientV1_0_11.fromURI(INITIATION_TEST_HTTPS_URI_V1_0_11); + expect(CredentialOfferClient.toURI(issuanceInitiationClient)).toEqual(INITIATION_TEST_HTTPS_URI_V1_0_11); + }); + + it('Should throw error on invalid URI', async () => { + const issuanceInitiationURI = INITIATION_TEST_HTTPS_URI.replace('?', ''); + await expect(async () => CredentialOfferClientV1_0_11.fromURI(issuanceInitiationURI)).rejects.toThrow('Invalid Credential Offer Request'); + }); + + it('Should return Credential Offer', async () => { + const client = await CredentialOfferClientV1_0_11.fromURI( + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D', + ); + expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11); + expect(client.baseUrl).toEqual('openid-credential-offer://'); + expect(client.scheme).toEqual('openid-credential-offer'); + expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io'); + expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X'); + }); +}); diff --git a/packages/client/lib/__tests__/MetadataClient.spec.ts b/packages/client/lib/__tests__/MetadataClient.spec.ts index f1afbd7d..51af2bf8 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 { CredentialOfferClient } from '../CredentialOfferClient'; +import { CredentialOfferClientV1_0_11 } from '../CredentialOfferClientV1_0_11' import { MetadataClient } from '../MetadataClient'; import { @@ -47,7 +47,7 @@ describe('MetadataClient with IdentiProof Issuer should', () => { const INITIATE_URI = 'openid-initiate-issuance://?issuer=https%3A%2F%2Fissuer.research.identiproof.io&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false'; - const initiation = await CredentialOfferClient.fromURI(INITIATE_URI); + const initiation = await CredentialOfferClientV1_0_11.fromURI(INITIATE_URI); const metadata = await MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(initiation.credential_offer) as string); expect(metadata.credential_endpoint).toEqual('https://issuer.research.identiproof.io/credential'); expect(metadata.token_endpoint).toEqual('https://auth.research.identiproof.io/oauth2/token'); diff --git a/packages/client/lib/__tests__/MetadataMocks.ts b/packages/client/lib/__tests__/MetadataMocks.ts index 4932267e..2ca57095 100644 --- a/packages/client/lib/__tests__/MetadataMocks.ts +++ b/packages/client/lib/__tests__/MetadataMocks.ts @@ -1,4 +1,8 @@ -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'; @@ -7,9 +11,14 @@ export const DANUBE_ISSUER_URL = 'https://oidc4vc.uniissuer.io'; export const WALT_ISSUER_URL = 'https://jff.walt.id/issuer-api/oidc'; export const INITIATION_TEST_HTTPS_URI = 'https://server.example.com?issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy'; +export const INITIATION_TEST_HTTPS_URI_V1_0_11 = + 'https://server.example.com?issuer=https%3A%2F%2Fserver%2Eexample%2Ecom&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FhealthCard&credential_type=https%3A%2F%2Fdid%2Eexample%2Eorg%2FdriverLicense&op_state=eyJhbGciOiJSU0Et...FYUaBy'; export const INITIATION_TEST_URI = '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%22Please%20provide%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'; +export const INITIATION_TEST_URI_V1_0_08 = + 'openid-initiate-issuance://?credential_type=OpenBadgeCredential&issuer=https%3A%2F%2Fjff%2Ewalt%2Eid%2Fissuer-api%2Foidc%2F&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false'; + export const INITIATION_TEST: CredentialOfferRequestWithBaseUrl = { baseUrl: 'openid-credential-offer://', credential_offer: { @@ -44,6 +53,33 @@ export const INITIATION_TEST: CredentialOfferRequestWithBaseUrl = { supportedFlows: [AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW], version: 1013, }; +export const INITIATION_TEST_V1_0_08: CredentialOfferRequestWithBaseUrl = { + baseUrl: 'openid-initiate-issuance://', + credential_offer: { + credential_issuer: 'https://jff.walt.id/issuer-api/oidc/', + credentials: ['OpenBadgeCredential'], + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE', + user_pin_required: false, + }, + }, + }, + original_credential_offer: { + credential_type: ['OpenBadgeCredential'], + issuer: 'https://jff.walt.id/issuer-api/oidc/', + 'pre-authorized_code': + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE', + user_pin_required: 'false', + }, + preAuthorizedCode: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE', + scheme: 'openid-initiate-issuance', + supportedFlows: [AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW], + userPinRequired: false, + version: 1008, +} as CredentialOfferRequestWithBaseUrl; export const IDENTIPROOF_AS_METADATA = { issuer: 'https://auth.research.identiproof.io', authorization_endpoint: 'https://auth.research.identiproof.io/oauth2/authorize', diff --git a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts index 922deef4..4e7fe6af 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClient.spec.ts @@ -3,20 +3,20 @@ import { CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-commo // @ts-ignore import nock from 'nock'; -import { OpenID4VCIClientV1_0_11 } from '../OpenID4VCIClientV1_0_11'; +import { OpenID4VCIClient } from '../OpenID4VCIClient'; const MOCK_URL = 'https://server.example.com/'; describe('OpenID4VCIClient should', () => { - let client: OpenID4VCIClientV1_0_11; + let client: OpenID4VCIClient; beforeEach(async () => { nock(MOCK_URL).get(/.*/).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {}); nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); - client = await OpenID4VCIClientV1_0_11.fromURI({ + client = await OpenID4VCIClient.fromURI({ clientId: 'test-client', - uri: 'openid-initiate-issuance://?issuer=https://server.example.com&credential_type=TestCredential', + uri: 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fserver.example.com%22%2C%22credential_configuration_ids%22%3A%5B%22TestCredential%22%5D%7D', createAuthorizationRequestURL: false, }); }); @@ -77,7 +77,7 @@ describe('OpenID4VCIClient should', () => { nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(200, {}); nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(200, {}); // Use a client with issuer only to trigger the error - client = await OpenID4VCIClientV1_0_11.fromCredentialIssuer({ + client = await OpenID4VCIClient.fromCredentialIssuer({ credentialIssuer: MOCK_URL, createAuthorizationRequestURL: false, retrieveServerMetadata: false, diff --git a/packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts similarity index 99% rename from packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts rename to packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts index b3da4758..b8c7a756 100644 --- a/packages/client/lib/__tests__/OpenID4VCIClientPAR.spec.ts +++ b/packages/client/lib/__tests__/OpenID4VCIClientPARV1_0_11.spec.ts @@ -6,7 +6,7 @@ import nock from 'nock'; import { OpenID4VCIClientV1_0_11 } from '../OpenID4VCIClientV1_0_11'; const MOCK_URL = 'https://server.example.com/'; -describe('OpenID4VCIClient', () => { +describe('OpenID4VCIClientV1_0_11', () => { let client: OpenID4VCIClientV1_0_11; beforeEach(async () => { diff --git a/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts new file mode 100644 index 00000000..4332bce3 --- /dev/null +++ b/packages/client/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts @@ -0,0 +1,202 @@ +import { CodeChallengeMethod, WellKnownEndpoints } from '@sphereon/oid4vci-common'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import nock from 'nock'; + +import { OpenID4VCIClientV1_0_11 } from '../OpenID4VCIClientV1_0_11'; + +const MOCK_URL = 'https://server.example.com/'; + +describe('OpenID4VCIClientV1_0_11 should', () => { + let client: OpenID4VCIClientV1_0_11; + + beforeEach(async () => { + nock(MOCK_URL).get(/.*/).reply(200, {}); + nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {}); + nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); + client = await OpenID4VCIClientV1_0_11.fromURI({ + clientId: 'test-client', + uri: 'openid-initiate-issuance://?issuer=https://server.example.com&credential_type=TestCredential', + createAuthorizationRequestURL: false, + }); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + it('should successfully construct an authorization request url', 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({ + authorizationRequest: { + scope: 'openid 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 authorization endpoint is not set in server metadata', async () => { + await expect( + client.createAuthorizationRequestUrl({ + authorizationRequest: { + scope: 'openid TestCredential', + redirectUri: 'http://localhost:8881/cb', + }, + }), + ).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, {}); + nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(200, {}); + // Use a client with issuer only to trigger the error + client = await OpenID4VCIClientV1_0_11.fromCredentialIssuer({ + credentialIssuer: MOCK_URL, + createAuthorizationRequestURL: false, + retrieveServerMetadata: false, + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + client._state.endpointMetadata = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + credentialIssuerMetadata: { + authorization_endpoint: `${MOCK_URL}v1/auth/authorize`, + token_endpoint: `${MOCK_URL}/token`, + }, + }; + // client._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; + + await expect( + client.createAuthorizationRequestUrl({ + pkce: { + codeChallengeMethod: CodeChallengeMethod.S256, + codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', + }, + authorizationRequest: { + redirectUri: 'http://localhost:8881/cb', + }, + }), + ).rejects.toThrow(Error('Please provide a scope or authorization_details if no credential offer is present')); + }); + it('create an authorization request url with authorization_details array property', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; + + await expect( + client.createAuthorizationRequestUrl({ + pkce: { + codeChallengeMethod: CodeChallengeMethod.S256, + codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', + }, + authorizationRequest: { + authorizationDetails: [ + { + type: 'openid_credential', + format: 'ldp_vc', + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + { + type: 'openid_credential', + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + format: 'mso_mdoc', + doctype: 'org.iso.18013.5.1.mDL', + }, + ], + redirectUri: 'http://localhost:8881/cb', + }, + }), + ).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', + ); + }); + it('create an authorization request url with authorization_details object property', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + client._state.endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; + + await expect( + client.createAuthorizationRequestUrl({ + pkce: { + codeChallengeMethod: CodeChallengeMethod.S256, + codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', + }, + authorizationRequest: { + authorizationDetails: { + type: 'openid_credential', + format: 'ldp_vc', + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + redirectUri: 'http://localhost:8881/cb', + }, + }), + ).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', + ); + }); + it('create an authorization request url with authorization_details and scope', async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + client._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`; + + await expect( + client.createAuthorizationRequestUrl({ + pkce: { + codeChallengeMethod: CodeChallengeMethod.S256, + codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs', + }, + authorizationRequest: { + authorizationDetails: { + type: 'openid_credential', + format: 'ldp_vc', + locations: ['https://test.com'], + credential_definition: { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://www.w3.org/2018/credentials/examples/v1'], + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + scope: 'openid', + redirectUri: 'http://localhost:8881/cb', + }, + }), + ).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%22locations%22%3A%5B%22https%3A%2F%2Ftest%2Ecom%22%2C%22https%3A%2F%2Fserver%2Eexample%2Ecom%22%5D%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%7D&redirect_uri=http%3A%2F%2Flocalhost%3A8881%2Fcb&client_id=test-client&scope=openid', + ); + }); +}); diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index 9f03b0e3..f959a6da 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -1,9 +1,9 @@ -import { AccessTokenRequest, CredentialOfferPayloadV1_0_13, CredentialRequestV1_0_11, CredentialSupportedSdJwtVc } from '@sphereon/oid4vci-common' +import { AccessTokenRequest, CredentialOfferPayloadV1_0_13, CredentialRequestV1_0_11, CredentialSupportedSdJwtVc } from '@sphereon/oid4vci-common'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import nock from 'nock'; -import { OpenID4VCIClient } from '..' +import { OpenID4VCIClient } from '..'; import { createAccessTokenResponse, IssuerMetadataBuilderV1_13, VcIssuerBuilder } from '../../../issuer'; export const UNIT_TEST_TIMEOUT = 30000; @@ -68,7 +68,7 @@ describe('sd-jwt vc', () => { 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { tx_code: { input_mode: 'text', - length: 3 + length: 3, }, 'pre-authorized_code': '123', }, @@ -81,7 +81,7 @@ describe('sd-jwt vc', () => { 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_configuration_ids%22%3A%5B%22SdJwtCredentialId%22%5D%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_issuer%22%3A%22https%3A%2F%2Fexample.com%22%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredentialId%22%5D%7D', ); const client = await OpenID4VCIClient.fromURI({ diff --git a/packages/client/lib/index.ts b/packages/client/lib/index.ts index 8799e7e7..38949357 100644 --- a/packages/client/lib/index.ts +++ b/packages/client/lib/index.ts @@ -1,11 +1,13 @@ export * from './AccessTokenClient'; +export * from './AccessTokenClientV1_0_11'; export * from './AuthorizationCodeClient'; export * from './AuthorizationCodeClientV1_0_11'; export * from './CredentialRequestClient'; export * from './CredentialOfferClient'; export * from './CredentialOfferClientV1_0_11'; -export * from './CredentialRequestClient'; +export * from './CredentialRequestClientV1_0_11'; export * from './CredentialRequestClientBuilder'; +export * from './CredentialRequestClientBuilderV1_0_11'; export * from './functions'; export * from './MetadataClient'; export * from './OpenID4VCIClient'; diff --git a/packages/common/lib/functions/CredentialOfferUtil.ts b/packages/common/lib/functions/CredentialOfferUtil.ts index b8557635..f8fb975b 100644 --- a/packages/common/lib/functions/CredentialOfferUtil.ts +++ b/packages/common/lib/functions/CredentialOfferUtil.ts @@ -37,7 +37,7 @@ export function determineSpecVersionFromURI(uri: string): OpenId4VCIVersion { // version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_09, 'initiate_issuance_uri') version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'credentials'); - version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'urn:ietf:params:oauth:grant-type:pre-authorized_code'); + version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_11, 'grants.user_pin_required'); version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_13, 'credential_configuration_ids'); version = getVersionFromURIParam(uri, version, OpenId4VCIVersion.VER_1_0_13, 'tx_code'); diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index f423e4a4..defe23a4 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -4,6 +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' export type InputCharSet = 'numeric' | 'text'; export type KeyProofType = 'jwt' | 'cwt' | 'ldp_vp'; @@ -204,7 +205,7 @@ export interface ErrorResponse extends Response { state?: string; } -export type UniformCredentialRequest = CredentialRequestV1_0_11; +export type UniformCredentialRequest = CredentialRequestV1_0_11 | CredentialRequestV1_0_13; export interface CommonCredentialRequest { format: OID4VCICredentialFormat /* | OID4VCICredentialFormat[];*/; // for now it seems only one is supported in the spec diff --git a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts index 68d54bb2..bab73fca 100644 --- a/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts +++ b/packages/issuer-rest/lib/__tests__/ClientIssuerIT.spec.ts @@ -196,12 +196,12 @@ describe('VcIssuer', () => { }, }, }, - credentials: ['UniversityDegree_JWT'], + credentials: { 'UniversityDegree_JWT': {format: 'ldp_vc', id: 'UniversityDegree_JWT'} as CredentialConfigurationSupported }, scheme: 'http', }) .then((response) => response.uri) expect(uri).toEqual( - '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%22test_code%22%7D%7D%2C%22credentials%22%3A%5B%22UniversityDegree_JWT%22%5D%2C%22credential_issuer%22%3A%22http%3A%2F%2Flocalhost%3A3456%2Ftest%22%2C%22credential_configuration_ids%22%3A%5B%22UniversityDegree_JWT%22%5D%7D', + '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%22test_code%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', ) }) @@ -222,7 +222,7 @@ describe('VcIssuer', () => { issuer_state: 'previously-created-state', }, 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { - 'pre-authorized_code': 'test_code' + 'pre-authorized_code': 'test_code', }, }, }, @@ -310,6 +310,7 @@ describe('VcIssuer', () => { accessToken = await client.acquireAccessToken({ pin: credOfferSession.userPin }) expect(accessToken).toBeDefined() }) + it('should issue credential', async () => { async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { return await new jose.SignJWT({ ...args.payload }) diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index 6502b970..9f179fd4 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -99,15 +99,14 @@ export class VcIssuer { }): Promise { let preAuthorizedCode: string | undefined = undefined let issuerState: string | undefined = undefined - const { grants, credentials, credentialDefinition } = opts + const { grants, credentials } = opts if (!grants?.authorization_code && !grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']) { throw Error(`No grant issuer state or pre-authorized code could be deduced`) } const credentialOfferPayload: CredentialOfferPayloadV1_0_13 = { ...(grants && { grants }), - ...(credentials && { credentials }), - ...(credentialDefinition && { credential_configuration_ids: credentials? Object.keys(credentials): []}), + ...(credentials && { credential_configuration_ids: credentials ? Object.keys(credentials) : [] }), credential_issuer: this.issuerMetadata.credential_issuer, } as CredentialOfferPayloadV1_0_13 if (grants?.authorization_code) { diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index a280b8d5..c10cd372 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -263,9 +263,9 @@ describe('VcIssuer', () => { scheme: 'http', baseUri: 'issuer-example.com', credentials: { - 'Credential': { - format: 'ldp_vc' - } as CredentialConfigurationSupported + Credential: { + format: 'ldp_vc', + } as CredentialConfigurationSupported, }, credentialOfferUri: 'https://somehost.com/offer-id', }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebd9ee67..86f6fcb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 3.2.5 rimraf: specifier: ^5.0.5 - version: 5.0.5 + version: 5.0.6 ts-jest: specifier: ^29.1.2 version: 29.1.2(@babel/core@7.24.5)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.5))(jest@29.7.0(@types/node@18.19.33)(ts-node@10.9.2(@types/node@18.19.33)(typescript@5.4.5)))(typescript@5.4.5) @@ -1790,8 +1790,8 @@ packages: resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - '@sigstore/protobuf-specs@0.3.1': - resolution: {integrity: sha512-aIL8Z9NsMr3C64jyQzE0XlkEyBLpgEJJFDHLVVStkFV5Q3Il/r/YtY6NJWKQ4cy4AE7spP1IX5Jq7VCAxHHMfQ==} + '@sigstore/protobuf-specs@0.3.2': + resolution: {integrity: sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==} engines: {node: ^16.14.0 || >=18.0.0} '@sigstore/sign@1.0.0': @@ -3381,8 +3381,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.4.762: - resolution: {integrity: sha512-rrFvGweLxPwwSwJOjIopy3Vr+J3cIPtZzuc74bmlvmBIgQO3VYJDvVrlj94iKZ3ukXUH64Ex31hSfRTLqvjYJQ==} + electron-to-chromium@1.4.763: + resolution: {integrity: sha512-k4J8NrtJ9QrvHLRo8Q18OncqBCB7tIUyqxRcJnlonQ0ioHKYB988GcDFF3ZePmnb8eHEopDs/wPHR/iGAFgoUQ==} elliptic@6.5.5: resolution: {integrity: sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==} @@ -6329,8 +6329,8 @@ packages: engines: {node: '>=14'} hasBin: true - rimraf@5.0.5: - resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + rimraf@5.0.6: + resolution: {integrity: sha512-X72SgyOf+1lFnGM6gYcmZ4+jMOwuT4E4SajKQzUIlI7EoR5eFHMhS/wf8Ll0mN+w2bxcIVldrJQ6xT7HFQywjg==} engines: {node: '>=14'} hasBin: true @@ -9756,13 +9756,13 @@ snapshots: '@sigstore/bundle@2.3.1': dependencies: - '@sigstore/protobuf-specs': 0.3.1 + '@sigstore/protobuf-specs': 0.3.2 '@sigstore/core@1.1.0': {} '@sigstore/protobuf-specs@0.2.1': {} - '@sigstore/protobuf-specs@0.3.1': {} + '@sigstore/protobuf-specs@0.3.2': {} '@sigstore/sign@1.0.0': dependencies: @@ -9776,7 +9776,7 @@ snapshots: dependencies: '@sigstore/bundle': 2.3.1 '@sigstore/core': 1.1.0 - '@sigstore/protobuf-specs': 0.3.1 + '@sigstore/protobuf-specs': 0.3.2 make-fetch-happen: 13.0.1 proc-log: 4.2.0 promise-retry: 2.0.1 @@ -9792,7 +9792,7 @@ snapshots: '@sigstore/tuf@2.3.3': dependencies: - '@sigstore/protobuf-specs': 0.3.1 + '@sigstore/protobuf-specs': 0.3.2 tuf-js: 2.2.1 transitivePeerDependencies: - supports-color @@ -9801,7 +9801,7 @@ snapshots: dependencies: '@sigstore/bundle': 2.3.1 '@sigstore/core': 1.1.0 - '@sigstore/protobuf-specs': 0.3.1 + '@sigstore/protobuf-specs': 0.3.2 '@sinclair/typebox@0.27.8': {} @@ -10885,7 +10885,7 @@ snapshots: browserslist@4.23.0: dependencies: caniuse-lite: 1.0.30001617 - electron-to-chromium: 1.4.762 + electron-to-chromium: 1.4.763 node-releases: 2.0.14 update-browserslist-db: 1.0.15(browserslist@4.23.0) @@ -11692,7 +11692,7 @@ snapshots: dependencies: jake: 10.9.1 - electron-to-chromium@1.4.762: {} + electron-to-chromium@1.4.763: {} elliptic@6.5.5: dependencies: @@ -14540,7 +14540,7 @@ snapshots: minizlib@2.1.2: dependencies: - minipass: 3.1.6 + minipass: 3.3.6 yallist: 4.0.0 mkdirp@0.5.6: @@ -15722,7 +15722,7 @@ snapshots: dependencies: glob: 9.3.5 - rimraf@5.0.5: + rimraf@5.0.6: dependencies: glob: 10.3.14 @@ -15909,7 +15909,7 @@ snapshots: dependencies: '@sigstore/bundle': 2.3.1 '@sigstore/core': 1.1.0 - '@sigstore/protobuf-specs': 0.3.1 + '@sigstore/protobuf-specs': 0.3.2 '@sigstore/sign': 2.3.1 '@sigstore/tuf': 2.3.3 '@sigstore/verify': 1.2.0