From c4adeccdbde6b42a7df85dfbdcb821f2fab8b819 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Mon, 10 Jun 2024 19:48:08 +0200 Subject: [PATCH] feat: expose functions for experimental subject issuer support --- .../client/lib/CredentialRequestClient.ts | 41 +++++-------------- .../lib/CredentialRequestClientBuilder.ts | 7 ++++ packages/client/lib/OpenID4VCIClient.ts | 27 +++++++++++- .../client/lib/functions/notifications.ts | 32 +++++++++++++++ .../lib/functions/CredentialResponseUtil.ts | 8 ++-- 5 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 packages/client/lib/functions/notifications.ts diff --git a/packages/client/lib/CredentialRequestClient.ts b/packages/client/lib/CredentialRequestClient.ts index 56cbe394..08309df7 100644 --- a/packages/client/lib/CredentialRequestClient.ts +++ b/packages/client/lib/CredentialRequestClient.ts @@ -5,9 +5,6 @@ import { getUniformFormat, isDeferredCredentialResponse, isValidURL, - NotificationErrorResponse, - NotificationRequest, - NotificationResult, OID4VCICredentialFormat, OpenId4VCIVersion, OpenIDResponse, @@ -22,7 +19,6 @@ import Debug from 'debug'; import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; -import { LOG } from './types'; const debug = Debug('sphereon:oid4vci:credential'); @@ -37,6 +33,7 @@ export interface CredentialRequestOpts { proof: ProofOfPossession; token: string; version: OpenId4VCIVersion; + subjectIssuance?: ExperimentalSubjectIssuance; } export async function buildProof( @@ -88,14 +85,16 @@ export class CredentialRequestClient { context?: string[]; format?: CredentialFormat | OID4VCICredentialFormat; subjectIssuance?: ExperimentalSubjectIssuance; - }): Promise> { + }): Promise & { access_token: string }> { const { credentialTypes, proofInput, format, context, subjectIssuance } = opts; const request = await this.createCredentialRequest({ proofInput, credentialTypes, context, format, version: this.version(), subjectIssuance }); return await this.acquireCredentialsUsingRequest(request); } - public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise> { + public async acquireCredentialsUsingRequest( + uniformRequest: UniformCredentialRequest, + ): Promise & { access_token: string }> { const request = getCredentialRequestForVersion(uniformRequest, this.version()); const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint; if (!isValidURL(credentialEndpoint)) { @@ -105,11 +104,14 @@ export class CredentialRequestClient { debug(`Acquiring credential(s) from: ${credentialEndpoint}`); debug(`request\n: ${JSON.stringify(request, null, 2)}`); const requestToken: string = this.credentialRequestOpts.token; - let response: OpenIDResponse = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken }); + let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken })) as OpenIDResponse & { + access_token: string; + }; 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; if ((uniformRequest.credential_subject_issuance && response.successBody) || response.successBody?.credential_subject_issuance) { if (uniformRequest.credential_subject_issuance !== response.successBody?.credential_subject_issuance) { @@ -125,7 +127,7 @@ export class CredentialRequestClient { opts?: { bearerToken?: string; }, - ): Promise> { + ): Promise & { access_token: string }> { const transactionId = response.transaction_id; const bearerToken = response.acceptance_token ?? opts?.bearerToken; const deferredCredentialEndpoint = this.getDeferredCredentialEndpoint(); @@ -215,29 +217,6 @@ export class CredentialRequestClient { throw new Error(`Unsupported format: ${format}`); } - public async sendNotification(request: NotificationRequest, accessToken: string): Promise { - LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`); - if (!this.credentialRequestOpts.notificationEndpoint) { - throw Error(`Cannot send notification when no notification endpoint is provided`); - } - const response = await post(this.credentialRequestOpts.notificationEndpoint, JSON.stringify(request), { - bearerToken: accessToken, - }); - const error = response.errorBody?.error !== undefined; - const result = { - error, - response: error ? await response.errorBody?.json() : undefined, - }; - if (error) { - LOG.warning( - `Notification endpoint returned an error for event '${request.event}' and id ${request.notification_id}: ${await response.errorBody?.json()}`, - ); - } else { - LOG.debug(`Notification endpoint returned success for event '${request.event}' and id ${request.notification_id}`); - } - return result; - } - private version(): OpenId4VCIVersion { return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11; } diff --git a/packages/client/lib/CredentialRequestClientBuilder.ts b/packages/client/lib/CredentialRequestClientBuilder.ts index a73205df..a9c06092 100644 --- a/packages/client/lib/CredentialRequestClientBuilder.ts +++ b/packages/client/lib/CredentialRequestClientBuilder.ts @@ -5,6 +5,7 @@ import { CredentialOfferRequestWithBaseUrl, determineSpecVersionFromOffer, EndpointMetadata, + ExperimentalSubjectIssuance, getIssuerFromCredentialOfferPayload, getTypesFromOffer, OID4VCICredentialFormat, @@ -25,6 +26,7 @@ export class CredentialRequestClientBuilder { format?: CredentialFormat | OID4VCICredentialFormat; token?: string; version?: OpenId4VCIVersion; + subjectIssuance?: ExperimentalSubjectIssuance; public static fromCredentialIssuer({ credentialIssuer, @@ -131,6 +133,11 @@ export class CredentialRequestClientBuilder { return this; } + public withSubjectIssuance(subjectIssuance: ExperimentalSubjectIssuance): this { + this.subjectIssuance = subjectIssuance; + return this; + } + public withToken(accessToken: string): this { this.token = accessToken; return this; diff --git a/packages/client/lib/OpenID4VCIClient.ts b/packages/client/lib/OpenID4VCIClient.ts index 93c97419..9162da0d 100644 --- a/packages/client/lib/OpenID4VCIClient.ts +++ b/packages/client/lib/OpenID4VCIClient.ts @@ -11,12 +11,15 @@ import { CredentialSupported, DefaultURISchemes, EndpointMetadataResult, + ExperimentalSubjectIssuance, getClientIdFromCredentialOfferPayload, getIssuerFromCredentialOfferPayload, getSupportedCredentials, getTypesFromCredentialSupported, JWK, KID_JWK_X5C_ERROR, + NotificationRequest, + NotificationResult, OID4VCICredentialFormat, OpenId4VCIVersion, PKCEOpts, @@ -29,10 +32,12 @@ import Debug from 'debug'; import { AccessTokenClient } from './AccessTokenClient'; import { createAuthorizationRequestUrl } from './AuthorizationCodeClient'; import { CredentialOfferClient } from './CredentialOfferClient'; +import { CredentialRequestOpts } from './CredentialRequestClient'; import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder'; import { MetadataClient } from './MetadataClient'; import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder'; import { generateMissingPKCEOpts } from './functions/AuthorizationUtil'; +import { sendNotification } from './functions/notifications'; const debug = Debug('sphereon:oid4vci'); @@ -339,7 +344,8 @@ export class OpenID4VCIClient { jti?: string; deferredCredentialAwait?: boolean; deferredCredentialIntervalInMS?: number; - }): Promise { + experimentalHolderIssuanceSupported?: boolean; + }): Promise { if ([jwk, kid].filter((v) => v !== undefined).length > 1) { throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`); } @@ -362,6 +368,7 @@ export class OpenID4VCIClient { requestBuilder.withTokenFromResponse(this.accessTokenResponse); requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS); + let subjectIssuance: ExperimentalSubjectIssuance | undefined; if (this.endpointMetadata?.credentialIssuerMetadata) { const metadata = this.endpointMetadata.credentialIssuerMetadata; const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes]; @@ -376,6 +383,9 @@ export class OpenID4VCIClient { (types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0]))) ) { typeSupported = true; + if (supportedCredential.credential_subject_issuance) { + subjectIssuance = { credential_subject_issuance: supportedCredential.credential_subject_issuance }; + } } }); @@ -391,6 +401,10 @@ export class OpenID4VCIClient { } // todo: Format check? We might end up with some disjoint type / format combinations supported by the server } + if (subjectIssuance) { + requestBuilder.withSubjectIssuance(subjectIssuance); + } + const credentialRequestClient = requestBuilder.build(); const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: this.accessTokenResponse, @@ -418,6 +432,7 @@ export class OpenID4VCIClient { credentialTypes, context, format, + subjectIssuance, }); if (response.errorBody) { debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`); @@ -434,7 +449,7 @@ export class OpenID4VCIClient { } for issuer ${this.getIssuer()} failed as there was no success response body`, ); } - return response.successBody; + return { ...response.successBody, access_token: response.access_token }; } public async exportState(): Promise { @@ -457,6 +472,14 @@ export class OpenID4VCIClient { }); } + public async sendNotification( + credentialRequestOpts: CredentialRequestOpts, + request: NotificationRequest, + accessToken?: string, + ): Promise { + return sendNotification(credentialRequestOpts, request, accessToken ?? this.accessTokenResponse.access_token); + } + getCredentialOfferTypes(): string[][] { if (!this.credentialOffer) { return []; diff --git a/packages/client/lib/functions/notifications.ts b/packages/client/lib/functions/notifications.ts new file mode 100644 index 00000000..ce226890 --- /dev/null +++ b/packages/client/lib/functions/notifications.ts @@ -0,0 +1,32 @@ +import { NotificationErrorResponse, NotificationRequest, NotificationResult, post } from '@sphereon/oid4vci-common'; + +import { CredentialRequestOpts } from '../CredentialRequestClient'; +import { LOG } from '../types'; + +export async function sendNotification( + credentialRequestOpts: CredentialRequestOpts, + request: NotificationRequest, + accessToken?: string, +): Promise { + LOG.info(`Sending status notification event '${request.event}' for id ${request.notification_id}`); + if (!credentialRequestOpts.notificationEndpoint) { + throw Error(`Cannot send notification when no notification endpoint is provided`); + } + const token = accessToken ?? credentialRequestOpts.token; + const response = await post(credentialRequestOpts.notificationEndpoint, JSON.stringify(request), { + bearerToken: token, + }); + const error = response.errorBody?.error !== undefined; + const result = { + error, + response: error ? await response.errorBody?.json() : undefined, + }; + if (error) { + LOG.warning( + `Notification endpoint returned an error for event '${request.event}' and id ${request.notification_id}: ${await response.errorBody?.json()}`, + ); + } else { + LOG.debug(`Notification endpoint returned success for event '${request.event}' and id ${request.notification_id}`); + } + return result; +} diff --git a/packages/common/lib/functions/CredentialResponseUtil.ts b/packages/common/lib/functions/CredentialResponseUtil.ts index 8574f2a8..139e0137 100644 --- a/packages/common/lib/functions/CredentialResponseUtil.ts +++ b/packages/common/lib/functions/CredentialResponseUtil.ts @@ -47,8 +47,8 @@ export async function acquireDeferredCredential({ deferredCredentialIntervalInMS?: number; deferredCredentialAwait?: boolean; deferredCredentialEndpoint: string; -}): Promise> { - let credentialResponse: OpenIDResponse = await acquireDeferredCredentialImpl({ +}): Promise & { access_token: string }> { + let credentialResponse: OpenIDResponse & { access_token: string } = await acquireDeferredCredentialImpl({ bearerToken, transactionId, deferredCredentialEndpoint, @@ -77,7 +77,7 @@ async function acquireDeferredCredentialImpl({ bearerToken: string; transactionId?: string; deferredCredentialEndpoint: string; -}): Promise> { +}): Promise & { access_token: string }> { const response: OpenIDResponse = await post( deferredCredentialEndpoint, JSON.stringify(transactionId ? { transaction_id: transactionId } : ''), @@ -86,5 +86,5 @@ async function acquireDeferredCredentialImpl({ console.log(JSON.stringify(response, null, 2)); assertNonFatalError(response); - return response; + return { ...response, access_token: bearerToken }; }