@sphereon/oid4vci-client 0.8.2-next.12 → 0.8.2-next.26
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/dist/AccessTokenClient.d.ts.map +1 -1
- package/dist/AccessTokenClient.js +8 -8
- package/dist/AccessTokenClient.js.map +1 -1
- package/dist/CredentialRequestClient.d.ts.map +1 -1
- package/dist/CredentialRequestClient.js +4 -5
- package/dist/CredentialRequestClient.js.map +1 -1
- package/dist/CredentialRequestClientBuilder.d.ts +6 -0
- package/dist/CredentialRequestClientBuilder.d.ts.map +1 -1
- package/dist/CredentialRequestClientBuilder.js +9 -0
- package/dist/CredentialRequestClientBuilder.js.map +1 -1
- package/dist/OpenID4VCIClient.d.ts +15 -5
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +99 -38
- 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 +10 -8
- package/lib/CredentialRequestClient.ts +4 -5
- package/lib/CredentialRequestClientBuilder.ts +19 -0
- package/lib/OpenID4VCIClient.ts +130 -38
- package/lib/ProofOfPossessionBuilder.ts +8 -0
- package/lib/__tests__/CredentialRequestClient.spec.ts +9 -2
- package/lib/__tests__/EBSIE2E.spec.test.ts +138 -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 +1 -1
- package/lib/functions/ProofUtil.ts +18 -4
- package/package.json +6 -4
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, getTypesFromCredentialSupported } 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,6 +355,7 @@ export class OpenID4VCIClient {
|
|
|
281
355
|
proofCallbacks,
|
|
282
356
|
format,
|
|
283
357
|
kid,
|
|
358
|
+
jwk,
|
|
284
359
|
alg,
|
|
285
360
|
jti,
|
|
286
361
|
}: {
|
|
@@ -288,25 +363,34 @@ export class OpenID4VCIClient {
|
|
|
288
363
|
proofCallbacks: ProofOfPossessionCallbacks<any>;
|
|
289
364
|
format?: CredentialFormat | OID4VCICredentialFormat;
|
|
290
365
|
kid?: string;
|
|
366
|
+
jwk?: JWK;
|
|
291
367
|
alg?: Alg | string;
|
|
292
368
|
jti?: string;
|
|
293
369
|
}): Promise<CredentialResponse> {
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
if (kid) {
|
|
298
|
-
this._kid = kid;
|
|
370
|
+
if ([jwk, kid].filter((v) => v !== undefined).length > 1) {
|
|
371
|
+
throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
|
|
299
372
|
}
|
|
300
373
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
374
|
+
if (alg) this._alg = alg;
|
|
375
|
+
if (jwk) this._jwk = jwk;
|
|
376
|
+
if (kid) this._kid = kid;
|
|
377
|
+
|
|
378
|
+
const requestBuilder = this.credentialOffer
|
|
379
|
+
? CredentialRequestClientBuilder.fromCredentialOffer({
|
|
380
|
+
credentialOffer: this.credentialOffer,
|
|
381
|
+
metadata: this.endpointMetadata,
|
|
382
|
+
})
|
|
383
|
+
: CredentialRequestClientBuilder.fromCredentialIssuer({
|
|
384
|
+
credentialIssuer: this.getIssuer(),
|
|
385
|
+
credentialTypes,
|
|
386
|
+
metadata: this.endpointMetadata,
|
|
387
|
+
version: this.version(),
|
|
388
|
+
});
|
|
305
389
|
|
|
306
390
|
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
|
|
307
391
|
if (this.endpointMetadata?.credentialIssuerMetadata) {
|
|
308
392
|
const metadata = this.endpointMetadata.credentialIssuerMetadata;
|
|
309
|
-
const types = Array.isArray(credentialTypes) ? credentialTypes.sort() : [credentialTypes];
|
|
393
|
+
const types = Array.isArray(credentialTypes) ? [...credentialTypes].sort() : [credentialTypes];
|
|
310
394
|
|
|
311
395
|
if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
312
396
|
let typeSupported = false;
|
|
@@ -322,10 +406,11 @@ export class OpenID4VCIClient {
|
|
|
322
406
|
});
|
|
323
407
|
|
|
324
408
|
if (!typeSupported) {
|
|
325
|
-
|
|
409
|
+
console.log(`Not all credential types ${JSON.stringify(credentialTypes)} are present in metadata for ${this.getIssuer()}`);
|
|
410
|
+
// throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
326
411
|
}
|
|
327
412
|
} else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) {
|
|
328
|
-
const credentialsSupported = metadata.credentials_supported
|
|
413
|
+
const credentialsSupported = metadata.credentials_supported;
|
|
329
414
|
if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) {
|
|
330
415
|
throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
331
416
|
}
|
|
@@ -339,8 +424,14 @@ export class OpenID4VCIClient {
|
|
|
339
424
|
version: this.version(),
|
|
340
425
|
})
|
|
341
426
|
.withIssuer(this.getIssuer())
|
|
342
|
-
.withAlg(this.alg)
|
|
343
|
-
|
|
427
|
+
.withAlg(this.alg);
|
|
428
|
+
|
|
429
|
+
if (this._jwk) {
|
|
430
|
+
proofBuilder.withJWK(this._jwk);
|
|
431
|
+
}
|
|
432
|
+
if (this._kid) {
|
|
433
|
+
proofBuilder.withKid(this._kid);
|
|
434
|
+
}
|
|
344
435
|
|
|
345
436
|
if (this.clientId) {
|
|
346
437
|
proofBuilder.withClientId(this.clientId);
|
|
@@ -354,7 +445,7 @@ export class OpenID4VCIClient {
|
|
|
354
445
|
format,
|
|
355
446
|
});
|
|
356
447
|
if (response.errorBody) {
|
|
357
|
-
debug(`Credential request error:\r\n${response.errorBody}`);
|
|
448
|
+
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
|
|
358
449
|
throw Error(
|
|
359
450
|
`Retrieving a credential from ${this._endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
360
451
|
response.origResponse.status
|
|
@@ -387,7 +478,9 @@ export class OpenID4VCIClient {
|
|
|
387
478
|
}
|
|
388
479
|
|
|
389
480
|
getCredentialOfferTypes(): string[][] {
|
|
390
|
-
if (this.credentialOffer
|
|
481
|
+
if (!this.credentialOffer) {
|
|
482
|
+
return [];
|
|
483
|
+
} else if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
391
484
|
const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
|
|
392
485
|
const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
|
|
393
486
|
const result: string[][] = [];
|
|
@@ -399,8 +492,8 @@ export class OpenID4VCIClient {
|
|
|
399
492
|
return [c];
|
|
400
493
|
} else if ('types' in c) {
|
|
401
494
|
return c.types;
|
|
402
|
-
} else if ('vct' in c
|
|
403
|
-
return [c.
|
|
495
|
+
} else if ('vct' in c) {
|
|
496
|
+
return [c.vct];
|
|
404
497
|
} else {
|
|
405
498
|
return c.credential_definition.types;
|
|
406
499
|
}
|
|
@@ -409,15 +502,15 @@ export class OpenID4VCIClient {
|
|
|
409
502
|
}
|
|
410
503
|
|
|
411
504
|
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
412
|
-
return this.credentialOffer.
|
|
505
|
+
return this.credentialOffer?.supportedFlows ?? [AuthzFlowType.AUTHORIZATION_CODE_FLOW];
|
|
413
506
|
}
|
|
414
507
|
|
|
415
|
-
get credentialOffer(): CredentialOfferRequestWithBaseUrl {
|
|
508
|
+
get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
|
|
416
509
|
return this._credentialOffer;
|
|
417
510
|
}
|
|
418
511
|
|
|
419
512
|
public version(): OpenId4VCIVersion {
|
|
420
|
-
return this.credentialOffer.
|
|
513
|
+
return this.credentialOffer?.version ?? OpenId4VCIVersion.VER_1_0_11;
|
|
421
514
|
}
|
|
422
515
|
|
|
423
516
|
public get endpointMetadata(): EndpointMetadataResult {
|
|
@@ -443,9 +536,6 @@ export class OpenID4VCIClient {
|
|
|
443
536
|
}
|
|
444
537
|
|
|
445
538
|
get clientId(): string | undefined {
|
|
446
|
-
/*if (!this._clientId) {
|
|
447
|
-
throw Error('No client id present');
|
|
448
|
-
}*/
|
|
449
539
|
return this._clientId;
|
|
450
540
|
}
|
|
451
541
|
|
|
@@ -457,7 +547,7 @@ export class OpenID4VCIClient {
|
|
|
457
547
|
|
|
458
548
|
public getIssuer(): string {
|
|
459
549
|
this.assertIssuerData();
|
|
460
|
-
return this.
|
|
550
|
+
return this._credentialIssuer!;
|
|
461
551
|
}
|
|
462
552
|
|
|
463
553
|
public getAccessTokenEndpoint(): string {
|
|
@@ -473,8 +563,10 @@ export class OpenID4VCIClient {
|
|
|
473
563
|
}
|
|
474
564
|
|
|
475
565
|
private assertIssuerData(): void {
|
|
476
|
-
if (!this._credentialOffer) {
|
|
566
|
+
if (!this._credentialOffer && this.issuerSupportedFlowTypes().includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
|
|
477
567
|
throw Error(`No issuance initiation or credential offer present`);
|
|
568
|
+
} else if (!this._credentialIssuer) {
|
|
569
|
+
throw Error(`No credential issuer value present`);
|
|
478
570
|
}
|
|
479
571
|
}
|
|
480
572
|
|
|
@@ -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,
|
|
@@ -128,7 +128,7 @@ describe('Credential Request Client ', () => {
|
|
|
128
128
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
129
129
|
});
|
|
130
130
|
expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy();
|
|
131
|
-
expect(credentialRequest.format).toEqual('
|
|
131
|
+
expect(credentialRequest.format).toEqual('jwt_vc');
|
|
132
132
|
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
|
|
133
133
|
expect(result?.successBody?.credential).toEqual(mockedVC);
|
|
134
134
|
});
|
|
@@ -157,8 +157,15 @@ describe('Credential Request Client ', () => {
|
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
describe('Credential Request Client with Walt.id ', () => {
|
|
160
|
-
|
|
160
|
+
beforeAll(() => {
|
|
161
|
+
nock.cleanAll();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
161
165
|
nock.cleanAll();
|
|
166
|
+
});
|
|
167
|
+
it('should have correct metadata endpoints', async function () {
|
|
168
|
+
// nock.cleanAll();
|
|
162
169
|
const WALT_IRR_URI =
|
|
163
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';
|
|
164
171
|
const credentialOffer = await CredentialOfferClient.fromURI(WALT_IRR_URI);
|
|
@@ -0,0 +1,138 @@
|
|
|
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' | '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
|
+
});
|
|
97
|
+
console.log(JSON.stringify(credentialResponse, null, 2));
|
|
98
|
+
expect(credentialResponse.credential).toBeDefined();
|
|
99
|
+
const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!);
|
|
100
|
+
expect(format.startsWith(wrappedVC.format)).toEqual(true);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Current conformance tests is not stable as changes are being applied it seems
|
|
104
|
+
|
|
105
|
+
it.skip(
|
|
106
|
+
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
|
|
107
|
+
async () => {
|
|
108
|
+
await test('CTWalletCrossPreAuthorisedInTime');
|
|
109
|
+
// await test('CTWalletCrossAuthorisedInTime');
|
|
110
|
+
},
|
|
111
|
+
UNIT_TEST_TIMEOUT,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
async function getCredentialOffer(credentialType: 'CTWalletCrossPreAuthorisedInTime' | 'CTWalletCrossAuthorisedInTime'): Promise<string> {
|
|
116
|
+
const credentialOffer = await fetch(
|
|
117
|
+
`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`,
|
|
118
|
+
{
|
|
119
|
+
method: 'GET',
|
|
120
|
+
headers: {
|
|
121
|
+
Accept: 'application/json',
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
return await credentialOffer.text();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
|
|
131
|
+
const importedJwk = await importJWK(jwk);
|
|
132
|
+
return await new SignJWT({ ...args.payload })
|
|
133
|
+
.setProtectedHeader({ ...args.header, kid: kid! })
|
|
134
|
+
.setIssuer(DID)
|
|
135
|
+
.setIssuedAt()
|
|
136
|
+
.setExpirationTime('2m')
|
|
137
|
+
.sign(importedJwk);
|
|
138
|
+
}
|
|
@@ -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`;
|