Skip to content

Commit

Permalink
feat: expose functions for experimental subject issuer support
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Jun 10, 2024
1 parent 3a411a5 commit c4adecc
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 37 deletions.
41 changes: 10 additions & 31 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import {
getUniformFormat,
isDeferredCredentialResponse,
isValidURL,
NotificationErrorResponse,
NotificationRequest,
NotificationResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
OpenIDResponse,
Expand All @@ -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');

Expand All @@ -37,6 +33,7 @@ export interface CredentialRequestOpts {
proof: ProofOfPossession;
token: string;
version: OpenId4VCIVersion;
subjectIssuance?: ExperimentalSubjectIssuance;
}

export async function buildProof<DIDDoc>(
Expand Down Expand Up @@ -88,14 +85,16 @@ export class CredentialRequestClient {
context?: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
subjectIssuance?: ExperimentalSubjectIssuance;
}): Promise<OpenIDResponse<CredentialResponse>> {
}): Promise<OpenIDResponse<CredentialResponse> & { 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<OpenIDResponse<CredentialResponse>> {
public async acquireCredentialsUsingRequest(
uniformRequest: UniformCredentialRequest,
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const request = getCredentialRequestForVersion(uniformRequest, this.version());
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
if (!isValidURL(credentialEndpoint)) {
Expand All @@ -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<CredentialResponse> = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken });
let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken })) as OpenIDResponse<CredentialResponse> & {
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) {
Expand All @@ -125,7 +127,7 @@ export class CredentialRequestClient {
opts?: {
bearerToken?: string;
},
): Promise<OpenIDResponse<CredentialResponse>> {
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const transactionId = response.transaction_id;
const bearerToken = response.acceptance_token ?? opts?.bearerToken;
const deferredCredentialEndpoint = this.getDeferredCredentialEndpoint();
Expand Down Expand Up @@ -215,29 +217,6 @@ export class CredentialRequestClient {
throw new Error(`Unsupported format: ${format}`);
}

public async sendNotification(request: NotificationRequest, accessToken: string): Promise<NotificationResult> {
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<NotificationErrorResponse>(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;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/client/lib/CredentialRequestClientBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CredentialOfferRequestWithBaseUrl,
determineSpecVersionFromOffer,
EndpointMetadata,
ExperimentalSubjectIssuance,
getIssuerFromCredentialOfferPayload,
getTypesFromOffer,
OID4VCICredentialFormat,
Expand All @@ -25,6 +26,7 @@ export class CredentialRequestClientBuilder {
format?: CredentialFormat | OID4VCICredentialFormat;
token?: string;
version?: OpenId4VCIVersion;
subjectIssuance?: ExperimentalSubjectIssuance;

public static fromCredentialIssuer({
credentialIssuer,
Expand Down Expand Up @@ -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;
Expand Down
27 changes: 25 additions & 2 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import {
CredentialSupported,
DefaultURISchemes,
EndpointMetadataResult,
ExperimentalSubjectIssuance,
getClientIdFromCredentialOfferPayload,
getIssuerFromCredentialOfferPayload,
getSupportedCredentials,
getTypesFromCredentialSupported,
JWK,
KID_JWK_X5C_ERROR,
NotificationRequest,
NotificationResult,
OID4VCICredentialFormat,
OpenId4VCIVersion,
PKCEOpts,
Expand All @@ -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');

Expand Down Expand Up @@ -339,7 +344,8 @@ export class OpenID4VCIClient {
jti?: string;
deferredCredentialAwait?: boolean;
deferredCredentialIntervalInMS?: number;
}): Promise<CredentialResponse> {
experimentalHolderIssuanceSupported?: boolean;
}): Promise<CredentialResponse & { access_token: string }> {
if ([jwk, kid].filter((v) => v !== undefined).length > 1) {
throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
}
Expand All @@ -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];
Expand All @@ -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 };
}
}
});

Expand All @@ -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,
Expand Down Expand Up @@ -418,6 +432,7 @@ export class OpenID4VCIClient {
credentialTypes,
context,
format,
subjectIssuance,
});
if (response.errorBody) {
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
Expand All @@ -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<string> {
Expand All @@ -457,6 +472,14 @@ export class OpenID4VCIClient {
});
}

public async sendNotification(
credentialRequestOpts: CredentialRequestOpts,
request: NotificationRequest,
accessToken?: string,
): Promise<NotificationResult> {
return sendNotification(credentialRequestOpts, request, accessToken ?? this.accessTokenResponse.access_token);
}

getCredentialOfferTypes(): string[][] {
if (!this.credentialOffer) {
return [];
Expand Down
32 changes: 32 additions & 0 deletions packages/client/lib/functions/notifications.ts
Original file line number Diff line number Diff line change
@@ -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<NotificationResult> {
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<NotificationErrorResponse>(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;
}
8 changes: 4 additions & 4 deletions packages/common/lib/functions/CredentialResponseUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export async function acquireDeferredCredential({
deferredCredentialIntervalInMS?: number;
deferredCredentialAwait?: boolean;
deferredCredentialEndpoint: string;
}): Promise<OpenIDResponse<CredentialResponse>> {
let credentialResponse: OpenIDResponse<CredentialResponse> = await acquireDeferredCredentialImpl({
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
let credentialResponse: OpenIDResponse<CredentialResponse> & { access_token: string } = await acquireDeferredCredentialImpl({
bearerToken,
transactionId,
deferredCredentialEndpoint,
Expand Down Expand Up @@ -77,7 +77,7 @@ async function acquireDeferredCredentialImpl({
bearerToken: string;
transactionId?: string;
deferredCredentialEndpoint: string;
}): Promise<OpenIDResponse<CredentialResponse>> {
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
const response: OpenIDResponse<CredentialResponse> = await post(
deferredCredentialEndpoint,
JSON.stringify(transactionId ? { transaction_id: transactionId } : ''),
Expand All @@ -86,5 +86,5 @@ async function acquireDeferredCredentialImpl({
console.log(JSON.stringify(response, null, 2));
assertNonFatalError(response);

return response;
return { ...response, access_token: bearerToken };
}

0 comments on commit c4adecc

Please sign in to comment.