@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.
Files changed (45) hide show
  1. package/README.md +9 -8
  2. package/dist/AccessTokenClient.d.ts.map +1 -1
  3. package/dist/AccessTokenClient.js +9 -9
  4. package/dist/AccessTokenClient.js.map +1 -1
  5. package/dist/AuthorizationDetailsBuilder.d.ts.map +1 -1
  6. package/dist/AuthorizationDetailsBuilder.js.map +1 -1
  7. package/dist/CredentialOfferClient.js.map +1 -1
  8. package/dist/CredentialRequestClient.d.ts +13 -0
  9. package/dist/CredentialRequestClient.d.ts.map +1 -1
  10. package/dist/CredentialRequestClient.js +87 -41
  11. package/dist/CredentialRequestClient.js.map +1 -1
  12. package/dist/CredentialRequestClientBuilder.d.ts +19 -7
  13. package/dist/CredentialRequestClientBuilder.d.ts.map +1 -1
  14. package/dist/CredentialRequestClientBuilder.js +31 -1
  15. package/dist/CredentialRequestClientBuilder.js.map +1 -1
  16. package/dist/MetadataClient.d.ts.map +1 -1
  17. package/dist/MetadataClient.js +12 -1
  18. package/dist/MetadataClient.js.map +1 -1
  19. package/dist/OpenID4VCIClient.d.ts +19 -5
  20. package/dist/OpenID4VCIClient.d.ts.map +1 -1
  21. package/dist/OpenID4VCIClient.js +119 -41
  22. package/dist/OpenID4VCIClient.js.map +1 -1
  23. package/dist/ProofOfPossessionBuilder.d.ts +3 -1
  24. package/dist/ProofOfPossessionBuilder.d.ts.map +1 -1
  25. package/dist/ProofOfPossessionBuilder.js +5 -0
  26. package/dist/ProofOfPossessionBuilder.js.map +1 -1
  27. package/dist/functions/ProofUtil.d.ts +2 -1
  28. package/dist/functions/ProofUtil.d.ts.map +1 -1
  29. package/dist/functions/ProofUtil.js +6 -4
  30. package/dist/functions/ProofUtil.js.map +1 -1
  31. package/lib/AccessTokenClient.ts +11 -9
  32. package/lib/AuthorizationDetailsBuilder.ts +2 -2
  33. package/lib/CredentialRequestClient.ts +105 -43
  34. package/lib/CredentialRequestClientBuilder.ts +53 -8
  35. package/lib/MetadataClient.ts +13 -1
  36. package/lib/OpenID4VCIClient.ts +152 -42
  37. package/lib/ProofOfPossessionBuilder.ts +8 -0
  38. package/lib/__tests__/CredentialRequestClient.spec.ts +21 -9
  39. package/lib/__tests__/EBSIE2E.spec.test.ts +143 -0
  40. package/lib/__tests__/MetadataClient.spec.ts +4 -1
  41. package/lib/__tests__/OpenID4VCIClient.spec.ts +5 -0
  42. package/lib/__tests__/SdJwt.spec.ts +161 -0
  43. package/lib/__tests__/data/VciDataFixtures.ts +14 -13
  44. package/lib/functions/ProofUtil.ts +18 -4
  45. package/package.json +8 -6
@@ -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: 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: CredentialOfferRequestWithBaseUrl;
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(credentialOffer: CredentialOfferRequestWithBaseUrl, kid?: string, alg?: Alg | string, clientId?: string) {
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(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), kid, alg, clientId);
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
- this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
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
- throw Error('Please provide a scope or authorization_details');
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.issuerState) {
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.issuerState) {
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 (alg) {
295
- this._alg = alg;
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
- const requestBuilder = CredentialRequestClientBuilder.fromCredentialOffer({
302
- credentialOffer: this.credentialOffer,
303
- metadata: this.endpointMetadata,
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
- if (!supportedCredential.types || supportedCredential.types.length === 0) {
316
- throw Error('types is required in the credentials supported');
317
- }
404
+ const subTypes = getTypesFromCredentialSupported(supportedCredential);
318
405
  if (
319
- supportedCredential.types.sort().every((t, i) => types[i] === t) ||
320
- (types.length === 1 && (types[0] === supportedCredential.id || supportedCredential.types.includes(types[0])))
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
- throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
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 as CredentialSupportedTypeV1_0_08;
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
- .withKid(this.kid);
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.version < OpenId4VCIVersion.VER_1_0_11) {
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
- return typeof c === 'string' ? [c] : c.types;
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.supportedFlows;
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.version;
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._endpointMetadata ? this.endpointMetadata.issuer : this.getIssuer();
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('jwt_vc_json');
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
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
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
- it('should have correct metadata endpoints', async function () {
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: 'jwt_vc_json-ld',
203
+ format: 'jwt_vc',
198
204
  version: OpenId4VCIVersion.VER_1_0_08,
199
205
  });
200
- expect(credentialRequest).toEqual(getMockData('spruce')?.credential.request);
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
- expect(credentialOffer).toEqual(getMockData('mattr')?.credential.request);
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
- expect(credentialOffer).toEqual(getMockData('diwala')?.credential.request);
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
- describe('Metadataclient with SpruceId should', () => {
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`;