Skip to content

Commit

Permalink
feat: Add deferred support
Browse files Browse the repository at this point in the history
  • Loading branch information
nklomp committed Jan 5, 2024
1 parent 7eb9494 commit 99dc87d
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/client/lib/AccessTokenClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export class AccessTokenClient {
this.assertNonEmptyCode(accessTokenRequest);
this.assertNonEmptyRedirectUri(accessTokenRequest);
} else {
this.throwNotSupportedFlow;
this.throwNotSupportedFlow();
}
}

Expand Down
71 changes: 65 additions & 6 deletions packages/client/lib/CredentialRequestClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
acquireDeferredCredential,
CredentialResponse,
getCredentialRequestForVersion,
getUniformFormat,
isDeferredCredentialResponse,
OID4VCICredentialFormat,
OpenId4VCIVersion,
OpenIDResponse,
Expand All @@ -19,25 +21,57 @@ import { isValidURL, post } from './functions';
const debug = Debug('sphereon:oid4vci:credential');

export interface CredentialRequestOpts {
deferredCredentialAwait?: boolean;
deferredCredentialIntervalInMS?: number;
credentialEndpoint: string;
deferredCredentialEndpoint?: string;
credentialTypes: string[];
format?: CredentialFormat | OID4VCICredentialFormat;
proof: ProofOfPossession;
token: string;
version: OpenId4VCIVersion;
}

export async function buildProof<DIDDoc>(
proofInput: ProofOfPossessionBuilder<DIDDoc> | ProofOfPossession,
opts: {
version: OpenId4VCIVersion;
cNonce?: string;
},
) {
if ('proof_type' in proofInput) {
if (opts.cNonce) {
throw Error(`Cnonce param is only supported when using a Proof of Posession builder`);
//decodeJwt(proofInput.jwt).
}
return await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build();
}
if (opts.cNonce) {
proofInput.withAccessTokenNonce(opts.cNonce);
}
return await proofInput.build();
}

export class CredentialRequestClient {
private readonly _credentialRequestOpts: Partial<CredentialRequestOpts>;
private _isDeferred = false;

get credentialRequestOpts(): CredentialRequestOpts {
return this._credentialRequestOpts as CredentialRequestOpts;
}

public isDeferred(): boolean {
return this._isDeferred;
}

public getCredentialEndpoint(): string {
return this.credentialRequestOpts.credentialEndpoint;
}

public getDeferredCredentialEndpoint(): string | undefined {
return this.credentialRequestOpts.deferredCredentialEndpoint;
}

public constructor(builder: CredentialRequestClientBuilder) {
this._credentialRequestOpts = { ...builder };
}
Expand All @@ -63,11 +97,40 @@ export class CredentialRequestClient {
debug(`Acquiring credential(s) from: ${credentialEndpoint}`);
debug(`request\n: ${JSON.stringify(request, null, 2)}`);
const requestToken: string = this.credentialRequestOpts.token;
const response: OpenIDResponse<CredentialResponse> = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken });
let response: OpenIDResponse<CredentialResponse> = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken });
this._isDeferred = isDeferredCredentialResponse(response);
if (this.isDeferred() && this.credentialRequestOpts.deferredCredentialAwait && response.successBody) {
response = await this.acquireDeferredCredential(response.successBody, { bearerToken: this.credentialRequestOpts.token });
}

debug(`Credential endpoint ${credentialEndpoint} response:\r\n${JSON.stringify(response, null, 2)}`);
return response;
}

public async acquireDeferredCredential(
response: Pick<CredentialResponse, 'transaction_id' | 'acceptance_token' | 'c_nonce'>,
opts?: {
bearerToken?: string;
},
): Promise<OpenIDResponse<CredentialResponse>> {
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<DIDDoc>(opts: {
proofInput: ProofOfPossessionBuilder<DIDDoc> | ProofOfPossession;
credentialTypes?: string | string[];
Expand All @@ -93,11 +156,7 @@ export class CredentialRequestClient {
else if (!this.isV11OrHigher() && types.length !== 1) {
throw Error('Only a single credential type is supported for V8/V9');
}

const proof =
'proof_type' in proofInput
? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build()
: await proofInput.build();
const proof = await buildProof(proofInput, opts);

// TODO: we should move format specific logic
if (format === 'jwt_vc_json' || format === 'jwt_vc') {
Expand Down
39 changes: 32 additions & 7 deletions packages/client/lib/CredentialRequestClientBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { CredentialRequestClient } from './CredentialRequestClient';

export class CredentialRequestClientBuilder {
credentialEndpoint?: string;
deferredCredentialEndpoint?: string;
deferredCredentialAwait = false;
deferredCredentialIntervalInMS = 5000;
credentialTypes: string[] = [];
format?: CredentialFormat | OID4VCICredentialFormat;
token?: string;
Expand All @@ -38,6 +41,9 @@ export class CredentialRequestClientBuilder {
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);
}
builder.withCredentialType(credentialTypes);
return builder;
}
Expand All @@ -60,6 +66,9 @@ export class 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);
}

if (version <= OpenId4VCIVersion.VER_1_0_08) {
//todo: This basically sets all types available during initiation. Probably the user only wants a subset. So do we want to do this?
Expand All @@ -86,37 +95,53 @@ export class CredentialRequestClientBuilder {
});
}

public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata): CredentialRequestClientBuilder {
public withCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata): this {
this.credentialEndpoint = metadata.credential_endpoint;
return this;
}

public withCredentialEndpoint(credentialEndpoint: string): CredentialRequestClientBuilder {
public withCredentialEndpoint(credentialEndpoint: string): this {
this.credentialEndpoint = credentialEndpoint;
return this;
}

public withCredentialType(credentialTypes: string | string[]): CredentialRequestClientBuilder {
public withDeferredCredentialEndpointFromMetadata(metadata: CredentialIssuerMetadata): this {
this.deferredCredentialEndpoint = metadata.deferred_credential_endpoint;
return this;
}

public withDeferredCredentialEndpoint(deferredCredentialEndpoint: string): this {
this.deferredCredentialEndpoint = deferredCredentialEndpoint;
return this;
}

public withDeferredCredentialAwait(deferredCredentialAwait: boolean, deferredCredentialIntervalInMS?: number): this {
this.deferredCredentialAwait = deferredCredentialAwait;
this.deferredCredentialIntervalInMS = deferredCredentialIntervalInMS ?? 5000;
return this;
}

public withCredentialType(credentialTypes: string | string[]): this {
this.credentialTypes = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
return this;
}

public withFormat(format: CredentialFormat | OID4VCICredentialFormat): CredentialRequestClientBuilder {
public withFormat(format: CredentialFormat | OID4VCICredentialFormat): this {
this.format = format;
return this;
}

public withToken(accessToken: string): CredentialRequestClientBuilder {
public withToken(accessToken: string): this {
this.token = accessToken;
return this;
}

public withTokenFromResponse(response: AccessTokenResponse): CredentialRequestClientBuilder {
public withTokenFromResponse(response: AccessTokenResponse): this {
this.token = response.access_token;
return this;
}

public withVersion(version: OpenId4VCIVersion): CredentialRequestClientBuilder {
public withVersion(version: OpenId4VCIVersion): this {
this.version = version;
return this;
}
Expand Down
14 changes: 13 additions & 1 deletion packages/client/lib/MetadataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class MetadataClient {
public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResult> {
let token_endpoint: string | undefined;
let credential_endpoint: string | undefined;
let deferred_credential_endpoint: string | undefined;
let authorization_endpoint: string | undefined;
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
let authorization_server: string = issuer;
Expand All @@ -53,6 +54,7 @@ export class MetadataClient {
if (credentialIssuerMetadata) {
debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${JSON.stringify(credentialIssuerMetadata)}`);
credential_endpoint = credentialIssuerMetadata.credential_endpoint;
deferred_credential_endpoint = credentialIssuerMetadata.deferred_credential_endpoint;
if (credentialIssuerMetadata.token_endpoint) {
token_endpoint = credentialIssuerMetadata.token_endpoint;
}
Expand Down Expand Up @@ -111,12 +113,21 @@ export class MetadataClient {
if (authMetadata.credential_endpoint) {
if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) {
debug(
`Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.token_endpoint}). Will use the issuer value`,
`Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.credential_endpoint}). Will use the issuer value`,
);
} else {
credential_endpoint = authMetadata.credential_endpoint;
}
}
if (authMetadata.deferred_credential_endpoint) {
if (deferred_credential_endpoint && authMetadata.deferred_credential_endpoint !== deferred_credential_endpoint) {
debug(
`Credential issuer has a different deferred_credential_endpoint (${deferred_credential_endpoint}) from the Authorization Server (${authMetadata.deferred_credential_endpoint}). Will use the issuer value`,
);
} else {
deferred_credential_endpoint = authMetadata.deferred_credential_endpoint;
}
}
}

if (!authorization_endpoint) {
Expand Down Expand Up @@ -148,6 +159,7 @@ export class MetadataClient {
issuer,
token_endpoint,
credential_endpoint,
deferred_credential_endpoint,
authorization_server,
authorization_endpoint,
authorizationServerType,
Expand Down
12 changes: 12 additions & 0 deletions packages/client/lib/OpenID4VCIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,17 @@ export class OpenID4VCIClient {
kid,
alg,
jti,
deferredCredentialAwait,
deferredCredentialIntervalInMS,
}: {
credentialTypes: string | string[];
proofCallbacks: ProofOfPossessionCallbacks<any>;
format?: CredentialFormat | OID4VCICredentialFormat;
kid?: string;
alg?: Alg | string;
jti?: string;
deferredCredentialAwait?: boolean;
deferredCredentialIntervalInMS?: number;
}): Promise<CredentialResponse> {
if (alg) {
this._alg = alg;
Expand All @@ -382,6 +386,7 @@ export class OpenID4VCIClient {
});

requestBuilder.withTokenFromResponse(this.accessTokenResponse);
requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);
if (this.endpointMetadata?.credentialIssuerMetadata) {
const metadata = this.endpointMetadata.credentialIssuerMetadata;
const types = Array.isArray(credentialTypes) ? [...credentialTypes].sort() : [credentialTypes];
Expand Down Expand Up @@ -550,6 +555,13 @@ export class OpenID4VCIClient {
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
}

public hasDeferredCredentialEndpoint(): boolean {
return !!this.getAccessTokenEndpoint();
}
public getDeferredCredentialEndpoint(): string {
this.assertIssuerData();
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
}
private assertIssuerData(): void {
if (!this._credentialOffer && this.issuerSupportedFlowTypes().includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
throw Error(`No issuance initiation or credential offer present`);
Expand Down
11 changes: 8 additions & 3 deletions packages/client/lib/__tests__/EBSIE2E.spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const kid = `${DID}#z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDD

// const jw = jose.importKey()
describe('OID4VCI-Client using Sphereon issuer should', () => {
async function test(credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossAuthorisedInTime') {
async function test(credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossPreAuthorisedDeferred' | 'CTWalletCrossAuthorisedInTime') {
debug.enable('*');
const offer = await getCredentialOffer(credentialType);
const client = await OpenID4VCIClient.fromURI({
Expand Down Expand Up @@ -93,6 +93,8 @@ describe('OID4VCI-Client using Sphereon issuer should', () => {
signCallback: proofOfPossessionCallbackFunction,
},
kid,
deferredCredentialAwait: true,
deferredCredentialIntervalInMS: 5000,
});
console.log(JSON.stringify(credentialResponse, null, 2));
expect(credentialResponse.credential).toBeDefined();
Expand All @@ -102,17 +104,20 @@ describe('OID4VCI-Client using Sphereon issuer should', () => {

// Current conformance tests is not stable as changes are being applied it seems

it.skip(
it(
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
async () => {
await test('CTWalletCrossPreAuthorisedInTime');
await test('CTWalletCrossPreAuthorisedDeferred');
// await test('CTWalletCrossAuthorisedInTime');
},
UNIT_TEST_TIMEOUT,
);
});

async function getCredentialOffer(credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossAuthorisedInTime'): Promise<string> {
async function getCredentialOffer(
credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossAuthorisedInTime' | 'CTWalletCrossPreAuthorisedDeferred',
): Promise<string> {
const credentialOffer = await fetch(
`https://conformance-test.ebsi.eu/conformance/v3/issuer-mock/initiate-credential-offer?credential_type=${credentialType}&client_id=${DID_URL_ENCODED}&credential_offer_endpoint=openid-credential-offer%3A%2F%2F`,
{
Expand Down
3 changes: 2 additions & 1 deletion packages/client/lib/__tests__/MetadataClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ describe('Metadataclient with Walt-id should', () => {
});
});

describe('Metadataclient with SpruceId should', () => {
// Spruce gives back 404's these days, so test is disabled
describe.skip('Metadataclient with SpruceId should', () => {
beforeAll(() => {
nock.cleanAll();
});
Expand Down
Loading

0 comments on commit 99dc87d

Please sign in to comment.