diff --git a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts index abebc5fa..572da72e 100644 --- a/packages/callback-example/lib/__tests__/issuerCallback.spec.ts +++ b/packages/callback-example/lib/__tests__/issuerCallback.spec.ts @@ -1,6 +1,6 @@ import { KeyObject } from 'crypto' -import { CredentialRequestClient, CredentialRequestClientBuilder, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client' +import { CredentialRequestClientBuilder, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client' import { Alg, CNonceState, @@ -252,7 +252,7 @@ describe('issuerCallback', () => { .withKid(kid) .build() - const credentialRequestClient = new CredentialRequestClient(credReqClient) + const credentialRequestClient = credReqClient.build() const credentialRequest: CredentialRequest = await credentialRequestClient.createCredentialRequest({ credentialIdentifier: 'VerifiableCredential', // format: 'jwt_vc_json', diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 6c0f4612..7959096a 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -19,7 +19,8 @@ import { ExperimentalSubjectIssuance } from '@sphereon/oid4vci-common/dist/exper import { CredentialFormat } from '@sphereon/ssi-types'; import Debug from 'debug'; -import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'; +import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; +import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; const debug = Debug('sphereon:oid4vci:credential'); @@ -78,7 +79,7 @@ export class CredentialRequestClient { return this.credentialRequestOpts.deferredCredentialEndpoint; } - public constructor(builder: CredentialRequestClientBuilder) { + public constructor(builder: CredentialRequestClientBuilderV1_0_13 | CredentialRequestClientBuilderV1_0_11) { this._credentialRequestOpts = { ...builder }; } diff --git a/packages/client/lib/CredentialRequestClientBuilder.ts b/packages/client/lib/CredentialRequestClientBuilder.ts index cb76e2e8..3b01cd41 100644 --- a/packages/client/lib/CredentialRequestClientBuilder.ts +++ b/packages/client/lib/CredentialRequestClientBuilder.ts @@ -1,12 +1,11 @@ import { AccessTokenResponse, + CredentialIssuerMetadata, CredentialIssuerMetadataV1_0_13, - CredentialOfferPayloadV1_0_13, CredentialOfferRequestWithBaseUrl, determineSpecVersionFromOffer, EndpointMetadata, ExperimentalSubjectIssuance, - getIssuerFromCredentialOfferPayload, OID4VCICredentialFormat, OpenId4VCIVersion, UniformCredentialOfferRequest, @@ -14,19 +13,21 @@ import { import { CredentialFormat } from '@sphereon/ssi-types'; import { CredentialOfferClient } from './CredentialOfferClient'; -import { CredentialRequestClient } from './CredentialRequestClient'; +import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; +import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13'; + +type CredentialRequestClientBuilderVersionSpecific = CredentialRequestClientBuilderV1_0_11 | CredentialRequestClientBuilderV1_0_13; + +function isV1_0_13(builder: CredentialRequestClientBuilderVersionSpecific): builder is CredentialRequestClientBuilderV1_0_13 { + return (builder as CredentialRequestClientBuilderV1_0_13).withCredentialIdentifier !== undefined; +} export class CredentialRequestClientBuilder { - credentialEndpoint?: string; - deferredCredentialEndpoint?: string; - deferredCredentialAwait = false; - deferredCredentialIntervalInMS = 5000; - credentialIdentifier?: string; - credentialTypes?: string[] = []; - format?: CredentialFormat | OID4VCICredentialFormat; - token?: string; - version?: OpenId4VCIVersion; - subjectIssuance?: ExperimentalSubjectIssuance; + private _builder: CredentialRequestClientBuilderVersionSpecific; + + private constructor(builder: CredentialRequestClientBuilderVersionSpecific) { + this._builder = builder; + } public static fromCredentialIssuer({ credentialIssuer, @@ -41,25 +42,40 @@ export class CredentialRequestClientBuilder { credentialIdentifier?: string; credentialTypes?: string | string[]; }): CredentialRequestClientBuilder { - const issuer = credentialIssuer; - const builder = new CredentialRequestClientBuilder(); - 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); - } - if (credentialIdentifier) { - builder.withCredentialIdentifier(credentialIdentifier); - } - if (credentialTypes) { - builder.withCredentialType(credentialTypes); + const specVersion = version ?? OpenId4VCIVersion.VER_1_0_13; + let builder; + + if (specVersion >= OpenId4VCIVersion.VER_1_0_13) { + builder = CredentialRequestClientBuilderV1_0_13.fromCredentialIssuer({ + credentialIssuer, + metadata, + version, + credentialIdentifier, + credentialTypes, + }); + } else { + if (!credentialTypes || credentialTypes.length === 0) { + throw new Error('CredentialTypes must be provided for v1_0_11'); + } + builder = CredentialRequestClientBuilderV1_0_11.fromCredentialIssuer({ + credentialIssuer, + metadata, + version, + credentialTypes, + }); } - return builder; + + return new CredentialRequestClientBuilder(builder); } public static async fromURI({ uri, metadata }: { uri: string; metadata?: EndpointMetadata }): Promise { const offer = await CredentialOfferClient.fromURI(uri); - return CredentialRequestClientBuilder.fromCredentialOfferRequest({ request: offer, ...offer, metadata, version: offer.version }); + return CredentialRequestClientBuilder.fromCredentialOfferRequest({ + request: offer, + ...offer, + metadata, + version: offer.version, + }); } public static fromCredentialOfferRequest(opts: { @@ -69,24 +85,17 @@ export class CredentialRequestClientBuilder { version?: OpenId4VCIVersion; metadata?: EndpointMetadata; }): CredentialRequestClientBuilder { - const { request, metadata } = opts; + const { request } = opts; const version = opts.version ?? request.version ?? determineSpecVersionFromOffer(request.original_credential_offer); + let builder; + if (version < OpenId4VCIVersion.VER_1_0_13) { - throw new Error('Versions below v1.0.13 (draft 13) are not supported.'); - } - const builder = new CredentialRequestClientBuilder(); - 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); - } - const ids: string[] = (request.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids; - // if there's only one in the offer, we pre-select it. if not, you should provide the credentialType - if (ids.length && ids.length === 1) { - builder.withCredentialIdentifier(ids[0]); + builder = CredentialRequestClientBuilderV1_0_11.fromCredentialOfferRequest(opts); + } else { + builder = CredentialRequestClientBuilderV1_0_13.fromCredentialOfferRequest(opts); } - return builder; + + return new CredentialRequestClientBuilder(builder); } public static fromCredentialOffer({ @@ -96,78 +105,100 @@ export class CredentialRequestClientBuilder { credentialOffer: CredentialOfferRequestWithBaseUrl; metadata?: EndpointMetadata; }): CredentialRequestClientBuilder { - return CredentialRequestClientBuilder.fromCredentialOfferRequest({ - request: credentialOffer, - metadata, - version: credentialOffer.version, - }); + const version = determineSpecVersionFromOffer(credentialOffer.credential_offer); + let builder; + + if (version < OpenId4VCIVersion.VER_1_0_13) { + builder = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ + credentialOffer, + metadata, + }); + } else { + builder = CredentialRequestClientBuilderV1_0_13.fromCredentialOffer({ + credentialOffer, + metadata, + }); + } + + return new CredentialRequestClientBuilder(builder); + } + + public getVersion(): OpenId4VCIVersion | undefined { + return this._builder.version; } - public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadataV1_0_13): this { - this.credentialEndpoint = metadata.credential_endpoint; + public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata | CredentialIssuerMetadataV1_0_13): this { + if (isV1_0_13(this._builder)) { + this._builder.withCredentialEndpointFromMetadata(metadata as CredentialIssuerMetadataV1_0_13); + } else { + this._builder.withCredentialEndpointFromMetadata(metadata as CredentialIssuerMetadata); + } return this; } public withCredentialEndpoint(credentialEndpoint: string): this { - this.credentialEndpoint = credentialEndpoint; + this._builder.withCredentialEndpoint(credentialEndpoint); return this; } - public withDeferredCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadataV1_0_13): this { - this.deferredCredentialEndpoint = metadata.deferred_credential_endpoint; + public withDeferredCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata | CredentialIssuerMetadataV1_0_13): this { + if (isV1_0_13(this._builder)) { + this._builder.withDeferredCredentialEndpointFromMetadata(metadata as CredentialIssuerMetadataV1_0_13); + } else { + this._builder.withDeferredCredentialEndpointFromMetadata(metadata as CredentialIssuerMetadata); + } return this; } public withDeferredCredentialEndpoint(deferredCredentialEndpoint: string): this { - this.deferredCredentialEndpoint = deferredCredentialEndpoint; + this._builder.withDeferredCredentialEndpoint(deferredCredentialEndpoint); return this; } public withDeferredCredentialAwait(deferredCredentialAwait: boolean, deferredCredentialIntervalInMS?: number): this { - this.deferredCredentialAwait = deferredCredentialAwait; - this.deferredCredentialIntervalInMS = deferredCredentialIntervalInMS ?? 5000; + this._builder.withDeferredCredentialAwait(deferredCredentialAwait, deferredCredentialIntervalInMS); return this; } public withCredentialIdentifier(credentialIdentifier: string): this { - this.credentialIdentifier = credentialIdentifier; + if (this._builder.version === undefined || this._builder.version < OpenId4VCIVersion.VER_1_0_13) { + throw new Error('Version of spec should be equal or higher than v1_0_13'); + } + (this._builder as CredentialRequestClientBuilderV1_0_13).withCredentialIdentifier(credentialIdentifier); return this; } public withCredentialType(credentialTypes: string | string[]): this { - this.credentialTypes = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes]; + this._builder.withCredentialType(credentialTypes); return this; } public withFormat(format: CredentialFormat | OID4VCICredentialFormat): this { - this.format = format; + this._builder.withFormat(format); return this; } public withSubjectIssuance(subjectIssuance: ExperimentalSubjectIssuance): this { - this.subjectIssuance = subjectIssuance; + this._builder.withSubjectIssuance(subjectIssuance); return this; } public withToken(accessToken: string): this { - this.token = accessToken; + this._builder.withToken(accessToken); return this; } public withTokenFromResponse(response: AccessTokenResponse): this { - this.token = response.access_token; + this._builder.withTokenFromResponse(response); return this; } public withVersion(version: OpenId4VCIVersion): this { - this.version = version; + this._builder.withVersion(version); return this; } - public build(): CredentialRequestClient { - if (!this.version) { - this.withVersion(OpenId4VCIVersion.VER_1_0_11); - } - return new CredentialRequestClient(this); + public build() { + return this._builder.build(); } } diff --git a/packages/client/lib/CredentialRequestClientBuilderV1_0_13.ts b/packages/client/lib/CredentialRequestClientBuilderV1_0_13.ts new file mode 100644 index 00000000..23508740 --- /dev/null +++ b/packages/client/lib/CredentialRequestClientBuilderV1_0_13.ts @@ -0,0 +1,173 @@ +import { + AccessTokenResponse, + CredentialIssuerMetadataV1_0_13, + CredentialOfferPayloadV1_0_13, + CredentialOfferRequestWithBaseUrl, + determineSpecVersionFromOffer, + EndpointMetadata, + ExperimentalSubjectIssuance, + getIssuerFromCredentialOfferPayload, + OID4VCICredentialFormat, + OpenId4VCIVersion, + UniformCredentialOfferRequest, +} from '@sphereon/oid4vci-common'; +import { CredentialFormat } from '@sphereon/ssi-types'; + +import { CredentialOfferClient } from './CredentialOfferClient'; +import { CredentialRequestClient } from './CredentialRequestClient'; + +export class CredentialRequestClientBuilderV1_0_13 { + credentialEndpoint?: string; + deferredCredentialEndpoint?: string; + deferredCredentialAwait = false; + deferredCredentialIntervalInMS = 5000; + credentialIdentifier?: string; + credentialTypes?: string[] = []; + format?: CredentialFormat | OID4VCICredentialFormat; + token?: string; + version?: OpenId4VCIVersion; + subjectIssuance?: ExperimentalSubjectIssuance; + + public static fromCredentialIssuer({ + credentialIssuer, + metadata, + version, + credentialIdentifier, + credentialTypes, + }: { + credentialIssuer: string; + metadata?: EndpointMetadata; + version?: OpenId4VCIVersion; + credentialIdentifier?: string; + credentialTypes?: string | string[]; + }): CredentialRequestClientBuilderV1_0_13 { + const issuer = credentialIssuer; + const builder = new CredentialRequestClientBuilderV1_0_13(); + builder.withVersion(version ?? OpenId4VCIVersion.VER_1_0_13); + builder.withCredentialEndpoint(metadata?.credential_endpoint ?? (issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`)); + if (metadata?.deferred_credential_endpoint) { + builder.withDeferredCredentialEndpoint(metadata.deferred_credential_endpoint); + } + if (credentialIdentifier) { + builder.withCredentialIdentifier(credentialIdentifier); + } + if (credentialTypes) { + builder.withCredentialType(credentialTypes); + } + return builder; + } + + public static async fromURI({ uri, metadata }: { uri: string; metadata?: EndpointMetadata }): Promise { + const offer = await CredentialOfferClient.fromURI(uri); + return CredentialRequestClientBuilderV1_0_13.fromCredentialOfferRequest({ request: offer, ...offer, metadata, version: offer.version }); + } + + public static fromCredentialOfferRequest(opts: { + request: UniformCredentialOfferRequest; + scheme?: string; + baseUrl?: string; + version?: OpenId4VCIVersion; + metadata?: EndpointMetadata; + }): CredentialRequestClientBuilderV1_0_13 { + const { request, metadata } = opts; + const version = opts.version ?? request.version ?? determineSpecVersionFromOffer(request.original_credential_offer); + if (version < OpenId4VCIVersion.VER_1_0_13) { + throw new Error('Versions below v1.0.13 (draft 13) are not supported.'); + } + const builder = new CredentialRequestClientBuilderV1_0_13(); + 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); + } + const ids: string[] = (request.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids; + // if there's only one in the offer, we pre-select it. if not, you should provide the credentialType + if (ids.length && ids.length === 1) { + builder.withCredentialIdentifier(ids[0]); + } + return builder; + } + + public static fromCredentialOffer({ + credentialOffer, + metadata, + }: { + credentialOffer: CredentialOfferRequestWithBaseUrl; + metadata?: EndpointMetadata; + }): CredentialRequestClientBuilderV1_0_13 { + return CredentialRequestClientBuilderV1_0_13.fromCredentialOfferRequest({ + request: credentialOffer, + metadata, + version: credentialOffer.version, + }); + } + + public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadataV1_0_13): this { + this.credentialEndpoint = metadata.credential_endpoint; + return this; + } + + public withCredentialEndpoint(credentialEndpoint: string): this { + this.credentialEndpoint = credentialEndpoint; + return this; + } + + public withDeferredCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadataV1_0_13): 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 withCredentialIdentifier(credentialIdentifier: string): this { + this.credentialIdentifier = credentialIdentifier; + 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 withSubjectIssuance(subjectIssuance: ExperimentalSubjectIssuance): this { + this.subjectIssuance = subjectIssuance; + 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(): CredentialRequestClient { + if (!this.version) { + this.withVersion(OpenId4VCIVersion.VER_1_0_11); + } + return new CredentialRequestClient(this); + } +} diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index d8543cdc..b7eb6947 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -42,7 +42,7 @@ import { createAuthorizationRequestUrl } from './AuthorizationCodeClient'; import { createAuthorizationRequestUrlV1_0_11 } from './AuthorizationCodeClientV1_0_11'; import { CredentialOfferClient } from './CredentialOfferClient'; import { CredentialRequestOpts } from './CredentialRequestClient'; -import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'; +import { CredentialRequestClientBuilderV1_0_13 } from './CredentialRequestClientBuilderV1_0_13'; import { CredentialRequestClientBuilderV1_0_11 } from './CredentialRequestClientBuilderV1_0_11'; import { MetadataClient } from './MetadataClient'; import { OpenID4VCIClientStateV1_0_11 } from './OpenID4VCIClientV1_0_11'; @@ -389,7 +389,7 @@ export class OpenID4VCIClient { if (jwk) this._state.jwk = jwk; if (kid) this._state.kid = kid; - let requestBuilder: CredentialRequestClientBuilder | CredentialRequestClientBuilderV1_0_11; + let requestBuilder: CredentialRequestClientBuilderV1_0_13 | CredentialRequestClientBuilderV1_0_11; if (this.version() < OpenId4VCIVersion.VER_1_0_13) { requestBuilder = this.credentialOffer ? CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ @@ -404,11 +404,11 @@ export class OpenID4VCIClient { }); } else { requestBuilder = this.credentialOffer - ? CredentialRequestClientBuilder.fromCredentialOffer({ + ? CredentialRequestClientBuilderV1_0_13.fromCredentialOffer({ credentialOffer: this.credentialOffer, metadata: this.endpointMetadata, }) - : CredentialRequestClientBuilder.fromCredentialIssuer({ + : CredentialRequestClientBuilderV1_0_13.fromCredentialIssuer({ credentialIssuer: this.getIssuer(), credentialTypes, metadata: this.endpointMetadata, diff --git a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts index e4fb5bac..64579f1f 100644 --- a/packages/client/lib/__tests__/CredentialRequestClient.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClient.spec.ts @@ -20,13 +20,8 @@ import * as jose from 'jose'; // @ts-ignore import nock from 'nock'; -import { - CredentialOfferClientV1_0_11, - CredentialRequestClientBuilder, - CredentialRequestClientBuilderV1_0_11, - MetadataClientV1_0_11, - ProofOfPossessionBuilder, -} from '..'; +import { CredentialOfferClient, MetadataClient, ProofOfPossessionBuilder } from '..'; +import { CredentialRequestClientBuilder } from '../CredentialRequestClientBuilder'; import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST, WALT_OID4VCI_METADATA } from './MetadataMocks'; import { getMockData } from './data/VciDataFixtures'; @@ -154,14 +149,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 CredentialOfferClientV1_0_11.fromURI(WALT_IRR_URI); + const credentialOffer = await CredentialOfferClient.fromURI(WALT_IRR_URI); const request = credentialOffer.credential_offer; - const metadata = await MetadataClientV1_0_11.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string); + 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({ + const credReqClient = CredentialRequestClientBuilder.fromCredentialOffer({ credentialOffer, metadata, }).build(); @@ -205,7 +200,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 CredentialRequestClientBuilderV1_0_11.fromURI({ + await CredentialRequestClientBuilder.fromURI({ uri: IRR_URI, metadata: getMockData('walt')?.metadata as unknown as EndpointMetadata, }) @@ -250,7 +245,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 CredentialRequestClientBuilderV1_0_11.fromURI({ + await CredentialRequestClientBuilder.fromURI({ uri: IRR_URI, metadata: getMockData('mattr')?.metadata as unknown as EndpointMetadata, }) @@ -273,7 +268,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 CredentialRequestClientBuilderV1_0_11.fromURI({ + await CredentialRequestClientBuilder.fromURI({ uri: IRR_URI, metadata: getMockData('diwala')?.metadata as unknown as EndpointMetadata, }) diff --git a/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts b/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts index 703f117f..f011edf1 100644 --- a/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClientBuilder.spec.ts @@ -11,11 +11,13 @@ import { } from '@sphereon/oid4vci-common'; import * as jose from 'jose'; -import { CredentialRequestClientBuilder, ProofOfPossessionBuilder } from '..'; +import { CredentialRequestOpts, ProofOfPossessionBuilder } from '..'; +import { CredentialRequestClientBuilder } from '../CredentialRequestClientBuilder'; import { IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA, INITIATION_TEST_URI, WALT_ISSUER_URL, WALT_OID4VCI_METADATA } from './MetadataMocks'; const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN'; +const partialJWT_withoutDid = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlYmZlYjFmNzEyZWJjNmYxYzI3N'; /*const jwtv1_0_08: Jwt = { header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' }, @@ -27,8 +29,15 @@ const jwtv1_0_11: Jwt = { payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL }, }; +const jwtv1_0_13_withoutDid: Jwt = { + header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+jwt' }, + payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL }, +}; + const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1'; +const kid_withoutDid = 'ebfeb1f712ebc6f1c276e12ec21/keys/1'; + let keypair: KeyPair; beforeAll(async () => { @@ -81,7 +90,7 @@ describe('Credential Request Client Builder', () => { .build(); expect(credReqClient.credentialRequestOpts.credentialEndpoint).toBe('https://oidc4vci.demo.spruceid.com/credential'); expect(credReqClient.credentialRequestOpts.format).toBe('jwt_vc'); - expect(credReqClient.credentialRequestOpts.credentialIdentifier).toStrictEqual('credentialType'); + expect((credReqClient.credentialRequestOpts as CredentialRequestOpts).credentialIdentifier).toStrictEqual('credentialType'); expect(credReqClient.credentialRequestOpts.token).toBe('token'); }); @@ -115,6 +124,35 @@ describe('Credential Request Client Builder', () => { } }); + it('should build credential request correctly without did', async () => { + const credReqClient = (await CredentialRequestClientBuilder.fromURI({ uri: INITIATION_TEST_URI })) + .withCredentialEndpoint('https://oidc4vci.demo.spruceid.com/credential') + .withFormat('jwt_vc') + .withCredentialType('OpenBadgeCredential') + .build(); + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt: jwtv1_0_13_withoutDid, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + verifyCallback: proofOfPossessionVerifierCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_13, + }) + .withClientId('sphereon:wallet') + .withKid(kid_withoutDid) + .build(); + await proofOfPossessionVerifierCallbackFunction({ ...proof, kid: kid_withoutDid }); + const credentialRequest: CredentialRequestV1_0_13 = await credReqClient.createCredentialRequest({ + proofInput: proof, + credentialTypes: 'OpenBadgeCredential', + version: OpenId4VCIVersion.VER_1_0_13, + }); + expect(credentialRequest.proof?.jwt).toContain(partialJWT_withoutDid); + if ('types' in credentialRequest) { + expect(credentialRequest.types).toStrictEqual(['OpenBadgeCredential']); + } + }); + it('should build correctly from metadata', async () => { const credReqClient = ( await CredentialRequestClientBuilder.fromURI({ diff --git a/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts b/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts index 216a2ac3..ebdda679 100644 --- a/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts +++ b/packages/client/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts @@ -28,14 +28,22 @@ import { import { getMockData } from './data/VciDataFixtures'; const partialJWT = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmN'; +const partialJWT_withoutDid = 'eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJlYmZlYjFmNzEyZWJjNmYxYzI3N'; 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 jwt_withoutDid: Jwt = { + header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' }, + payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL }, +}; + const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1'; +const kid_withoutDid = 'ebfeb1f712ebc6f1c276e12ec21/keys/1'; + let keypair: KeyPair; async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { @@ -102,6 +110,36 @@ describe('Credential Request Client ', () => { expect(result?.errorBody?.error).toBe('unsupported_format'); }); + it('should get a failed credential response with an unsupported format and without did', 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: jwt_withoutDid, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + // .withEndpointMetadata(metadata) + .withClientId('sphereon:wallet') + .withKid(kid_withoutDid) + .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_withoutDid)).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'; @@ -138,6 +176,42 @@ describe('Credential Request Client ', () => { expect(result?.successBody?.credential).toEqual(mockedVC); }); + it('should get success credential response without did', async function () { + const mockedVC = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJlYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImlhdCI6MTcxODM1NzcxOH0.7iiOTuIjQRyrIincYyDW6m0nBYmDoYfXcTYFrywsKEY'; + 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: jwt_withoutDid, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + // .withEndpointMetadata(metadata) + .withKid(kid_withoutDid) + .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_withoutDid)).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') @@ -159,6 +233,28 @@ describe('Credential Request Client ', () => { Error(URL_NOT_VALID), ); }); + + it('should fail with invalid url without did', 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: jwt_withoutDid, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + // .withEndpointMetadata(metadata) + .withKid(kid_withoutDid) + .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 ', () => { diff --git a/packages/client/lib/__tests__/IT.spec.ts b/packages/client/lib/__tests__/IT.spec.ts index 0a0f70e4..e14c599e 100644 --- a/packages/client/lib/__tests__/IT.spec.ts +++ b/packages/client/lib/__tests__/IT.spec.ts @@ -13,32 +13,52 @@ import { // @ts-ignore import nock from 'nock'; -import { - AccessTokenClient, - AccessTokenClientV1_0_11, - CredentialOfferClientV1_0_11, - CredentialRequestClientBuilder, - CredentialRequestClientBuilderV1_0_11, - OpenID4VCIClientV1_0_11, - ProofOfPossessionBuilder, -} from '..'; +import { AccessTokenClient, AccessTokenClientV1_0_11, OpenID4VCIClient, OpenID4VCIClientV1_0_13, ProofOfPossessionBuilder } from '..'; import { CredentialOfferClient } from '../CredentialOfferClient'; +import { CredentialRequestClientBuilder } from '../CredentialRequestClientBuilder'; -import { IDENTIPROOF_AS_METADATA, IDENTIPROOF_AS_URL, IDENTIPROOF_ISSUER_URL, IDENTIPROOF_OID4VCI_METADATA } from './MetadataMocks'; +import { + IDENTIPROOF_AS_METADATA, + IDENTIPROOF_AS_URL, + IDENTIPROOF_ISSUER_URL, + IDENTIPROOF_OID4VCI_METADATA, + IDENTIPROOF_OID4VCI_METADATA_v13, +} from './MetadataMocks'; export const UNIT_TEST_TIMEOUT = 30000; const ISSUER_URL = 'https://issuer.research.identiproof.io'; -const jwt = { +const jwtDid = { header: { alg: Alg.ES256, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+jwt' }, payload: { iss: 'test-clientId', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: ISSUER_URL }, }; +const jwtWithoutDid = { + header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'openid4vci-proof+jwt' }, + payload: { iss: 'test-clientId', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: ISSUER_URL }, +}; + +const mockedVC = + 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA'; + +// Access token mocks +const mockedAccessTokenResponse: AccessTokenResponse = { + access_token: 'ey6546.546654.64565', + authorization_pending: false, + c_nonce: 'c_nonce2022101300', + c_nonce_expires_in: 2025101300, + interval: 2025101300, + token_type: 'Bearer', +}; + +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,%22length%22:22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise { + return 'ey.val.ue'; +} describe('OID4VCI-Client should', () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise { - return 'ey.val.ue'; - } beforeEach(() => { nock.cleanAll(); }); @@ -55,8 +75,6 @@ describe('OID4VCI-Client should', () => { interval: 2025101300, token_type: 'Bearer', }; - const mockedVC = - 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzU2NTA0OSIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.z5vgMTK1nfizNCg5N-niCOL3WUIAL7nXy-nGhDZYO_-PNGeE-0djCpWAMH8fD8eWSID5PfkPBYkx_dfLJnQ7NA'; 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_V1_0_08 = @@ -67,14 +85,14 @@ describe('OID4VCI-Client should', () => { 'https://issuer.research.identiproof.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22eyJhbGciOiJSU0Et...FYUaBy%22%7D%7D%7D'; const HTTPS_OFFER_QR_PRE_AUTHORIZED = 'https://issuer.research.identiproof.io?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%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%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%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,%22length%22:22,%22description%22:%22Please%20enter%20the%20serial%20number%20of%20your%20physical%20drivers%20license%22%7D%7D%7D%7D'; + const HTTPS_OFFER_QR_PRE_AUTHORIZED_v13 = + 'https://issuer.research.identiproof.io?credential_offer=%7B%0A%20%20%20%20%22credential_issuer%22%3A%20%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%0A%20%20%20%20%22credential_configuration_ids%22%3A%20%5B%0A%20%20%20%20%20%20%20%20%22UniversityDegreeCredential%22%0A%20%20%20%20%5D%2C%0A%20%20%20%20%22grants%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22pre-authorized_code%22%3A%20%22adhjhdjajkdkhjhdj%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22tx_code%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22length%22%3A%204%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22input_mode%22%3A%20%22numeric%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22description%22%3A%20%22Please%20provide%20the%20one-time%20code%20that%20was%20sent%20via%20e-mail%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%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)); nock(IDENTIPROOF_AS_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); + nock(IDENTIPROOF_ISSUER_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(200, {}); nock(IDENTIPROOF_AS_URL) .post(/oauth2\/token.*/) .reply(200, JSON.stringify(mockedAccessTokenResponse)); @@ -88,7 +106,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({ + const client = await OpenID4VCIClient.fromURI({ uri: INITIATE_QR_V1_0_08, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, @@ -99,7 +117,7 @@ describe('OID4VCI-Client should', () => { it('succeed with a full flow with the client using OpenID4VCI version 11 and deeplink', async () => { succeedWithAFullFlowWithClientSetup(); - const client = await OpenID4VCIClientV1_0_11.fromURI({ + const client = await OpenID4VCIClient.fromURI({ uri: OFFER_QR_V1_0_08, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, @@ -110,7 +128,7 @@ describe('OID4VCI-Client should', () => { it('succeed with a full flow with the client using OpenID4VCI draft < 9 and https', async () => { succeedWithAFullFlowWithClientSetup(); - const client = await OpenID4VCIClientV1_0_11.fromURI({ + const client = await OpenID4VCIClient.fromURI({ uri: HTTPS_INITIATE_QR, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, @@ -121,7 +139,7 @@ describe('OID4VCI-Client should', () => { it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and authorization_code flow', async () => { succeedWithAFullFlowWithClientSetup(); - const client = await OpenID4VCIClientV1_0_11.fromURI({ + const client = await OpenID4VCIClient.fromURI({ uri: HTTPS_OFFER_QR_AUTHORIZATION_CODE, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, @@ -132,7 +150,65 @@ describe('OID4VCI-Client should', () => { it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and preauthorized_code flow', async () => { succeedWithAFullFlowWithClientSetup(); - const client = await OpenID4VCIClientV1_0_11.fromURI({ + const client = await OpenID4VCIClient.fromURI({ + uri: HTTPS_OFFER_QR_PRE_AUTHORIZED, + kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', + alg: Alg.ES256, + clientId: 'test-clientId', + }); + await assertionOfsucceedWithAFullFlowWithClient(client); + }); + + it('should succeed with a full flow with the client using OpenID4VCI draft >= 13, https and preauthorized_code flow without did', async () => { + nock(IDENTIPROOF_ISSUER_URL) + .get(WellKnownEndpoints.OPENID_CONFIGURATION) + .reply(200, { + token_endpoint: `${IDENTIPROOF_ISSUER_URL}/token`, + authorization_endpoint: `${IDENTIPROOF_ISSUER_URL}/authorize`, + }); + nock(IDENTIPROOF_ISSUER_URL).post('/token').reply(200, { + access_token: 'ey6546.546654.64565', + authorization_pending: false, + c_nonce: 'c_nonce2022101300', + c_nonce_expires_in: 2025101300, + interval: 2025101300, + token_type: 'Bearer', + }); + nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA_v13)); + nock(ISSUER_URL) + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); + const client = await OpenID4VCIClientV1_0_13.fromURI({ + uri: HTTPS_OFFER_QR_PRE_AUTHORIZED_v13, + kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', + alg: Alg.ES256, + clientId: 'test-clientId', + }); + expect(client.credentialOffer).toBeDefined(); + expect(client.endpointMetadata).toBeDefined(); + expect(client.getIssuer()).toEqual('https://issuer.research.identiproof.io'); + expect(client.getCredentialEndpoint()).toEqual('https://issuer.research.identiproof.io/credential'); + expect(client.getAccessTokenEndpoint()).toEqual('https://issuer.research.identiproof.io/token'); + + const accessToken = await client.acquireAccessToken({ pin: '1234', code: 'ABCD' }); + expect(accessToken).toEqual(mockedAccessTokenResponse); + + const credentialResponse = await client.acquireCredentials({ + credentialIdentifier: 'OpenBadgeCredential', + format: 'jwt_vc_json-ld', + proofCallbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + }); + expect(credentialResponse.credential).toEqual(mockedVC); + }); + + it('should succeed with a full flow with the client using OpenID4VCI draft > 11, https and preauthorized_code flow', async () => { + succeedWithAFullFlowWithClientSetup(); + const client = await OpenID4VCIClient.fromURI({ uri: HTTPS_OFFER_QR_PRE_AUTHORIZED, kid: 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1', alg: Alg.ES256, @@ -141,7 +217,7 @@ describe('OID4VCI-Client should', () => { await assertionOfsucceedWithAFullFlowWithClient(client); }); - async function assertionOfsucceedWithAFullFlowWithClient(client: OpenID4VCIClientV1_0_11) { + async function assertionOfsucceedWithAFullFlowWithClient(client: OpenID4VCIClient) { expect(client.credentialOffer).toBeDefined(); expect(client.endpointMetadata).toBeDefined(); expect(client.getIssuer()).toEqual('https://issuer.research.identiproof.io'); @@ -165,7 +241,7 @@ describe('OID4VCI-Client should', () => { 'succeed with a full flow without the client v1_0_11', async () => { /* Convert the URI into an object */ - const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClientV1_0_11.fromURI(INITIATE_QR_V1_0_08); + const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_08); expect(credentialOffer.baseUrl).toEqual('openid-initiate-issuance://'); expect(credentialOffer.original_credential_offer).toEqual({ @@ -191,18 +267,14 @@ describe('OID4VCI-Client should', () => { format: 'jwt-vc', credential: mockedVC, }); - const credReqClient = CredentialRequestClientBuilderV1_0_11.fromCredentialOffer({ credentialOffer: credentialOffer }) + const credReqClient = CredentialRequestClientBuilder.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, + jwt: jwtDid, callbacks: { signCallback: proofOfPossessionCallbackFunction, }, @@ -261,12 +333,8 @@ describe('OID4VCI-Client should', () => { .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, + jwt: jwtDid, callbacks: { signCallback: proofOfPossessionCallbackFunction, }, @@ -290,9 +358,6 @@ describe('OID4VCI-Client should', () => { }); describe('OIDVCI-Client for v1_0_13 should', () => { - const INITIATE_QR_V1_0_13_CREDENCO = - 'openid-credential-offer://mijnkvk.acc.credenco.com/?credential_offer_uri=https%3A%2F%2Fmijnkvk.acc.credenco.com%2Fopenid4vc%2FcredentialOffer%3Fid%3D32fc4ebf-9e31-4149-9877-e3c0b602d559'; - const mockedCredentialOffer = { credential_issuer: 'https://mijnkvk.acc.credenco.com', credential_configuration_ids: ['BevoegdheidUittreksel_jwt_vc_json'], @@ -320,21 +385,6 @@ describe('OIDVCI-Client for v1_0_13 should', () => { nock.cleanAll(); }); - /*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)); - nock(IDENTIPROOF_AS_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {}); - nock(IDENTIPROOF_AS_URL) - .post(/oauth2\/token.*!/) - .reply(200, JSON.stringify(mockedAccessTokenResponse)); - nock(ISSUER_URL) - .post(/credential/) - .reply(200, { - format: 'jwt-vc', - credential: mockedVC, - }); - }*/ - it('should successfully resolve the credential offer URI', async () => { const uri = 'https://mijnkvk.acc.credenco.com/openid4vc/credentialOffer?id=32fc4ebf-9e31-4149-9877-e3c0b602d559'; @@ -343,78 +393,65 @@ describe('OIDVCI-Client for v1_0_13 should', () => { expect(credentialOffer).toEqual(mockedCredentialOffer); }); - // TODO: ksadjad remove the skipped test - it.skip( - 'succeed credenco with a full flow without the client v1_0_13', + it( + 'succeed with a full flow without the client and without did', async () => { /* Convert the URI into an object */ - // openid-credential-offer://?credential_offer%3D%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.research.identiproof.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%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%22adhjhdjajkdkhjhdj%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D - const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_13_CREDENCO); - /** - * {"credential_issuer":"https://mijnkvk.acc.credenco.com","credential_configuration_ids":["BevoegdheidUittreksel_jwt_vc_json"],"grants":{"authorization_code":{"issuer_state":"32fc4ebf-9e31-4149-9877-e3c0b602d559"},"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA"}}} - */ - const preAuthorizedCode = - 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiIzMmZjNGViZi05ZTMxLTQxNDktOTg3Ny1lM2MwYjYwMmQ1NTkiLCJpc3MiOiJodHRwczovL21pam5rdmsuYWNjLmNyZWRlbmNvLmNvbSIsImF1ZCI6IlRPS0VOIn0.754aiQ87O0vHYSpRvPqAS9cLOgf-pewdeXbpLziRwsxEp9mENfaXpY62muYpzOaWcYmTOydkzhFul-NDYXJZCA'; - expect(credentialOffer.baseUrl).toEqual('openid-credential-offer://mijnkvk.acc.credenco.com/'); - expect((credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13).credential_configuration_ids).toEqual([ - 'BevoegdheidUittreksel_jwt_vc_json', - ]); - expect(credentialOffer.original_credential_offer.grants).toEqual({ - authorization_code: { - issuer_state: '32fc4ebf-9e31-4149-9877-e3c0b602d559', - }, - 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { - 'pre-authorized_code': preAuthorizedCode, + const credentialOffer: CredentialOfferRequestWithBaseUrl = await CredentialOfferClient.fromURI(INITIATE_QR_V1_0_13); + + expect(credentialOffer.baseUrl).toEqual('openid-credential-offer://'); + expect(credentialOffer.original_credential_offer).toEqual({ + credential_configuration_ids: ['OpenBadgeCredentialUrl'], + credential_issuer: ISSUER_URL, + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': 'oaKazRN8I0IbtZ0C7JuMn5', + tx_code: { + description: 'Please enter the serial number of your physical drivers license', + input_mode: 'text', + length: 22, + }, + }, }, }); - /*nock(ISSUER_URL) - .post(/token.*!/) - .reply(200, JSON.stringify(mockedAccessTokenResponse));*/ + nock(ISSUER_URL) + .post(/token.*/) + .reply(200, JSON.stringify(mockedAccessTokenResponse)); /* The actual access token calls */ const accessTokenClient: AccessTokenClient = new AccessTokenClient(); - const accessTokenResponse = await accessTokenClient.acquireAccessToken({ - credentialOffer: credentialOffer, - pin: preAuthorizedCode /*, metadata: {}*/, - }); - expect(accessTokenResponse.successBody).toEqual({}); - /*// Get the credential + 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, - }); + .post(/credential/) + .reply(200, { + format: 'jwt-vc', + credential: mockedVC, + }); const credReqClient = CredentialRequestClientBuilder.fromCredentialOffer({ credentialOffer: credentialOffer }) - .withFormat('jwt_vc') + .withFormat('jwt_vc') - .withTokenFromResponse(accessTokenResponse.successBody!) - .build(); + .withTokenFromResponse(accessTokenResponse.successBody!) + .build(); - //TS2322: Type '(args: ProofOfPossessionCallbackArgs) => Promise' - // is not assignable to type 'ProofOfPossessionCallback'. - // 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, + jwt: jwtWithoutDid, 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', + version: OpenId4VCIVersion.VER_1_0_13, }) - .withKid('did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1') - .build(); - const credResponse = await credReqClient.acquireCredentialsUsingProof({ - proofInput: proof, - credentialTypes: credentialOffer.original_credential_offer.credential_configuration_ids, - }); - expect(credResponse.successBody?.credential).toEqual(mockedVC);*/ + .withEndpointMetadata({ + issuer: 'https://issuer.research.identiproof.io', + credential_endpoint: 'https://issuer.research.identiproof.io/credential', + token_endpoint: 'https://issuer.research.identiproof.io/token', + }) + .withKid('ebfeb1f712ebc6f1c276e12ec21/keys/1') + .build(); + const credResponse = await credReqClient.acquireCredentialsUsingProof({ proofInput: proof, credentialIdentifier: 'OpenBadgeCredentialUrl' }); + expect(credResponse.successBody?.credential).toEqual(mockedVC); }, UNIT_TEST_TIMEOUT, ); diff --git a/packages/client/lib/__tests__/MetadataMocks.ts b/packages/client/lib/__tests__/MetadataMocks.ts index a5d06eeb..3f03a782 100644 --- a/packages/client/lib/__tests__/MetadataMocks.ts +++ b/packages/client/lib/__tests__/MetadataMocks.ts @@ -127,6 +127,41 @@ export const IDENTIPROOF_OID4VCI_METADATA = { }, }, }; +export const IDENTIPROOF_OID4VCI_METADATA_v13 = { + issuer: 'https://issuer.research.identiproof.io', + authorization_server: 'https://auth.research.identiproof.io', + credential_endpoint: 'https://issuer.research.identiproof.io/credential', + jwks_uri: 'https://issuer.research.identiproof.io/.well-known/did.json', + credential_configurations_supported: { + 'Cyber Security Certificate': { + formats: { + jwt_vc: { + types: ['VerifiableCredential', 'Cyber Security Certificate'], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256'], + }, + }, + }, + OpenBadgeCredential: { + formats: { + jwt_vc: { + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256'], + }, + }, + }, + OpenBadgeExtendedCredential: { + formats: { + jwt_vc: { + types: ['VerifiableCredential', 'OpenBadgeExtendedCredential'], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256'], + }, + }, + }, + }, +}; export const SPRUCE_OID4VCI_METADATA = { issuer: 'https://ngi-oidc4vci-test.spruceid.xyz', diff --git a/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts b/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts index fcb112ef..54693daf 100644 --- a/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts +++ b/packages/client/lib/__tests__/ProofOfPossessionBuilder.spec.ts @@ -12,8 +12,15 @@ const jwt: Jwt = { payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL, iat: Date.now() / 1000 }, }; +const jwt_withoutDid: Jwt = { + header: { alg: Alg.ES256, kid: 'ebfeb1f712ebc6f1c276e12ec21/keys/1', typ: 'jwt' }, + payload: { iss: 'sphereon:wallet', nonce: 'tZignsnFbp', jti: 'tZignsnFbp223', aud: IDENTIPROOF_ISSUER_URL, iat: Date.now() / 1000 }, +}; + const kid = 'did:example:ebfeb1f712ebc6f1c276e12ec21/keys/1'; +const kid_withoutDid = 'ebfeb1f712ebc6f1c276e12ec21/keys/1'; + let keypair: KeyPair; async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise { @@ -52,6 +59,16 @@ describe('ProofOfPossession Builder ', () => { ).rejects.toThrow(Error(PROOF_CANT_BE_CONSTRUCTED)); }); + it('should fail without supplied proof or callbacks and with kid without did', async function () { + await expect( + ProofOfPossessionBuilder.fromProof(undefined as never, OpenId4VCIVersion.VER_1_0_13) + .withIssuer(IDENTIPROOF_ISSUER_URL) + .withClientId('sphereon:wallet') + .withKid(kid_withoutDid) + .build(), + ).rejects.toThrow(Error(PROOF_CANT_BE_CONSTRUCTED)); + }); + it('should fail wit undefined jwt supplied', async function () { await expect(() => ProofOfPossessionBuilder.fromJwt({ jwt, callbacks: { signCallback: proofOfPossessionCallbackFunction }, version: OpenId4VCIVersion.VER_1_0_08 }) @@ -63,6 +80,21 @@ describe('ProofOfPossession Builder ', () => { ).toThrow(Error(NO_JWT_PROVIDED)); }); + it('should fail with undefined jwt supplied and kid without did', async function () { + await expect(() => + ProofOfPossessionBuilder.fromJwt({ + jwt: jwt_withoutDid, + callbacks: { signCallback: proofOfPossessionCallbackFunction }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + .withJwt(undefined as never) + .withIssuer(IDENTIPROOF_ISSUER_URL) + .withClientId('sphereon:wallet') + .withKid(kid_withoutDid) + .build(), + ).toThrow(Error(NO_JWT_PROVIDED)); + }); + it('should build a proof with all required params present', async function () { const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ jwt, @@ -78,6 +110,21 @@ describe('ProofOfPossession Builder ', () => { expect(proof).toBeDefined(); }); + it('should build a proof with all required params present without did', async function () { + const proof: ProofOfPossession = await ProofOfPossessionBuilder.fromJwt({ + jwt: jwt_withoutDid, + callbacks: { + signCallback: proofOfPossessionCallbackFunction, + }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + .withIssuer(IDENTIPROOF_ISSUER_URL) + .withKid(kid_withoutDid) + .withClientId('sphereon:wallet') + .build(); + expect(proof).toBeDefined(); + }); + it('should fail creating a proof of possession with simple verification', async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise { @@ -93,6 +140,25 @@ describe('ProofOfPossession Builder ', () => { ).rejects.toThrow(Error(JWS_NOT_VALID)); }); + it('should fail creating a proof of possession with simple verification and without did', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise { + throw new Error(JWS_NOT_VALID); + } + + await expect( + ProofOfPossessionBuilder.fromJwt({ + jwt: jwt_withoutDid, + callbacks: { signCallback: proofOfPossessionCallbackFunction }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + .withIssuer(IDENTIPROOF_ISSUER_URL) + .withClientId('sphereon:wallet') + .withKid(kid_withoutDid) + .build(), + ).rejects.toThrow(Error(JWS_NOT_VALID)); + }); + it('should fail creating a proof of possession without verify callback', async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise { @@ -107,4 +173,23 @@ describe('ProofOfPossession Builder ', () => { .build(), ).rejects.toThrow(Error(JWS_NOT_VALID)); }); + + it('should fail creating a proof of possession without verify callback and without did', async () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async function proofOfPossessionCallbackFunction(_args: Jwt, _kid?: string): Promise { + throw new Error(JWS_NOT_VALID); + } + + await expect( + ProofOfPossessionBuilder.fromJwt({ + jwt: jwt_withoutDid, + callbacks: { signCallback: proofOfPossessionCallbackFunction }, + version: OpenId4VCIVersion.VER_1_0_08, + }) + .withIssuer(IDENTIPROOF_ISSUER_URL) + .withClientId('sphereon:wallet') + .withKid(kid_withoutDid) + .build(), + ).rejects.toThrow(Error(JWS_NOT_VALID)); + }); }); diff --git a/packages/client/lib/__tests__/SdJwt.spec.ts b/packages/client/lib/__tests__/SdJwt.spec.ts index c3abc296..aa137210 100644 --- a/packages/client/lib/__tests__/SdJwt.spec.ts +++ b/packages/client/lib/__tests__/SdJwt.spec.ts @@ -166,4 +166,107 @@ describe('sd-jwt vc', () => { }, UNIT_TEST_TIMEOUT, ); + + it( + 'succeed with a full flow without did', + async () => { + const offerUri = await vcIssuer.createCredentialOfferURI({ + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + tx_code: { + input_mode: 'text', + length: 3, + }, + 'pre-authorized_code': '123', + }, + }, + credential_configuration_ids: ['SdJwtCredential'], + }); + + nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(issuerMetadata)); + nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/openid-configuration').reply(404); + nock(vcIssuer.issuerMetadata.credential_issuer).get('/.well-known/oauth-authorization-server').reply(404); + + expect(offerUri.uri).toEqual( + 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22123%22%2C%22tx_code%22%3A%7B%22input_mode%22%3A%22text%22%2C%22length%22%3A3%7D%7D%7D%2C%22credential_configuration_ids%22%3A%5B%22SdJwtCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fexample.com%22%7D', + ); + + const client = await OpenID4VCIClientV1_0_13.fromURI({ + uri: offerUri.uri, + }); + + expect(client.credentialOffer?.credential_offer).toEqual({ + credential_issuer: 'https://example.com', + credential_configuration_ids: ['SdJwtCredential'], + grants: { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': '123', + tx_code: { + input_mode: 'text', + length: 3, + }, + }, + }, + }); + + const supported = client.getCredentialsSupported('vc+sd-jwt'); + expect(supported).toEqual({ SdJwtCredentialId: { format: 'vc+sd-jwt', id: 'SdJwtCredentialId', vct: 'SdJwtCredentialId' } }); + + const offered = supported['SdJwtCredentialId'] as CredentialSupportedSdJwtVc; + + nock(issuerMetadata.token_endpoint as string) + .post('/') + .reply(200, async (_, body: string) => { + const parsedBody = Object.fromEntries(body.split('&').map((x) => x.split('='))); + return createAccessTokenResponse(parsedBody as AccessTokenRequest, { + credentialOfferSessions: vcIssuer.credentialOfferSessions, + accessTokenIssuer: 'https://issuer.example.com', + cNonces: vcIssuer.cNonces, + cNonce: 'a-c-nonce', + accessTokenSignerCallback: async () => 'ey.val.ue', + tokenExpiresIn: 500, + }); + }); + + await client.acquireAccessToken({ pin: '123' }); + nock(issuerMetadata.credential_endpoint as string) + .post('/') + .reply(200, async (_, body) => + vcIssuer.issueCredential({ + credentialRequest: { ...(body as CredentialRequestV1_0_13), credential_identifier: offered.vct }, + credential: { + vct: 'Hello', + iss: 'example.com', + iat: 123, + // Defines what can be disclosed (optional) + __disclosureFrame: { + name: true, + }, + }, + newCNonce: 'new-c-nonce', + }), + ); + + const credentials = await client.acquireCredentials({ + credentialIdentifier: offered.vct, + // format: 'vc+sd-jwt', + alg, + jwk, + proofCallbacks: { + // When using sd-jwt for real, this jwt should include a jwk + signCallback: async () => 'ey.ja.ja', + }, + }); + + expect(credentials).toEqual({ + notification_id: expect.any(String), + access_token: 'ey.val.ue', + c_nonce: 'new-c-nonce', + c_nonce_expires_in: 300, + credential: 'sd-jwt', + // format: 'vc+sd-jwt', + }); + }, + UNIT_TEST_TIMEOUT, + ); }); diff --git a/packages/client/lib/index.ts b/packages/client/lib/index.ts index 4b422e25..8b959462 100644 --- a/packages/client/lib/index.ts +++ b/packages/client/lib/index.ts @@ -13,6 +13,7 @@ export * from './CredentialOfferClientV1_0_11'; export * from './CredentialOfferClientV1_0_13'; export * from './CredentialRequestClientV1_0_11'; export * from './CredentialRequestClientBuilder'; +export * from './CredentialRequestClientBuilderV1_0_13'; export * from './CredentialRequestClientBuilderV1_0_11'; export * from './functions'; export * from './MetadataClient'; diff --git a/packages/common/lib/functions/ProofUtil.ts b/packages/common/lib/functions/ProofUtil.ts index cf1cebd3..0a82a56a 100644 --- a/packages/common/lib/functions/ProofUtil.ts +++ b/packages/common/lib/functions/ProofUtil.ts @@ -115,6 +115,7 @@ export interface JwtProps { typ?: Typ; kid?: string; jwk?: JWK; + x5c?: string[]; aud?: string | string[]; issuer?: string; clientId?: string; @@ -134,12 +135,13 @@ const createJWT = (mode: PoPMode, jwtProps?: JwtProps, existingJwt?: Jwt): Jwt = // : getJwtProperty('iss', false, jwtProps?.issuer, existingJwt?.payload?.iss); const client_id = mode === 'jwt' ? getJwtProperty('client_id', false, jwtProps?.clientId, existingJwt?.payload?.client_id) : undefined; const jti = getJwtProperty('jti', false, jwtProps?.jti, existingJwt?.payload?.jti); - const typ = getJwtProperty('typ', true, jwtProps?.typ, existingJwt?.header?.typ, 'jwt'); + const typ = getJwtProperty('typ', true, jwtProps?.typ, existingJwt?.header?.typ, 'openid4vci-proof+jwt'); const nonce = getJwtProperty('nonce', false, jwtProps?.nonce, existingJwt?.payload?.nonce); // Officially this is required, but some implementations don't have it // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const alg = getJwtProperty('alg', false, jwtProps?.alg, existingJwt?.header?.alg, 'ES256')!; const kid = getJwtProperty('kid', false, jwtProps?.kid, existingJwt?.header?.kid); const jwk = getJwtProperty('jwk', false, jwtProps?.jwk, existingJwt?.header?.jwk); + const x5c = getJwtProperty('x5c', false, jwtProps?.x5c, existingJwt?.header.x5c); const jwt: Partial = existingJwt ? existingJwt : {}; const now = +new Date(); const jwtPayload: Partial = { @@ -157,6 +159,7 @@ const createJWT = (mode: PoPMode, jwtProps?: JwtProps, existingJwt?: Jwt): Jwt = alg, ...(kid && { kid }), ...(jwk && { jwk }), + ...(x5c && { x5c }), }; return { payload: { ...jwt.payload, ...jwtPayload }, @@ -171,7 +174,7 @@ const getJwtProperty = ( jwtProperty?: T, defaultValue?: T, ): T | undefined => { - if (typeof option === 'string' && option && jwtProperty && option !== jwtProperty) { + if ((typeof option === 'string' || Array.isArray(option)) && option && jwtProperty && option !== jwtProperty) { throw Error(`Cannot have a property '${propertyName}' with value '${option}' and different JWT value '${jwtProperty}' at the same time`); } let result = (jwtProperty ? jwtProperty : option) as T | undefined; diff --git a/packages/common/lib/types/Generic.types.ts b/packages/common/lib/types/Generic.types.ts index daad64b6..a1912208 100644 --- a/packages/common/lib/types/Generic.types.ts +++ b/packages/common/lib/types/Generic.types.ts @@ -62,7 +62,7 @@ export interface CredentialSupplierConfig { export interface CredentialIssuerMetadataOpts { credential_endpoint?: string; // REQUIRED. URL of the Credential Issuer's Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. batch_credential_endpoint?: string; // OPTIONAL. URL of the Credential Issuer's Batch Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path and query parameter components. If omitted, the Credential Issuer does not support the Batch Credential Endpoint. - credentials_supported: CredentialConfigurationSupported[]; // REQUIRED. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. + credentials_supported?: CredentialConfigurationSupported[]; // REQUIRED in versions below 13. A JSON array containing a list of JSON objects, each of them representing metadata about a separate credential type that the Credential Issuer can issue. The JSON objects in the array MUST conform to the structure of the Section 10.2.3.1. credential_issuer: string; // REQUIRED. The Credential Issuer's identifier. authorization_server?: string; // OPTIONAL. Identifier of the OAuth 2.0 Authorization Server (as defined in [RFC8414]) the Credential Issuer relies on for authorization. If this element is omitted, the entity providing the Credential Issuer is also acting as the AS, i.e. the Credential Issuer's identifier is used as the OAuth 2.0 Issuer value to obtain the Authorization Server metadata as per [RFC8414]. token_endpoint?: string; diff --git a/packages/issuer/lib/VcIssuer.ts b/packages/issuer/lib/VcIssuer.ts index e56527ca..2dc7e9dd 100644 --- a/packages/issuer/lib/VcIssuer.ts +++ b/packages/issuer/lib/VcIssuer.ts @@ -498,8 +498,13 @@ export class VcIssuer { // only 1 is allowed, but need to look into whether jwk and x5c are allowed together throw Error(KID_JWK_X5C_ERROR) } else if (kid && !did) { - // Make sure the callback function extracts the DID from the kid - throw Error(KID_DID_NO_DID_ERROR) + if (!jwk && !x5c) { + // Make sure the callback function extracts the DID from the kid + throw Error(KID_DID_NO_DID_ERROR) + } else { + // If JWK or x5c is present, log the information and proceed + console.log(`KID present but no DID, using JWK or x5c`) + } } else if (did && !didDocument) { // Make sure the callback function does DID resolution when a did is present throw Error(DID_NO_DIDDOC_ERROR) diff --git a/packages/issuer/lib/__tests__/VcIssuer.spec.ts b/packages/issuer/lib/__tests__/VcIssuer.spec.ts index aa037c77..f30c58a7 100644 --- a/packages/issuer/lib/__tests__/VcIssuer.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuer.spec.ts @@ -33,6 +33,21 @@ const verifiableCredential = { }, } +const verifiableCredential_withoutDid = { + '@context': ['https://www.w3.org/2018/credentials/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + id: 'http://university.example/credentials/1872', + type: ['VerifiableCredential', 'ExampleAlumniCredential'], + issuer: 'https://university.example/issuers/565049', + issuanceDate: new Date().toISOString(), + credentialSubject: { + id: 'ebfeb1f712ebc6f1c276e12ec21', + alumniOf: { + id: 'c276e12ec21ebfeb1f712ebc6f1', + name: 'Example University', + }, + }, +} + describe('VcIssuer', () => { let vcIssuer: VcIssuer const issuerState = 'previously-created-state' @@ -434,3 +449,253 @@ describe('VcIssuer', () => { ).rejects.toThrow(Error(ALG_ERROR)) }) }) + +describe('VcIssuer without did', () => { + let vcIssuer: VcIssuer + const issuerState = 'previously-created-state' + const clientId = 'sphereon:wallet' + const preAuthorizedCode = 'test_code' + + const jwtVerifyCallback: jest.Mock = jest.fn() + + beforeEach(async () => { + jest.clearAllMocks() + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') + .withCryptographicBindingMethod('jwk') + .withFormat('jwt_vc_json') + .withCredentialName('UniversityDegree_JWT') + .withCredentialDefinition({ + type: ['VerifiableCredential', '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('previously-created-state', { + issuerState, + clientId, + preAuthorizedCode, + createdAt: +new Date(), + lastUpdatedAt: +new Date(), + status: IssueStatus.OFFER_CREATED, + notification_id: v4(), + userPin: '123456', + credentialOffer: { + credential_offer: { + credential_issuer: 'test.com', + credentials: [ + { + format: 'ldp_vc', + credential_definition: { + types: ['VerifiableCredential'], + '@context': ['https://www.w3.org/2018/credentials/v1'], + credentialSubject: {}, + }, + }, + ], + grants: { + authorization_code: { issuer_state: issuerState }, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': preAuthorizedCode, + tx_code: { + input_mode: 'text', + length: 4, + }, + }, + }, + }, + }, + }) + vcIssuer = new VcIssuerBuilder() + .withAuthorizationServers('https://authorization-server') + .withCredentialEndpoint('https://credential-endpoint') + .withCredentialIssuer(IDENTIPROOF_ISSUER_URL) + .withIssuerDisplay({ + name: 'example issuer', + locale: 'en-US', + }) + .withCredentialConfigurationsSupported(credentialsSupported) + .withCredentialOfferStateManager(stateManager) + .withInMemoryCNonceState() + .withInMemoryCredentialOfferURIState() + .withCredentialSignerCallback(() => + Promise.resolve({ + '@context': ['https://www.w3.org/2018/credentials/v1'], + type: ['VerifiableCredential'], + issuer: 'test.com', + issuanceDate: new Date().toISOString(), + credentialSubject: {}, + proof: { + type: IProofType.JwtProof2020, + jwt: 'ye.ye.ye', + created: new Date().toISOString(), + proofPurpose: IProofPurpose.assertionMethod, + verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf', + }, + }), + ) + .withJWTVerifyCallback(jwtVerifyCallback) + .build() + }) + + afterAll(async () => { + jest.clearAllMocks() + // await new Promise((resolve) => setTimeout((v: void) => resolve(v), 500)) + }) + + // Of course this doesn't work. The state is part of the proof to begin with + it('should fail issuing credential if an invalid state is used', async () => { + jwtVerifyCallback.mockResolvedValue({ + alg: Alg.ES256K, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: Alg.ES256K, + x5c: ['12', '34', '56'], + }, + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date() / 1000, + nonce: 'test-nonce', + }, + }, + }) + + await expect( + vcIssuer.issueCredential({ + credentialRequest: { + credential_identifier: 'VerifiableCredential', + format: 'jwt_vc_json', + proof: { + proof_type: 'jwt', + jwt: 'ye.ye.ye', + }, + }, + // issuerState: 'invalid state', + }), + ).rejects.toThrow(Error(STATE_MISSING_ERROR + ' (test-nonce)')) + }) + + it.each([...Object.values(Alg), 'CUSTOM'])('should issue %s signed credential if a valid state is passed in', async (alg: string) => { + jwtVerifyCallback.mockResolvedValue({ + alg: alg, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: alg, + x5c: ['12', '34', '56'], + }, + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date() / 1000, + nonce: 'test-nonce', + }, + }, + }) + + const createdAt = +new Date() + await vcIssuer.cNonces.set('test-nonce', { + cNonce: 'test-nonce', + preAuthorizedCode: 'test-pre-authorized-code', + createdAt: createdAt, + }) + await vcIssuer.credentialOfferSessions.set('test-pre-authorized-code', { + createdAt: createdAt, + notification_id: '43243', + preAuthorizedCode: 'test-pre-authorized-code', + credentialOffer: { + credential_offer: { + credential_issuer: 'test.com', + credentials: [], + }, + }, + lastUpdatedAt: createdAt, + status: IssueStatus.ACCESS_TOKEN_CREATED, + }) + + expect( + vcIssuer.issueCredential({ + credential: verifiableCredential_withoutDid, + credentialRequest: { + credential_identifier: 'VerifiableCredential', + format: 'jwt_vc_json', + proof: { + proof_type: 'jwt', + jwt: 'ye.ye.ye', + }, + }, + newCNonce: 'new-test-nonce', + }), + ).resolves.toEqual({ + c_nonce: 'new-test-nonce', + c_nonce_expires_in: 300, + notification_id: '43243', + credential: { + '@context': ['https://www.w3.org/2018/credentials/v1'], + credentialSubject: {}, + issuanceDate: expect.any(String), + issuer: 'test.com', + proof: { + created: expect.any(String), + jwt: 'ye.ye.ye', + proofPurpose: 'assertionMethod', + type: 'JwtProof2020', + verificationMethod: 'sdfsdfasdfasdfasdfasdfassdfasdf', + }, + type: ['VerifiableCredential'], + }, + // format: 'jwt_vc_json', + }) + }) + + it('should fail issuing credential if the signing algorithm is missing', async () => { + const createdAt = +new Date() + await vcIssuer.cNonces.set('test-nonce', { + cNonce: 'test-nonce', + preAuthorizedCode: 'test-pre-authorized-code', + createdAt: createdAt, + }) + + jwtVerifyCallback.mockResolvedValue({ + alg: undefined, + jwt: { + header: { + typ: 'openid4vci-proof+jwt', + alg: undefined, + x5c: ['12', '34', '56'], + }, + payload: { + aud: IDENTIPROOF_ISSUER_URL, + iat: +new Date() / 1000, + nonce: 'test-nonce', + }, + }, + }) + + expect( + vcIssuer.issueCredential({ + credentialRequest: { + credential_identifier: 'VerifiableCredential', + format: 'jwt_vc_json', + proof: { + proof_type: 'jwt', + jwt: 'ye.ye.ye', + }, + }, + }), + ).rejects.toThrow(Error(ALG_ERROR)) + }) +}) diff --git a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts index fbf67db7..34b4c298 100644 --- a/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts +++ b/packages/issuer/lib/__tests__/VcIssuerBuilder.spec.ts @@ -150,4 +150,63 @@ describe('VcIssuer builder should', () => { credentialOffer: { credential_offer: { credentials: ['test_credential'], credential_issuer: 'test_issuer' } }, }) }) + + it('should successfully attach an instance of the ICredentialOfferStateManager to the VcIssuer instance without did', async () => { + const credentialsSupported: Record = new CredentialSupportedBuilderV1_13() + .withCredentialSigningAlgValuesSupported('ES256K') + .withCryptographicBindingMethod('jwk') + .withFormat('jwt_vc_json') + .withCredentialName('UniversityDegree_JWT') + .withCredentialDefinition({ + type: ['VerifiableCredential', '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 vcIssuer = new VcIssuerBuilder() + .withAuthorizationServers('https://authorization-server') + .withCredentialEndpoint('https://credential-endpoint') + .withCredentialIssuer('https://credential-issuer') + .withIssuerDisplay({ + name: 'example issuer', + locale: 'en-US', + }) + .withCredentialConfigurationsSupported(credentialsSupported) + .withInMemoryCredentialOfferState() + .withInMemoryCNonceState() + .build() + expect(vcIssuer).toBeDefined() + const preAuthorizedCodecreatedAt = +new Date() + await vcIssuer.credentialOfferSessions?.set('test', { + notification_id: v4(), + issuerState: v4(), + lastUpdatedAt: preAuthorizedCodecreatedAt, + status: IssueStatus.OFFER_CREATED, + clientId: 'test_client', + createdAt: preAuthorizedCodecreatedAt, + userPin: '123456', + credentialOffer: { credential_offer: { credentials: ['test_credential'], credential_issuer: 'test_issuer' } }, + }) + await expect(vcIssuer.credentialOfferSessions?.get('test')).resolves.toMatchObject({ + clientId: 'test_client', + userPin: '123456', + status: IssueStatus.OFFER_CREATED, + lastUpdatedAt: preAuthorizedCodecreatedAt, + createdAt: preAuthorizedCodecreatedAt, + credentialOffer: { credential_offer: { credentials: ['test_credential'], credential_issuer: 'test_issuer' } }, + }) + }) + })