UNPKG

@sphereon/oid4vci-client

Version:

OpenID for Verifiable Credential Issuance (OpenID4VCI) client

241 lines (209 loc) • 9.15 kB
import { createDPoP, CreateDPoPClientOpts, getCreateDPoPOptions } from '@sphereon/oid4vc-common'; import { acquireDeferredCredential, CredentialResponse, DPoPResponseParams, getCredentialRequestForVersion, getUniformFormat, isDeferredCredentialResponse, isValidURL, JsonLdIssuerCredentialDefinition, OID4VCICredentialFormat, OpenId4VCIVersion, OpenIDResponse, post, 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 { shouldRetryResourceRequestWithDPoPNonce } from './functions/dpopUtil'; 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<CredentialRequestOptsV1_0_11>; 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; createDPoPOpts?: CreateDPoPClientOpts; }): Promise<OpenIDResponse<CredentialResponse, DPoPResponseParams> & { access_token: string }> { const { credentialTypes, proofInput, format, context } = opts; const request = await this.createCredentialRequest({ proofInput, credentialTypes, context, format, version: this.version() }); return await this.acquireCredentialsUsingRequest(request, opts.createDPoPOpts); } public async acquireCredentialsUsingRequest( uniformRequest: UniformCredentialRequest, createDPoPOpts?: CreateDPoPClientOpts, ): Promise<OpenIDResponse<CredentialResponse, DPoPResponseParams> & { access_token: string }> { 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 dPoP = createDPoPOpts ? await createDPoP(getCreateDPoPOptions(createDPoPOpts, credentialEndpoint, { accessToken: requestToken })) : undefined; let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken, customHeaders: { ...(createDPoPOpts && { dpop: dPoP }) }, })) as OpenIDResponse<CredentialResponse> & { access_token: string; }; let nextDPoPNonce = createDPoPOpts?.jwtPayloadProps.nonce; const retryWithNonce = shouldRetryResourceRequestWithDPoPNonce(response); if (retryWithNonce.ok && createDPoPOpts) { createDPoPOpts.jwtPayloadProps.nonce = retryWithNonce.dpopNonce; dPoP = await createDPoP(getCreateDPoPOptions(createDPoPOpts, credentialEndpoint, { accessToken: requestToken })); response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken, customHeaders: { ...(createDPoPOpts && { dpop: dPoP }) }, })) as OpenIDResponse<CredentialResponse> & { access_token: string; }; const successDPoPNonce = response.origResponse.headers.get('DPoP-Nonce'); nextDPoPNonce = successDPoPNonce ?? retryWithNonce.dpopNonce; } this._isDeferred = isDeferredCredentialResponse(response); if (this.isDeferred() && this.credentialRequestOpts.deferredCredentialAwait && response.successBody) { response = await this.acquireDeferredCredential(response.successBody, { bearerToken: this.credentialRequestOpts.token }); } response.access_token = requestToken; debug(`Credential endpoint ${credentialEndpoint} response:\r\n${JSON.stringify(response, null, 2)}`); return { ...response, ...(nextDPoPNonce && { params: { dpop: { dpopNonce: nextDPoPNonce } } }), }; } public async acquireDeferredCredential( response: Pick<CredentialResponse, 'transaction_id' | 'acceptance_token' | 'c_nonce'>, opts?: { bearerToken?: string; }, ): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> { 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<UniformCredentialRequest> { 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 }), } as JsonLdIssuerCredentialDefinition, }; } 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], }; } else if (format === 'mso_mdoc') { if (types.length > 1) { throw Error(`Only a single credential type is supported for ${format}`); } return { format, proof, doctype: 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; } }