Skip to content

Commit 9dd2ecf

Browse files
committed
chore: Many fixes to get VCI working for V13 and lower
1 parent 449364b commit 9dd2ecf

18 files changed

Lines changed: 294 additions & 71 deletions

packages/client/lib/AccessTokenClient.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@ import {
66
AuthorizationServerOpts,
77
AuthzFlowType,
88
convertJsonToURI,
9-
CredentialOfferPayloadV1_0_13,
10-
CredentialOfferV1_0_13,
11-
determineSpecVersionFromOffer,
129
EndpointMetadata,
1310
formPost,
1411
getIssuerFromCredentialOfferPayload,
1512
GrantTypes,
1613
IssuerOpts,
1714
JsonURIMode,
18-
OpenId4VCIVersion,
1915
OpenIDResponse,
2016
PRE_AUTH_CODE_LITERAL,
2117
TokenErrorResponse,
@@ -91,11 +87,9 @@ export class AccessTokenClient {
9187

9288
public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
9389
const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
94-
const credentialOfferRequest =
95-
opts.credentialOffer &&
96-
determineSpecVersionFromOffer(opts.credentialOffer as CredentialOfferPayloadV1_0_13).valueOf() <= OpenId4VCIVersion.VER_1_0_13.valueOf()
97-
? await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_13)
98-
: undefined;
90+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
91+
// @ts-ignore
92+
const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : undefined;
9993
const request: Partial<AccessTokenRequest> = {};
10094

10195
if (asOpts?.clientId) {

packages/client/lib/CredentialOfferClient.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import {
22
convertJsonToURI,
33
convertURIToJsonObject,
4+
CredentialOffer,
5+
CredentialOfferPayload,
6+
CredentialOfferPayloadV1_0_09,
47
CredentialOfferRequestWithBaseUrl,
8+
CredentialOfferV1_0_11,
59
CredentialOfferV1_0_13,
610
determineSpecVersionFromURI,
711
getClientIdFromCredentialOfferPayload,
@@ -10,6 +14,8 @@ import {
1014
} from '@sphereon/oid4vci-common';
1115
import Debug from 'debug';
1216

17+
import { LOG } from './types';
18+
1319
const debug = Debug('sphereon:oid4vci:offer');
1420

1521
export class CredentialOfferClient {
@@ -22,15 +28,27 @@ export class CredentialOfferClient {
2228
const scheme = uri.split('://')[0];
2329
const baseUrl = uri.split('?')[0];
2430
const version = determineSpecVersionFromURI(uri);
25-
const credentialOffer = convertURIToJsonObject(uri, {
26-
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
27-
arrayTypeProperties: uri.includes('credential_offer_uri=')
28-
? ['credential_configuration_ids', 'credential_offer_uri=']
29-
: ['credential_configuration_ids', 'credential_offer='],
30-
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
31-
}) as CredentialOfferV1_0_13;
32-
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
33-
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
31+
LOG.log(`Offer URL determined to be of version ${version}`);
32+
let credentialOffer: CredentialOffer;
33+
let credentialOfferPayload: CredentialOfferPayload;
34+
// credential offer was introduced in draft 9 and credential_offer_uri in draft 11
35+
if (version < OpenId4VCIVersion.VER_1_0_11) {
36+
credentialOfferPayload = convertURIToJsonObject(uri, {
37+
arrayTypeProperties: ['credential_type'],
38+
requiredProperties: uri.includes('credential_offer=') ? ['credential_offer'] : ['issuer', 'credential_type'],
39+
}) as CredentialOfferPayloadV1_0_09;
40+
credentialOffer = {
41+
credential_offer: credentialOfferPayload,
42+
};
43+
} else {
44+
credentialOffer = convertURIToJsonObject(uri, {
45+
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
46+
arrayTypeProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
47+
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
48+
}) as CredentialOfferV1_0_11 | CredentialOfferV1_0_13;
49+
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
50+
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
51+
}
3452
}
3553

3654
const request = await toUniformCredentialOfferRequest(credentialOffer, {
@@ -49,6 +67,10 @@ export class CredentialOfferClient {
4967
...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && {
5068
preAuthorizedCode: grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']['pre-authorized_code'],
5169
}),
70+
userPinRequired:
71+
request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.user_pin_required ??
72+
!!request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code ??
73+
false,
5274
...(request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code &&
5375
{
5476
// txCode: request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code,

packages/client/lib/CredentialOfferClientV1_0_11.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ export class CredentialOfferClientV1_0_11 {
5656
return {
5757
scheme,
5858
baseUrl,
59-
clientId,
59+
...(clientId && { clientId }),
6060
...request,
6161
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
6262
...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && {
6363
preAuthorizedCode: grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']['pre-authorized_code'],
6464
}),
65-
userPinRequired: request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.user_pin_required ?? false,
65+
userPinRequired: !!request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.user_pin_required ?? false,
6666
};
6767
}
6868

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
convertJsonToURI,
3+
convertURIToJsonObject,
4+
CredentialOfferRequestWithBaseUrl,
5+
CredentialOfferV1_0_13,
6+
determineSpecVersionFromURI,
7+
getClientIdFromCredentialOfferPayload,
8+
OpenId4VCIVersion,
9+
toUniformCredentialOfferRequest,
10+
} from '@sphereon/oid4vci-common';
11+
import Debug from 'debug';
12+
13+
const debug = Debug('sphereon:oid4vci:offer');
14+
15+
export class CredentialOfferClientV1_0_13 {
16+
public static async fromURI(uri: string, opts?: { resolve?: boolean }): Promise<CredentialOfferRequestWithBaseUrl> {
17+
debug(`Credential Offer URI: ${uri}`);
18+
if (!uri.includes('?') || !uri.includes('://')) {
19+
debug(`Invalid Credential Offer URI: ${uri}`);
20+
throw Error(`Invalid Credential Offer Request`);
21+
}
22+
const scheme = uri.split('://')[0];
23+
const baseUrl = uri.split('?')[0];
24+
const version = determineSpecVersionFromURI(uri);
25+
const credentialOffer = convertURIToJsonObject(uri, {
26+
// It must have the '=' sign after credential_offer otherwise the uri will get split at openid_credential_offer
27+
arrayTypeProperties: uri.includes('credential_offer_uri=')
28+
? ['credential_configuration_ids', 'credential_offer_uri=']
29+
: ['credential_configuration_ids', 'credential_offer='],
30+
requiredProperties: uri.includes('credential_offer_uri=') ? ['credential_offer_uri='] : ['credential_offer='],
31+
}) as CredentialOfferV1_0_13;
32+
if (credentialOffer?.credential_offer_uri === undefined && !credentialOffer?.credential_offer) {
33+
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
34+
}
35+
36+
const request = await toUniformCredentialOfferRequest(credentialOffer, {
37+
...opts,
38+
version,
39+
});
40+
const clientId = getClientIdFromCredentialOfferPayload(request.credential_offer);
41+
const grants = request.credential_offer?.grants;
42+
43+
return {
44+
scheme,
45+
baseUrl,
46+
...(clientId && { clientId }),
47+
...request,
48+
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
49+
...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && {
50+
preAuthorizedCode: grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']['pre-authorized_code'],
51+
}),
52+
userPinRequired: !!request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code ?? false,
53+
...(request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code &&
54+
{
55+
// txCode: request.credential_offer?.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.tx_code,
56+
}),
57+
};
58+
}
59+
60+
public static toURI(
61+
requestWithBaseUrl: CredentialOfferRequestWithBaseUrl,
62+
opts?: {
63+
version?: OpenId4VCIVersion;
64+
},
65+
): string {
66+
debug(`Credential Offer Request with base URL: ${JSON.stringify(requestWithBaseUrl)}`);
67+
const version = opts?.version ?? requestWithBaseUrl.version;
68+
let baseUrl = requestWithBaseUrl.baseUrl.includes(requestWithBaseUrl.scheme)
69+
? requestWithBaseUrl.baseUrl
70+
: `${requestWithBaseUrl.scheme.replace('://', '')}://${requestWithBaseUrl.baseUrl}`;
71+
let param: string | undefined;
72+
73+
const isUri = requestWithBaseUrl.credential_offer_uri !== undefined;
74+
75+
if (version.valueOf() >= OpenId4VCIVersion.VER_1_0_11.valueOf()) {
76+
// v11 changed from encoding every param to a encoded json object with a credential_offer param key
77+
if (!baseUrl.includes('?')) {
78+
param = isUri ? 'credential_offer_uri' : 'credential_offer';
79+
} else {
80+
const split = baseUrl.split('?');
81+
if (split.length > 1 && split[1] !== '') {
82+
if (baseUrl.endsWith('&')) {
83+
param = isUri ? 'credential_offer_uri' : 'credential_offer';
84+
} else if (!baseUrl.endsWith('=')) {
85+
baseUrl += `&`;
86+
param = isUri ? 'credential_offer_uri' : 'credential_offer';
87+
}
88+
}
89+
}
90+
}
91+
return convertJsonToURI(requestWithBaseUrl.credential_offer_uri ?? requestWithBaseUrl.original_credential_offer, {
92+
baseUrl,
93+
arrayTypeProperties: isUri ? [] : ['credential_type'],
94+
uriTypeProperties: isUri
95+
? ['credential_offer_uri']
96+
: version >= OpenId4VCIVersion.VER_1_0_13
97+
? ['credential_issuer', 'credential_type']
98+
: ['issuer', 'credential_type'],
99+
param,
100+
version,
101+
});
102+
}
103+
}

packages/client/lib/CredentialRequestClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class CredentialRequestClient {
108108
uniformRequest: UniformCredentialRequest,
109109
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
110110
if (this.version() < OpenId4VCIVersion.VER_1_0_13) {
111-
throw new Error('Versions below v1.0.13 (draft 13) are not supported.');
111+
throw new Error('Versions below v1.0.13 (draft 13) are not supported by the V13 credential request client.');
112112
}
113113
const request: CredentialRequestV1_0_13 = getCredentialRequestForVersion(uniformRequest, this.version()) as CredentialRequestV1_0_13;
114114
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;

packages/client/lib/CredentialRequestClientBuilderV1_0_11.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CredentialOfferRequestWithBaseUrl,
77
determineSpecVersionFromOffer,
88
EndpointMetadata,
9+
ExperimentalSubjectIssuance,
910
getIssuerFromCredentialOfferPayload,
1011
getTypesFromOfferV1_0_11,
1112
OID4VCICredentialFormat,
@@ -26,6 +27,7 @@ export class CredentialRequestClientBuilderV1_0_11 {
2627
format?: CredentialFormat | OID4VCICredentialFormat;
2728
token?: string;
2829
version?: OpenId4VCIVersion;
30+
subjectIssuance?: ExperimentalSubjectIssuance;
2931

3032
public static fromCredentialIssuer({
3133
credentialIssuer,
@@ -132,6 +134,11 @@ export class CredentialRequestClientBuilderV1_0_11 {
132134
return this;
133135
}
134136

137+
public withSubjectIssuance(subjectIssuance: ExperimentalSubjectIssuance): this {
138+
this.subjectIssuance = subjectIssuance;
139+
return this;
140+
}
141+
135142
public withToken(accessToken: string): this {
136143
this.token = accessToken;
137144
return this;

packages/client/lib/CredentialRequestClientV1_0_11.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,16 @@ export class CredentialRequestClientV1_0_11 {
6464
credentialTypes?: string | string[];
6565
context?: string[];
6666
format?: CredentialFormat | OID4VCICredentialFormat;
67-
}): Promise<OpenIDResponse<CredentialResponse>> {
67+
}): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
6868
const { credentialTypes, proofInput, format, context } = opts;
6969

7070
const request = await this.createCredentialRequest({ proofInput, credentialTypes, context, format, version: this.version() });
7171
return await this.acquireCredentialsUsingRequest(request);
7272
}
7373

74-
public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise<OpenIDResponse<CredentialResponse>> {
74+
public async acquireCredentialsUsingRequest(
75+
uniformRequest: UniformCredentialRequest,
76+
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
7577
const request = getCredentialRequestForVersion(uniformRequest, this.version());
7678
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
7779
if (!isValidURL(credentialEndpoint)) {
@@ -81,11 +83,14 @@ export class CredentialRequestClientV1_0_11 {
8183
debug(`Acquiring credential(s) from: ${credentialEndpoint}`);
8284
debug(`request\n: ${JSON.stringify(request, null, 2)}`);
8385
const requestToken: string = this.credentialRequestOpts.token;
84-
let response: OpenIDResponse<CredentialResponse> = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken });
86+
let response = (await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken })) as OpenIDResponse<CredentialResponse> & {
87+
access_token: string;
88+
};
8589
this._isDeferred = isDeferredCredentialResponse(response);
8690
if (this.isDeferred() && this.credentialRequestOpts.deferredCredentialAwait && response.successBody) {
8791
response = await this.acquireDeferredCredential(response.successBody, { bearerToken: this.credentialRequestOpts.token });
8892
}
93+
response.access_token = requestToken;
8994

9095
debug(`Credential endpoint ${credentialEndpoint} response:\r\n${JSON.stringify(response, null, 2)}`);
9196
return response;
@@ -96,7 +101,7 @@ export class CredentialRequestClientV1_0_11 {
96101
opts?: {
97102
bearerToken?: string;
98103
},
99-
): Promise<OpenIDResponse<CredentialResponse>> {
104+
): Promise<OpenIDResponse<CredentialResponse> & { access_token: string }> {
100105
const transactionId = response.transaction_id;
101106
const bearerToken = response.acceptance_token ?? opts?.bearerToken;
102107
const deferredCredentialEndpoint = this.getDeferredCredentialEndpoint();

0 commit comments

Comments
 (0)