@sphereon/oid4vci-client 0.8.2-next.4 → 0.8.2-next.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/AccessTokenClient.d.ts.map +1 -1
- package/dist/AccessTokenClient.js +9 -9
- package/dist/AccessTokenClient.js.map +1 -1
- package/dist/AuthorizationDetailsBuilder.d.ts.map +1 -1
- package/dist/AuthorizationDetailsBuilder.js.map +1 -1
- package/dist/CredentialOfferClient.js.map +1 -1
- package/dist/CredentialRequestClient.d.ts +13 -0
- package/dist/CredentialRequestClient.d.ts.map +1 -1
- package/dist/CredentialRequestClient.js +87 -41
- package/dist/CredentialRequestClient.js.map +1 -1
- package/dist/CredentialRequestClientBuilder.d.ts +19 -7
- package/dist/CredentialRequestClientBuilder.d.ts.map +1 -1
- package/dist/CredentialRequestClientBuilder.js +31 -1
- package/dist/CredentialRequestClientBuilder.js.map +1 -1
- package/dist/MetadataClient.d.ts.map +1 -1
- package/dist/MetadataClient.js +12 -1
- package/dist/MetadataClient.js.map +1 -1
- package/dist/OpenID4VCIClient.d.ts +19 -5
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +119 -41
- package/dist/OpenID4VCIClient.js.map +1 -1
- package/dist/ProofOfPossessionBuilder.d.ts +3 -1
- package/dist/ProofOfPossessionBuilder.d.ts.map +1 -1
- package/dist/ProofOfPossessionBuilder.js +5 -0
- package/dist/ProofOfPossessionBuilder.js.map +1 -1
- package/dist/functions/ProofUtil.d.ts +2 -1
- package/dist/functions/ProofUtil.d.ts.map +1 -1
- package/dist/functions/ProofUtil.js +6 -4
- package/dist/functions/ProofUtil.js.map +1 -1
- package/lib/AccessTokenClient.ts +11 -9
- package/lib/AuthorizationDetailsBuilder.ts +2 -2
- package/lib/CredentialRequestClient.ts +105 -43
- package/lib/CredentialRequestClientBuilder.ts +53 -8
- package/lib/MetadataClient.ts +13 -1
- package/lib/OpenID4VCIClient.ts +152 -42
- package/lib/ProofOfPossessionBuilder.ts +8 -0
- package/lib/__tests__/CredentialRequestClient.spec.ts +21 -9
- package/lib/__tests__/EBSIE2E.spec.test.ts +143 -0
- package/lib/__tests__/MetadataClient.spec.ts +4 -1
- package/lib/__tests__/OpenID4VCIClient.spec.ts +5 -0
- package/lib/__tests__/SdJwt.spec.ts +161 -0
- package/lib/__tests__/data/VciDataFixtures.ts +14 -13
- package/lib/functions/ProofUtil.ts +18 -4
- package/package.json +8 -6
package/lib/OpenID4VCIClient.ts
CHANGED
|
@@ -8,15 +8,18 @@ import {
|
|
|
8
8
|
CredentialResponse,
|
|
9
9
|
CredentialSupported,
|
|
10
10
|
EndpointMetadataResult,
|
|
11
|
+
getIssuerFromCredentialOfferPayload,
|
|
12
|
+
getSupportedCredentials,
|
|
13
|
+
getTypesFromCredentialSupported,
|
|
11
14
|
JsonURIMode,
|
|
15
|
+
JWK,
|
|
16
|
+
KID_JWK_X5C_ERROR,
|
|
12
17
|
OID4VCICredentialFormat,
|
|
13
18
|
OpenId4VCIVersion,
|
|
14
19
|
ProofOfPossessionCallbacks,
|
|
15
20
|
PushedAuthorizationResponse,
|
|
16
21
|
ResponseType,
|
|
17
22
|
} from '@sphereon/oid4vci-common';
|
|
18
|
-
import { getSupportedCredentials } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
|
|
19
|
-
import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types';
|
|
20
23
|
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
21
24
|
import Debug from 'debug';
|
|
22
25
|
|
|
@@ -39,27 +42,66 @@ interface AuthDetails {
|
|
|
39
42
|
|
|
40
43
|
interface AuthRequestOpts {
|
|
41
44
|
codeChallenge: string;
|
|
42
|
-
codeChallengeMethod
|
|
45
|
+
codeChallengeMethod?: CodeChallengeMethod;
|
|
43
46
|
authorizationDetails?: AuthDetails | AuthDetails[];
|
|
44
47
|
redirectUri: string;
|
|
45
48
|
scope?: string;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export class OpenID4VCIClient {
|
|
49
|
-
private readonly _credentialOffer
|
|
52
|
+
private readonly _credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
53
|
+
private _credentialIssuer: string;
|
|
50
54
|
private _clientId?: string;
|
|
51
55
|
private _kid: string | undefined;
|
|
56
|
+
private _jwk: JWK | undefined;
|
|
52
57
|
private _alg: Alg | string | undefined;
|
|
53
58
|
private _endpointMetadata: EndpointMetadataResult | undefined;
|
|
54
59
|
private _accessTokenResponse: AccessTokenResponse | undefined;
|
|
55
60
|
|
|
56
|
-
private constructor(
|
|
61
|
+
private constructor({
|
|
62
|
+
credentialOffer,
|
|
63
|
+
clientId,
|
|
64
|
+
kid,
|
|
65
|
+
alg,
|
|
66
|
+
credentialIssuer,
|
|
67
|
+
}: {
|
|
68
|
+
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
69
|
+
kid?: string;
|
|
70
|
+
alg?: Alg | string;
|
|
71
|
+
clientId?: string;
|
|
72
|
+
credentialIssuer?: string;
|
|
73
|
+
}) {
|
|
57
74
|
this._credentialOffer = credentialOffer;
|
|
75
|
+
const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
|
|
76
|
+
if (!issuer) {
|
|
77
|
+
throw Error('No credential issuer supplied or deduced from offer');
|
|
78
|
+
}
|
|
79
|
+
this._credentialIssuer = issuer;
|
|
58
80
|
this._kid = kid;
|
|
59
81
|
this._alg = alg;
|
|
60
82
|
this._clientId = clientId;
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
public static async fromCredentialIssuer({
|
|
86
|
+
kid,
|
|
87
|
+
alg,
|
|
88
|
+
retrieveServerMetadata,
|
|
89
|
+
clientId,
|
|
90
|
+
credentialIssuer,
|
|
91
|
+
}: {
|
|
92
|
+
credentialIssuer: string;
|
|
93
|
+
kid?: string;
|
|
94
|
+
alg?: Alg | string;
|
|
95
|
+
retrieveServerMetadata?: boolean;
|
|
96
|
+
clientId?: string;
|
|
97
|
+
}) {
|
|
98
|
+
const client = new OpenID4VCIClient({ kid, alg, clientId, credentialIssuer });
|
|
99
|
+
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
100
|
+
await client.retrieveServerMetadata();
|
|
101
|
+
}
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
public static async fromURI({
|
|
64
106
|
uri,
|
|
65
107
|
kid,
|
|
@@ -75,7 +117,12 @@ export class OpenID4VCIClient {
|
|
|
75
117
|
resolveOfferUri?: boolean;
|
|
76
118
|
clientId?: string;
|
|
77
119
|
}): Promise<OpenID4VCIClient> {
|
|
78
|
-
const client = new OpenID4VCIClient(
|
|
120
|
+
const client = new OpenID4VCIClient({
|
|
121
|
+
credentialOffer: await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }),
|
|
122
|
+
kid,
|
|
123
|
+
alg,
|
|
124
|
+
clientId,
|
|
125
|
+
});
|
|
79
126
|
|
|
80
127
|
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
81
128
|
await client.retrieveServerMetadata();
|
|
@@ -86,16 +133,41 @@ export class OpenID4VCIClient {
|
|
|
86
133
|
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
|
|
87
134
|
this.assertIssuerData();
|
|
88
135
|
if (!this._endpointMetadata) {
|
|
89
|
-
|
|
136
|
+
if (this.credentialOffer) {
|
|
137
|
+
this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
|
|
138
|
+
} else if (this._credentialIssuer) {
|
|
139
|
+
this._endpointMetadata = await MetadataClient.retrieveAllMetadata(this._credentialIssuer);
|
|
140
|
+
} else {
|
|
141
|
+
throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`);
|
|
142
|
+
}
|
|
90
143
|
}
|
|
91
144
|
return this.endpointMetadata;
|
|
92
145
|
}
|
|
93
146
|
|
|
147
|
+
// todo: Unify this method with the par method
|
|
148
|
+
|
|
94
149
|
public createAuthorizationRequestUrl({ codeChallengeMethod, codeChallenge, authorizationDetails, redirectUri, scope }: AuthRequestOpts): string {
|
|
95
150
|
// Scope and authorization_details can be used in the same authorization request
|
|
96
151
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
97
152
|
if (!scope && !authorizationDetails) {
|
|
98
|
-
|
|
153
|
+
if (!this.credentialOffer) {
|
|
154
|
+
throw Error('Please provide a scope or authorization_details');
|
|
155
|
+
}
|
|
156
|
+
const creds = this.credentialOffer.credential_offer.credentials;
|
|
157
|
+
|
|
158
|
+
authorizationDetails = creds
|
|
159
|
+
.flatMap((cred) => (typeof cred === 'string' ? this.getCredentialsSupported(true) : (cred as CredentialSupported)))
|
|
160
|
+
.map((cred) => {
|
|
161
|
+
return {
|
|
162
|
+
...cred,
|
|
163
|
+
type: 'openid_credential',
|
|
164
|
+
locations: [this._credentialIssuer],
|
|
165
|
+
format: cred.format,
|
|
166
|
+
} satisfies AuthDetails;
|
|
167
|
+
});
|
|
168
|
+
if (authorizationDetails.length === 0) {
|
|
169
|
+
throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
|
|
170
|
+
}
|
|
99
171
|
}
|
|
100
172
|
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
101
173
|
// handling this because of the support for v1_0-08
|
|
@@ -117,7 +189,7 @@ export class OpenID4VCIClient {
|
|
|
117
189
|
|
|
118
190
|
const queryObj: { [key: string]: string } = {
|
|
119
191
|
response_type: ResponseType.AUTH_CODE,
|
|
120
|
-
code_challenge_method: codeChallengeMethod,
|
|
192
|
+
code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
|
|
121
193
|
code_challenge: codeChallenge,
|
|
122
194
|
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
123
195
|
redirect_uri: redirectUri,
|
|
@@ -128,7 +200,7 @@ export class OpenID4VCIClient {
|
|
|
128
200
|
queryObj['client_id'] = this.clientId;
|
|
129
201
|
}
|
|
130
202
|
|
|
131
|
-
if (this.credentialOffer
|
|
203
|
+
if (this.credentialOffer?.issuerState) {
|
|
132
204
|
queryObj['issuer_state'] = this.credentialOffer.issuerState;
|
|
133
205
|
}
|
|
134
206
|
|
|
@@ -140,6 +212,7 @@ export class OpenID4VCIClient {
|
|
|
140
212
|
});
|
|
141
213
|
}
|
|
142
214
|
|
|
215
|
+
// todo: Unify this method with the create auth request url method
|
|
143
216
|
public async acquirePushedAuthorizationRequestURI({
|
|
144
217
|
codeChallengeMethod,
|
|
145
218
|
codeChallenge,
|
|
@@ -173,7 +246,7 @@ export class OpenID4VCIClient {
|
|
|
173
246
|
|
|
174
247
|
const queryObj: { [key: string]: string } = {
|
|
175
248
|
response_type: ResponseType.AUTH_CODE,
|
|
176
|
-
code_challenge_method: codeChallengeMethod,
|
|
249
|
+
code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
|
|
177
250
|
code_challenge: codeChallenge,
|
|
178
251
|
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
179
252
|
redirect_uri: redirectUri,
|
|
@@ -184,7 +257,7 @@ export class OpenID4VCIClient {
|
|
|
184
257
|
queryObj['client_id'] = this.clientId;
|
|
185
258
|
}
|
|
186
259
|
|
|
187
|
-
if (this.credentialOffer
|
|
260
|
+
if (this.credentialOffer?.issuerState) {
|
|
188
261
|
queryObj['issuer_state'] = this.credentialOffer.issuerState;
|
|
189
262
|
}
|
|
190
263
|
|
|
@@ -249,6 +322,7 @@ export class OpenID4VCIClient {
|
|
|
249
322
|
const response = await accessTokenClient.acquireAccessToken({
|
|
250
323
|
credentialOffer: this.credentialOffer,
|
|
251
324
|
metadata: this.endpointMetadata,
|
|
325
|
+
credentialIssuer: this.getIssuer(),
|
|
252
326
|
pin,
|
|
253
327
|
codeVerifier,
|
|
254
328
|
code,
|
|
@@ -281,53 +355,67 @@ export class OpenID4VCIClient {
|
|
|
281
355
|
proofCallbacks,
|
|
282
356
|
format,
|
|
283
357
|
kid,
|
|
358
|
+
jwk,
|
|
284
359
|
alg,
|
|
285
360
|
jti,
|
|
361
|
+
deferredCredentialAwait,
|
|
362
|
+
deferredCredentialIntervalInMS,
|
|
286
363
|
}: {
|
|
287
364
|
credentialTypes: string | string[];
|
|
288
365
|
proofCallbacks: ProofOfPossessionCallbacks<any>;
|
|
289
366
|
format?: CredentialFormat | OID4VCICredentialFormat;
|
|
290
367
|
kid?: string;
|
|
368
|
+
jwk?: JWK;
|
|
291
369
|
alg?: Alg | string;
|
|
292
370
|
jti?: string;
|
|
371
|
+
deferredCredentialAwait?: boolean;
|
|
372
|
+
deferredCredentialIntervalInMS?: number;
|
|
293
373
|
}): Promise<CredentialResponse> {
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
if (kid) {
|
|
298
|
-
this._kid = kid;
|
|
374
|
+
if ([jwk, kid].filter((v) => v !== undefined).length > 1) {
|
|
375
|
+
throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
|
|
299
376
|
}
|
|
300
377
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
378
|
+
if (alg) this._alg = alg;
|
|
379
|
+
if (jwk) this._jwk = jwk;
|
|
380
|
+
if (kid) this._kid = kid;
|
|
381
|
+
|
|
382
|
+
const requestBuilder = this.credentialOffer
|
|
383
|
+
? CredentialRequestClientBuilder.fromCredentialOffer({
|
|
384
|
+
credentialOffer: this.credentialOffer,
|
|
385
|
+
metadata: this.endpointMetadata,
|
|
386
|
+
})
|
|
387
|
+
: CredentialRequestClientBuilder.fromCredentialIssuer({
|
|
388
|
+
credentialIssuer: this.getIssuer(),
|
|
389
|
+
credentialTypes,
|
|
390
|
+
metadata: this.endpointMetadata,
|
|
391
|
+
version: this.version(),
|
|
392
|
+
});
|
|
305
393
|
|
|
306
394
|
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
|
|
395
|
+
requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);
|
|
307
396
|
if (this.endpointMetadata?.credentialIssuerMetadata) {
|
|
308
397
|
const metadata = this.endpointMetadata.credentialIssuerMetadata;
|
|
309
|
-
const types = Array.isArray(credentialTypes) ? credentialTypes.sort() : [credentialTypes];
|
|
398
|
+
const types = Array.isArray(credentialTypes) ? [...credentialTypes].sort() : [credentialTypes];
|
|
310
399
|
|
|
311
400
|
if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
312
401
|
let typeSupported = false;
|
|
313
402
|
|
|
314
403
|
metadata.credentials_supported.forEach((supportedCredential) => {
|
|
315
|
-
|
|
316
|
-
throw Error('types is required in the credentials supported');
|
|
317
|
-
}
|
|
404
|
+
const subTypes = getTypesFromCredentialSupported(supportedCredential);
|
|
318
405
|
if (
|
|
319
|
-
|
|
320
|
-
(types.length === 1 && (types[0] === supportedCredential.id ||
|
|
406
|
+
subTypes.sort().every((t, i) => types[i] === t) ||
|
|
407
|
+
(types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0])))
|
|
321
408
|
) {
|
|
322
409
|
typeSupported = true;
|
|
323
410
|
}
|
|
324
411
|
});
|
|
325
412
|
|
|
326
413
|
if (!typeSupported) {
|
|
327
|
-
|
|
414
|
+
console.log(`Not all credential types ${JSON.stringify(credentialTypes)} are present in metadata for ${this.getIssuer()}`);
|
|
415
|
+
// throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
328
416
|
}
|
|
329
417
|
} else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) {
|
|
330
|
-
const credentialsSupported = metadata.credentials_supported
|
|
418
|
+
const credentialsSupported = metadata.credentials_supported;
|
|
331
419
|
if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) {
|
|
332
420
|
throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
333
421
|
}
|
|
@@ -341,8 +429,14 @@ export class OpenID4VCIClient {
|
|
|
341
429
|
version: this.version(),
|
|
342
430
|
})
|
|
343
431
|
.withIssuer(this.getIssuer())
|
|
344
|
-
.withAlg(this.alg)
|
|
345
|
-
|
|
432
|
+
.withAlg(this.alg);
|
|
433
|
+
|
|
434
|
+
if (this._jwk) {
|
|
435
|
+
proofBuilder.withJWK(this._jwk);
|
|
436
|
+
}
|
|
437
|
+
if (this._kid) {
|
|
438
|
+
proofBuilder.withKid(this._kid);
|
|
439
|
+
}
|
|
346
440
|
|
|
347
441
|
if (this.clientId) {
|
|
348
442
|
proofBuilder.withClientId(this.clientId);
|
|
@@ -356,7 +450,7 @@ export class OpenID4VCIClient {
|
|
|
356
450
|
format,
|
|
357
451
|
});
|
|
358
452
|
if (response.errorBody) {
|
|
359
|
-
debug(`Credential request error:\r\n${response.errorBody}`);
|
|
453
|
+
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
|
|
360
454
|
throw Error(
|
|
361
455
|
`Retrieving a credential from ${this._endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
362
456
|
response.origResponse.status
|
|
@@ -389,7 +483,9 @@ export class OpenID4VCIClient {
|
|
|
389
483
|
}
|
|
390
484
|
|
|
391
485
|
getCredentialOfferTypes(): string[][] {
|
|
392
|
-
if (this.credentialOffer
|
|
486
|
+
if (!this.credentialOffer) {
|
|
487
|
+
return [];
|
|
488
|
+
} else if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
393
489
|
const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
|
|
394
490
|
const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
|
|
395
491
|
const result: string[][] = [];
|
|
@@ -397,21 +493,29 @@ export class OpenID4VCIClient {
|
|
|
397
493
|
return result;
|
|
398
494
|
} else {
|
|
399
495
|
return this.credentialOffer.credential_offer.credentials.map((c) => {
|
|
400
|
-
|
|
496
|
+
if (typeof c === 'string') {
|
|
497
|
+
return [c];
|
|
498
|
+
} else if ('types' in c) {
|
|
499
|
+
return c.types;
|
|
500
|
+
} else if ('vct' in c) {
|
|
501
|
+
return [c.vct];
|
|
502
|
+
} else {
|
|
503
|
+
return c.credential_definition.types;
|
|
504
|
+
}
|
|
401
505
|
});
|
|
402
506
|
}
|
|
403
507
|
}
|
|
404
508
|
|
|
405
509
|
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
406
|
-
return this.credentialOffer.
|
|
510
|
+
return this.credentialOffer?.supportedFlows ?? [AuthzFlowType.AUTHORIZATION_CODE_FLOW];
|
|
407
511
|
}
|
|
408
512
|
|
|
409
|
-
get credentialOffer(): CredentialOfferRequestWithBaseUrl {
|
|
513
|
+
get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
|
|
410
514
|
return this._credentialOffer;
|
|
411
515
|
}
|
|
412
516
|
|
|
413
517
|
public version(): OpenId4VCIVersion {
|
|
414
|
-
return this.credentialOffer.
|
|
518
|
+
return this.credentialOffer?.version ?? OpenId4VCIVersion.VER_1_0_11;
|
|
415
519
|
}
|
|
416
520
|
|
|
417
521
|
public get endpointMetadata(): EndpointMetadataResult {
|
|
@@ -437,9 +541,6 @@ export class OpenID4VCIClient {
|
|
|
437
541
|
}
|
|
438
542
|
|
|
439
543
|
get clientId(): string | undefined {
|
|
440
|
-
/*if (!this._clientId) {
|
|
441
|
-
throw Error('No client id present');
|
|
442
|
-
}*/
|
|
443
544
|
return this._clientId;
|
|
444
545
|
}
|
|
445
546
|
|
|
@@ -451,7 +552,7 @@ export class OpenID4VCIClient {
|
|
|
451
552
|
|
|
452
553
|
public getIssuer(): string {
|
|
453
554
|
this.assertIssuerData();
|
|
454
|
-
return this.
|
|
555
|
+
return this._credentialIssuer!;
|
|
455
556
|
}
|
|
456
557
|
|
|
457
558
|
public getAccessTokenEndpoint(): string {
|
|
@@ -466,9 +567,18 @@ export class OpenID4VCIClient {
|
|
|
466
567
|
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
|
|
467
568
|
}
|
|
468
569
|
|
|
570
|
+
public hasDeferredCredentialEndpoint(): boolean {
|
|
571
|
+
return !!this.getAccessTokenEndpoint();
|
|
572
|
+
}
|
|
573
|
+
public getDeferredCredentialEndpoint(): string {
|
|
574
|
+
this.assertIssuerData();
|
|
575
|
+
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
|
|
576
|
+
}
|
|
469
577
|
private assertIssuerData(): void {
|
|
470
|
-
if (!this._credentialOffer) {
|
|
578
|
+
if (!this._credentialOffer && this.issuerSupportedFlowTypes().includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
|
|
471
579
|
throw Error(`No issuance initiation or credential offer present`);
|
|
580
|
+
} else if (!this._credentialIssuer) {
|
|
581
|
+
throw Error(`No credential issuer value present`);
|
|
472
582
|
}
|
|
473
583
|
}
|
|
474
584
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
AccessTokenResponse,
|
|
3
3
|
Alg,
|
|
4
4
|
EndpointMetadata,
|
|
5
|
+
JWK,
|
|
5
6
|
Jwt,
|
|
6
7
|
NO_JWT_PROVIDED,
|
|
7
8
|
OpenId4VCIVersion,
|
|
@@ -19,6 +20,7 @@ export class ProofOfPossessionBuilder<DIDDoc> {
|
|
|
19
20
|
private readonly version: OpenId4VCIVersion;
|
|
20
21
|
|
|
21
22
|
private kid?: string;
|
|
23
|
+
private jwk?: JWK;
|
|
22
24
|
private clientId?: string;
|
|
23
25
|
private issuer?: string;
|
|
24
26
|
private jwt?: Jwt;
|
|
@@ -91,6 +93,11 @@ export class ProofOfPossessionBuilder<DIDDoc> {
|
|
|
91
93
|
return this;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
withJWK(jwk: JWK): this {
|
|
97
|
+
this.jwk = jwk;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
withIssuer(issuer: string): this {
|
|
95
102
|
this.issuer = issuer;
|
|
96
103
|
return this;
|
|
@@ -182,6 +189,7 @@ export class ProofOfPossessionBuilder<DIDDoc> {
|
|
|
182
189
|
{
|
|
183
190
|
typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'),
|
|
184
191
|
kid: this.kid,
|
|
192
|
+
jwk: this.jwk,
|
|
185
193
|
jti: this.jti,
|
|
186
194
|
alg: this.alg,
|
|
187
195
|
issuer: this.issuer,
|
|
@@ -3,6 +3,7 @@ import { KeyObject } from 'crypto';
|
|
|
3
3
|
import {
|
|
4
4
|
Alg,
|
|
5
5
|
EndpointMetadata,
|
|
6
|
+
getCredentialRequestForVersion,
|
|
6
7
|
getIssuerFromCredentialOfferPayload,
|
|
7
8
|
Jwt,
|
|
8
9
|
OpenId4VCIVersion,
|
|
@@ -127,7 +128,7 @@ describe('Credential Request Client ', () => {
|
|
|
127
128
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
128
129
|
});
|
|
129
130
|
expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy();
|
|
130
|
-
expect(credentialRequest.format).toEqual('
|
|
131
|
+
expect(credentialRequest.format).toEqual('jwt_vc');
|
|
131
132
|
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
|
|
132
133
|
expect(result?.successBody?.credential).toEqual(mockedVC);
|
|
133
134
|
});
|
|
@@ -149,17 +150,22 @@ describe('Credential Request Client ', () => {
|
|
|
149
150
|
.withKid(kid)
|
|
150
151
|
.withClientId('sphereon:wallet')
|
|
151
152
|
.build();
|
|
152
|
-
|
|
153
|
-
// @ts-ignore
|
|
154
|
-
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json-ld', types: ['random'], proof })).rejects.toThrow(
|
|
153
|
+
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json', types: ['random'], proof })).rejects.toThrow(
|
|
155
154
|
Error(URL_NOT_VALID),
|
|
156
155
|
);
|
|
157
156
|
});
|
|
158
157
|
});
|
|
159
158
|
|
|
160
159
|
describe('Credential Request Client with Walt.id ', () => {
|
|
161
|
-
|
|
160
|
+
beforeAll(() => {
|
|
162
161
|
nock.cleanAll();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
nock.cleanAll();
|
|
166
|
+
});
|
|
167
|
+
it('should have correct metadata endpoints', async function () {
|
|
168
|
+
// nock.cleanAll();
|
|
163
169
|
const WALT_IRR_URI =
|
|
164
170
|
'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false';
|
|
165
171
|
const credentialOffer = await CredentialOfferClient.fromURI(WALT_IRR_URI);
|
|
@@ -194,10 +200,11 @@ describe('Credential Request Client with different issuers ', () => {
|
|
|
194
200
|
jwt: getMockData('spruce')?.credential.request.proof.jwt as string,
|
|
195
201
|
},
|
|
196
202
|
credentialTypes: ['OpenBadgeCredential'],
|
|
197
|
-
format: '
|
|
203
|
+
format: 'jwt_vc',
|
|
198
204
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
199
205
|
});
|
|
200
|
-
|
|
206
|
+
const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08);
|
|
207
|
+
expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request);
|
|
201
208
|
});
|
|
202
209
|
|
|
203
210
|
it('should create correct CredentialRequest for Walt', async () => {
|
|
@@ -264,7 +271,8 @@ describe('Credential Request Client with different issuers ', () => {
|
|
|
264
271
|
format: 'ldp_vc',
|
|
265
272
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
266
273
|
});
|
|
267
|
-
|
|
274
|
+
const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08);
|
|
275
|
+
expect(credentialRequest).toEqual(getMockData('mattr')?.credential.request);
|
|
268
276
|
});
|
|
269
277
|
|
|
270
278
|
it('should create correct CredentialRequest for diwala', async () => {
|
|
@@ -286,6 +294,10 @@ describe('Credential Request Client with different issuers ', () => {
|
|
|
286
294
|
format: 'ldp_vc',
|
|
287
295
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
288
296
|
});
|
|
289
|
-
|
|
297
|
+
|
|
298
|
+
// createCredentialRequest returns uniform format in draft 11
|
|
299
|
+
const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08);
|
|
300
|
+
|
|
301
|
+
expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request);
|
|
290
302
|
});
|
|
291
303
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Alg, CodeChallengeMethod, Jwt } from '@sphereon/oid4vci-common';
|
|
2
|
+
import { toJwk } from '@sphereon/ssi-sdk-ext.key-utils';
|
|
3
|
+
import { CredentialMapper } from '@sphereon/ssi-types';
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
5
|
+
//@ts-ignore
|
|
6
|
+
import { from } from '@trust/keyto';
|
|
7
|
+
import { fetch } from 'cross-fetch';
|
|
8
|
+
import debug from 'debug';
|
|
9
|
+
import { base64url, importJWK, JWK, SignJWT } from 'jose';
|
|
10
|
+
import * as u8a from 'uint8arrays';
|
|
11
|
+
|
|
12
|
+
import { OpenID4VCIClient } from '..';
|
|
13
|
+
|
|
14
|
+
export const UNIT_TEST_TIMEOUT = 30000;
|
|
15
|
+
|
|
16
|
+
const ISSUER_URL = 'https://conformance-test.ebsi.eu/conformance/v3/issuer-mock';
|
|
17
|
+
const AUTH_URL = 'https://conformance-test.ebsi.eu/conformance/v3/auth-mock';
|
|
18
|
+
|
|
19
|
+
const jwk: JWK = {
|
|
20
|
+
alg: 'ES256',
|
|
21
|
+
use: 'sig',
|
|
22
|
+
kty: 'EC',
|
|
23
|
+
crv: 'P-256',
|
|
24
|
+
x: 'hUWYK06qFvdudydiqnEhVJhZ-73jcLtuzH8kIyNOSHE',
|
|
25
|
+
y: 'UZf7oUkJdo65SQekMD5ssiRclEimG2SmlsjXf3QwQJo',
|
|
26
|
+
d: 'zDeeo3K0Pk8dofeKcajvJYxMZ1vijx_cVDJQl1IpbAM',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
console.log(`JWK (private/orig): ${JSON.stringify(jwk, null, 2)}`);
|
|
30
|
+
|
|
31
|
+
const privateKey = from(jwk, 'jwk').toString('blk', 'private');
|
|
32
|
+
const publicKey = from(jwk, 'jwk').toString('blk', 'public');
|
|
33
|
+
console.log(`Private key: ${privateKey}`);
|
|
34
|
+
console.log(`Public key: ${publicKey}`);
|
|
35
|
+
console.log(`Private key (b64): ${base64url.encode(u8a.fromString(privateKey, 'base16'))}`);
|
|
36
|
+
console.log(`JWK (private 2) ${JSON.stringify(toJwk(privateKey, 'Secp256r1', { isPrivateKey: true }))}`);
|
|
37
|
+
console.log(`JWK (public 2) ${JSON.stringify(toJwk(publicKey, 'Secp256r1', { isPrivateKey: false }))}`);
|
|
38
|
+
|
|
39
|
+
// const DID_METHOD = 'did:key'
|
|
40
|
+
const DID =
|
|
41
|
+
'did:key:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDDhR1QJ5RHPMXUq5MzYpZL2k35vya5eMiNxschNy9AJ74CC3MmcYiZJGZfyhWQ6qDgTVcDSHdquwPYvLDut383JbrgYdZYYSC2merTMgmQtUi3huYhaky1qE';
|
|
42
|
+
const DID_URL_ENCODED =
|
|
43
|
+
'did%3Akey%3Az2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDDhR1QJ5RHPMXUq5MzYpZL2k35vya5eMiNxschNy9AJ74CC3MmcYiZJGZfyhWQ6qDgTVcDSHdquwPYvLDut383JbrgYdZYYSC2merTMgmQtUi3huYhaky1qE';
|
|
44
|
+
// const PRIVATE_KEY_HEX = '7dd923e40f4615ac496119f7e793cc2899e99b64b88ca8603db986700089532b'
|
|
45
|
+
|
|
46
|
+
// const PUBLIC_KEY_HEX =
|
|
47
|
+
// '04a23cb4c83901acc2eb0f852599610de0caeac260bf8ed05e7f902eaac0f9c8d74dd4841b94d13424d32af8ec0e9976db9abfa7e3a59e10d565c5d4d901b4be63'
|
|
48
|
+
|
|
49
|
+
// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
|
|
50
|
+
// priv hex: 913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5
|
|
51
|
+
// const did = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
|
|
52
|
+
const kid = `${DID}#z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbrm54tL4pRrDDhR1QJ5RHPMXUq5MzYpZL2k35vya5eMiNxschNy9AJ74CC3MmcYiZJGZfyhWQ6qDgTVcDSHdquwPYvLDut383JbrgYdZYYSC2merTMgmQtUi3huYhaky1qE`;
|
|
53
|
+
|
|
54
|
+
// const jw = jose.importKey()
|
|
55
|
+
describe('OID4VCI-Client using Sphereon issuer should', () => {
|
|
56
|
+
async function test(credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossPreAuthorisedDeferred' | 'CTWalletCrossAuthorisedInTime') {
|
|
57
|
+
debug.enable('*');
|
|
58
|
+
const offer = await getCredentialOffer(credentialType);
|
|
59
|
+
const client = await OpenID4VCIClient.fromURI({
|
|
60
|
+
uri: offer,
|
|
61
|
+
kid,
|
|
62
|
+
alg: Alg.ES256,
|
|
63
|
+
clientId: DID_URL_ENCODED,
|
|
64
|
+
});
|
|
65
|
+
expect(client.credentialOffer).toBeDefined();
|
|
66
|
+
expect(client.endpointMetadata).toBeDefined();
|
|
67
|
+
expect(client.getCredentialEndpoint()).toEqual(`${ISSUER_URL}/credential`);
|
|
68
|
+
expect(client.getAccessTokenEndpoint()).toEqual(`${AUTH_URL}/token`);
|
|
69
|
+
|
|
70
|
+
if (credentialType !== 'CTWalletCrossPreAuthorisedInTime') {
|
|
71
|
+
const url = client.createAuthorizationRequestUrl({
|
|
72
|
+
redirectUri: 'openid4vc%3A',
|
|
73
|
+
codeChallenge: 'mE2kPHmIprOqtkaYmESWj35yz-PB5vzdiSu0tAZ8sqs',
|
|
74
|
+
codeChallengeMethod: CodeChallengeMethod.SHA256,
|
|
75
|
+
});
|
|
76
|
+
const result = await fetch(url);
|
|
77
|
+
console.log(result.text());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const accessToken = await client.acquireAccessToken({ pin: '0891' });
|
|
81
|
+
// console.log(accessToken);
|
|
82
|
+
expect(accessToken).toMatchObject({
|
|
83
|
+
expires_in: 86400,
|
|
84
|
+
// scope: 'GuestCredential',
|
|
85
|
+
token_type: 'Bearer',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const format = 'jwt_vc';
|
|
89
|
+
const credentialResponse = await client.acquireCredentials({
|
|
90
|
+
credentialTypes: client.getCredentialOfferTypes()[0],
|
|
91
|
+
format,
|
|
92
|
+
proofCallbacks: {
|
|
93
|
+
signCallback: proofOfPossessionCallbackFunction,
|
|
94
|
+
},
|
|
95
|
+
kid,
|
|
96
|
+
deferredCredentialAwait: true,
|
|
97
|
+
deferredCredentialIntervalInMS: 5000,
|
|
98
|
+
});
|
|
99
|
+
console.log(JSON.stringify(credentialResponse, null, 2));
|
|
100
|
+
expect(credentialResponse.credential).toBeDefined();
|
|
101
|
+
const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!);
|
|
102
|
+
expect(format.startsWith(wrappedVC.format)).toEqual(true);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Current conformance tests is not stable as changes are being applied it seems
|
|
106
|
+
|
|
107
|
+
it(
|
|
108
|
+
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
|
|
109
|
+
async () => {
|
|
110
|
+
await test('CTWalletCrossPreAuthorisedInTime');
|
|
111
|
+
await test('CTWalletCrossPreAuthorisedDeferred');
|
|
112
|
+
// await test('CTWalletCrossAuthorisedInTime');
|
|
113
|
+
},
|
|
114
|
+
UNIT_TEST_TIMEOUT,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
async function getCredentialOffer(
|
|
119
|
+
credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossAuthorisedInTime' | 'CTWalletCrossPreAuthorisedDeferred',
|
|
120
|
+
): Promise<string> {
|
|
121
|
+
const credentialOffer = await fetch(
|
|
122
|
+
`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`,
|
|
123
|
+
{
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: {
|
|
126
|
+
Accept: 'application/json',
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return await credentialOffer.text();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
|
|
136
|
+
const importedJwk = await importJWK(jwk);
|
|
137
|
+
return await new SignJWT({ ...args.payload })
|
|
138
|
+
.setProtectedHeader({ ...args.header, kid: kid! })
|
|
139
|
+
.setIssuer(DID)
|
|
140
|
+
.setIssuedAt()
|
|
141
|
+
.setExpirationTime('2m')
|
|
142
|
+
.sign(importedJwk);
|
|
143
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getIssuerFromCredentialOfferPayload, WellKnownEndpoints } from '@sphereon/oid4vci-common';
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
+
// @ts-ignore
|
|
2
4
|
import nock from 'nock';
|
|
3
5
|
|
|
4
6
|
import { CredentialOfferClient } from '../CredentialOfferClient';
|
|
@@ -211,7 +213,8 @@ describe('Metadataclient with Walt-id should', () => {
|
|
|
211
213
|
});
|
|
212
214
|
});
|
|
213
215
|
|
|
214
|
-
|
|
216
|
+
// Spruce gives back 404's these days, so test is disabled
|
|
217
|
+
describe.skip('Metadataclient with SpruceId should', () => {
|
|
215
218
|
beforeAll(() => {
|
|
216
219
|
nock.cleanAll();
|
|
217
220
|
});
|
|
@@ -68,6 +68,11 @@ describe('OpenID4VCIClient should', () => {
|
|
|
68
68
|
expect(scope?.[0]).toBe('openid');
|
|
69
69
|
});
|
|
70
70
|
it('throw an error if no scope and no authorization_details is provided', async () => {
|
|
71
|
+
nock(MOCK_URL).get(/.*/).reply(200, {});
|
|
72
|
+
nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {});
|
|
73
|
+
nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {});
|
|
74
|
+
// Use a client with issuer only to trigger the error
|
|
75
|
+
client = await OpenID4VCIClient.fromCredentialIssuer({ credentialIssuer: 'https://server.example.com' });
|
|
71
76
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
72
77
|
// @ts-ignore
|
|
73
78
|
client._endpointMetadata?.credentialIssuerMetadata.authorization_endpoint = `${MOCK_URL}v1/auth/authorize`;
|