@sphereon/oid4vci-client 0.6.1-next.8 → 0.7.1-next.10

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.
@@ -1,11 +1,11 @@
1
1
  import {
2
+ AuthorizationServerMetadata,
3
+ AuthorizationServerType,
2
4
  CredentialIssuerMetadata,
3
5
  CredentialOfferPayload,
4
6
  CredentialOfferRequestWithBaseUrl,
5
- EndpointMetadata,
7
+ EndpointMetadataResult,
6
8
  getIssuerFromCredentialOfferPayload,
7
- OAuth2ASMetadata,
8
- Oauth2ASWithOID4VCIMetadata,
9
9
  OpenIDResponse,
10
10
  WellKnownEndpoints,
11
11
  } from '@sphereon/oid4vci-common';
@@ -21,7 +21,7 @@ export class MetadataClient {
21
21
  *
22
22
  * @param credentialOffer
23
23
  */
24
- public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<EndpointMetadata> {
24
+ public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<EndpointMetadataResult> {
25
25
  return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer);
26
26
  }
27
27
 
@@ -29,9 +29,10 @@ export class MetadataClient {
29
29
  * Retrieve the metada using the initiation request obtained from a previous step
30
30
  * @param request
31
31
  */
32
- public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise<EndpointMetadata> {
33
- if (getIssuerFromCredentialOfferPayload(request)) {
34
- return MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string);
32
+ public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise<EndpointMetadataResult> {
33
+ const issuer = getIssuerFromCredentialOfferPayload(request);
34
+ if (issuer) {
35
+ return MetadataClient.retrieveAllMetadata(issuer);
35
36
  }
36
37
  throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present");
37
38
  }
@@ -41,75 +42,115 @@ export class MetadataClient {
41
42
  * @param issuer The issuer URL
42
43
  * @param opts
43
44
  */
44
- public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadata> {
45
- let token_endpoint;
46
- let credential_endpoint;
47
- const response = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuer);
48
- let issuerMetadata = response?.successBody;
49
- if (issuerMetadata) {
50
- debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${issuerMetadata}`);
51
- credential_endpoint = issuerMetadata.credential_endpoint;
52
- token_endpoint = issuerMetadata.token_endpoint;
53
- if (!token_endpoint && issuerMetadata.authorization_server) {
54
- debug(
55
- `Issuer ${issuer} OID4VCI metadata has separate authorization_server ${issuerMetadata.authorization_server} that contains the token endpoint`,
56
- );
57
- // Crossword uses this to separate the AS metadata. We fail when not found, since we now have no way of getting the token endpoint
58
- const response: OpenIDResponse<OAuth2ASMetadata> = await this.retrieveWellknown(
59
- issuerMetadata.authorization_server,
60
- WellKnownEndpoints.OAUTH_AS,
61
- {
62
- errorOnNotFound: true,
63
- },
64
- );
65
- token_endpoint = response.successBody?.token_endpoint;
45
+ public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResult> {
46
+ let token_endpoint: string | undefined;
47
+ let credential_endpoint: string | undefined;
48
+ let authorization_endpoint: string | undefined;
49
+ let authorizationServerType: AuthorizationServerType = 'OID4VCI';
50
+ let authorization_server: string = issuer;
51
+ const oid4vciResponse = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
52
+ let credentialIssuerMetadata = oid4vciResponse?.successBody;
53
+ if (credentialIssuerMetadata) {
54
+ debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${JSON.stringify(credentialIssuerMetadata)}`);
55
+ credential_endpoint = credentialIssuerMetadata.credential_endpoint;
56
+ if (credentialIssuerMetadata.token_endpoint) {
57
+ token_endpoint = credentialIssuerMetadata.token_endpoint;
58
+ }
59
+ if (credentialIssuerMetadata.authorization_server) {
60
+ authorization_server = credentialIssuerMetadata.authorization_server;
66
61
  }
62
+ if (credentialIssuerMetadata.authorization_endpoint) {
63
+ authorization_endpoint = credentialIssuerMetadata.authorization_endpoint;
64
+ }
65
+ }
66
+ // No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first
67
+ let response: OpenIDResponse<AuthorizationServerMetadata> = await MetadataClient.retrieveWellknown(
68
+ authorization_server,
69
+ WellKnownEndpoints.OPENID_CONFIGURATION,
70
+ {
71
+ errorOnNotFound: false,
72
+ },
73
+ );
74
+ let authMetadata = response.successBody;
75
+ if (authMetadata) {
76
+ debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`);
77
+ authorizationServerType = 'OIDC';
67
78
  } else {
68
- // No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OpenID IDP. Let's start with OIDC first
69
- let response: OpenIDResponse<Oauth2ASWithOID4VCIMetadata> = await MetadataClient.retrieveWellknown(
70
- issuer,
71
- WellKnownEndpoints.OPENID_CONFIGURATION,
72
- {
73
- errorOnNotFound: false,
74
- },
75
- );
76
- let asConfig = response.successBody;
77
- if (asConfig) {
78
- debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`);
79
- } else {
80
- // Now oAuth2
81
- response = await MetadataClient.retrieveWellknown(issuer, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
82
- asConfig = response.successBody;
79
+ // Now let's do OAuth2
80
+ response = await MetadataClient.retrieveWellknown(authorization_server, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
81
+ authMetadata = response.successBody;
82
+ }
83
+ if (!authMetadata) {
84
+ // We will always throw an error, no matter whether the user provided the option not to, because this is bad.
85
+ if (issuer !== authorization_server) {
86
+ throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_server}, but that server did not provide metadata`);
87
+ }
88
+ } else {
89
+ if (!authorizationServerType) {
90
+ authorizationServerType = 'OAuth 2.0';
83
91
  }
84
- if (asConfig) {
85
- debug(`Issuer ${issuer} has oAuth2 Server metadata in well-known location`);
86
- issuerMetadata = asConfig;
87
- credential_endpoint = issuerMetadata.credential_endpoint;
88
- token_endpoint = issuerMetadata.token_endpoint;
92
+ debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`);
93
+ if (!authMetadata.authorization_endpoint) {
94
+ throw Error(`Authorization Sever ${authorization_server} did not provide an authorization_endpoint`);
95
+ } else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) {
96
+ throw Error(
97
+ `Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`,
98
+ );
99
+ }
100
+ authorization_endpoint = authMetadata.authorization_endpoint;
101
+ if (!authMetadata.token_endpoint) {
102
+ throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`);
103
+ } else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
104
+ throw Error(
105
+ `Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,
106
+ );
107
+ }
108
+ token_endpoint = authMetadata.token_endpoint;
109
+ if (authMetadata.credential_endpoint) {
110
+ if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) {
111
+ debug(
112
+ `Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.token_endpoint}). Will use the issuer value`,
113
+ );
114
+ } else {
115
+ credential_endpoint = authMetadata.credential_endpoint;
116
+ }
89
117
  }
90
118
  }
119
+
120
+ if (!authorization_endpoint) {
121
+ debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`);
122
+ }
91
123
  if (!token_endpoint) {
92
124
  debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`);
93
125
  if (opts?.errorOnNotFound) {
94
- throw new Error(`Could not deduce the token endpoint for ${issuer}`);
126
+ throw Error(`Could not deduce the token_endpoint for ${issuer}`);
95
127
  } else {
96
- token_endpoint = `${issuer}${issuer.endsWith('/') ? '' : '/'}token`;
128
+ token_endpoint = `${issuer}${issuer.endsWith('/') ? 'token' : '/token'}`;
97
129
  }
98
130
  }
99
131
  if (!credential_endpoint) {
100
132
  debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`);
101
133
  if (opts?.errorOnNotFound) {
102
- throw new Error(`Could not deduce the credential endpoint for ${issuer}`);
134
+ throw Error(`Could not deduce the credential endpoint for ${issuer}`);
103
135
  } else {
104
- credential_endpoint = `${issuer}${issuer.endsWith('/') ? '' : '/'}credential`;
136
+ credential_endpoint = `${issuer}${issuer.endsWith('/') ? 'credential' : '/credential'}`;
105
137
  }
106
138
  }
139
+
140
+ if (!credentialIssuerMetadata && authMetadata) {
141
+ // Apparently everything worked out and the issuer is exposing everything in oAuth2/OIDC well-knowns. Spec is vague about this situation, but we can support it
142
+ credentialIssuerMetadata = authMetadata as CredentialIssuerMetadata;
143
+ }
107
144
  debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`);
108
145
  return {
109
146
  issuer,
110
147
  token_endpoint,
111
148
  credential_endpoint,
112
- issuerMetadata,
149
+ authorization_server,
150
+ authorization_endpoint,
151
+ authorizationServerType,
152
+ credentialIssuerMetadata: credentialIssuerMetadata,
153
+ authorizationServerMetadata: authMetadata,
113
154
  };
114
155
  }
115
156
 
@@ -118,9 +159,15 @@ export class MetadataClient {
118
159
  *
119
160
  * @param issuerHost The issuer hostname
120
161
  */
121
- public static async retrieveOpenID4VCIServerMetadata(issuerHost: string): Promise<OpenIDResponse<CredentialIssuerMetadata> | undefined> {
122
- // Since the server metadata endpoint is optional we are not going to throw an error.
123
- return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, { errorOnNotFound: false });
162
+ public static async retrieveOpenID4VCIServerMetadata(
163
+ issuerHost: string,
164
+ opts?: {
165
+ errorOnNotFound?: boolean;
166
+ },
167
+ ): Promise<OpenIDResponse<CredentialIssuerMetadata> | undefined> {
168
+ return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {
169
+ errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound,
170
+ });
124
171
  }
125
172
 
126
173
  /**
@@ -138,9 +185,9 @@ export class MetadataClient {
138
185
  const result: OpenIDResponse<T> = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, {
139
186
  exceptionOnHttpErrorStatus: opts?.errorOnNotFound,
140
187
  });
141
- if (result.origResponse.status === 404) {
188
+ if (result.origResponse.status >= 400) {
142
189
  // We only get here when error on not found is false
143
- debug(`host ${host} with endpoint type ${endpointType} was not found (404)`);
190
+ debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`);
144
191
  }
145
192
  return result;
146
193
  }
@@ -9,6 +9,7 @@ import {
9
9
  CredentialResponse,
10
10
  CredentialSupported,
11
11
  EndpointMetadata,
12
+ EndpointMetadataResult,
12
13
  OID4VCICredentialFormat,
13
14
  OpenId4VCIVersion,
14
15
  OpenIDResponse,
@@ -53,7 +54,7 @@ export class OpenID4VCIClient {
53
54
  private _clientId?: string;
54
55
  private _kid: string | undefined;
55
56
  private _alg: Alg | string | undefined;
56
- private _endpointMetadata: EndpointMetadata | undefined;
57
+ private _endpointMetadata: EndpointMetadataResult | undefined;
57
58
  private _accessTokenResponse: AccessTokenResponse | undefined;
58
59
 
59
60
  private constructor(
@@ -119,9 +120,14 @@ export class OpenID4VCIClient {
119
120
  if (!scope && !authorizationDetails) {
120
121
  throw Error('Please provide a scope or authorization_details');
121
122
  }
122
- // todo: handling this because of the support for v1_0-08
123
- if (this._endpointMetadata && this._endpointMetadata.issuerMetadata && 'authorization_endpoint' in this._endpointMetadata.issuerMetadata) {
124
- this._endpointMetadata.authorization_endpoint = this._endpointMetadata.issuerMetadata.authorization_endpoint as string;
123
+ // todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
124
+ // handling this because of the support for v1_0-08
125
+ if (
126
+ this._endpointMetadata &&
127
+ this._endpointMetadata.credentialIssuerMetadata &&
128
+ 'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
129
+ ) {
130
+ this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
125
131
  }
126
132
  if (!this._endpointMetadata?.authorization_endpoint) {
127
133
  throw Error('Server metadata does not contain authorization endpoint');
@@ -169,13 +175,13 @@ export class OpenID4VCIClient {
169
175
  // What happens if it doesn't ???
170
176
  // let parEndpoint: string
171
177
  if (
172
- !this._endpointMetadata?.issuerMetadata ||
173
- !('pushed_authorization_request_endpoint' in this._endpointMetadata.issuerMetadata) ||
174
- typeof this._endpointMetadata.issuerMetadata.pushed_authorization_request_endpoint !== 'string'
178
+ !this._endpointMetadata?.credentialIssuerMetadata ||
179
+ !('pushed_authorization_request_endpoint' in this._endpointMetadata.credentialIssuerMetadata) ||
180
+ typeof this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint !== 'string'
175
181
  ) {
176
182
  throw Error('Server metadata does not contain pushed authorization request endpoint');
177
183
  }
178
- const parEndpoint: string = this._endpointMetadata.issuerMetadata.pushed_authorization_request_endpoint;
184
+ const parEndpoint: string = this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint;
179
185
 
180
186
  // add 'openid' scope if not present
181
187
  if (scope && !scope.includes('openid')) {
@@ -207,7 +213,10 @@ export class OpenID4VCIClient {
207
213
  }
208
214
 
209
215
  private handleLocations(authorizationDetails: AuthDetails) {
210
- if (authorizationDetails && (this.endpointMetadata.issuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)) {
216
+ if (
217
+ authorizationDetails &&
218
+ (this.endpointMetadata.credentialIssuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)
219
+ ) {
211
220
  if (authorizationDetails.locations) {
212
221
  if (Array.isArray(authorizationDetails.locations)) {
213
222
  (authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
@@ -293,8 +302,8 @@ export class OpenID4VCIClient {
293
302
  metadata: this.endpointMetadata,
294
303
  });
295
304
  requestBuilder.withTokenFromResponse(this.accessTokenResponse);
296
- if (this.endpointMetadata?.issuerMetadata) {
297
- const metadata = this.endpointMetadata.issuerMetadata;
305
+ if (this.endpointMetadata?.credentialIssuerMetadata) {
306
+ const metadata = this.endpointMetadata.credentialIssuerMetadata;
298
307
  const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
299
308
  if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
300
309
  for (const type of types) {
@@ -357,79 +366,29 @@ export class OpenID4VCIClient {
357
366
  return response.successBody;
358
367
  }
359
368
 
360
- getCredentialsSupported(restrictToInitiationTypes: boolean, supportedType?: string): CredentialSupported[] {
369
+ getCredentialsSupported(
370
+ restrictToInitiationTypes: boolean,
371
+ format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[],
372
+ ): CredentialSupported[] {
361
373
  return getSupportedCredentials({
362
- issuerMetadata: this.endpointMetadata.issuerMetadata,
374
+ issuerMetadata: this.endpointMetadata.credentialIssuerMetadata,
363
375
  version: this.version(),
364
- supportedType,
365
- credentialTypes: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
376
+ format: format,
377
+ types: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
366
378
  });
367
- /*//FIXME: delegate to getCredentialsSupported from IssuerMetadataUtils
368
- let credentialsSupported = this.endpointMetadata?.issuerMetadata?.credentials_supported
369
-
370
- if (this.version() === OpenId4VCIVersion.VER_1_0_08 || typeof credentialsSupported === 'object') {
371
- const issuerMetadata = this.endpointMetadata.issuerMetadata as IssuerMetadataV1_0_08
372
- const v8CredentialsSupported = issuerMetadata.credentials_supported
373
- credentialsSupported = []
374
- credentialsSupported = Object.entries(v8CredentialsSupported).map((key, value) => )
375
-
376
- }
377
-
378
-
379
- if (!credentialsSupported) {
380
- return []
381
- } else if (!restrictToInitiationTypes) {
382
- return credentialsSupported
383
- }
384
-
385
-
386
-
387
- /!**
388
- * the following (not array part is a legacy code from version 1_0-08 which jff implementors used)
389
- *!/
390
- if (!Array.isArray(credentialsSupported)) {
391
- const credentialsSupportedV8: CredentialSupportedV1_0_08 = credentialsSupported as CredentialSupportedV1_0_08;
392
- const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes();
393
- const supported: IssuerCredentialSubject = {};
394
- for (const [key, value] of Object.entries(credentialsSupportedV8)) {
395
- if (initiationTypes.includes(key)) {
396
- supported[key] = value;
397
- }
398
- }
399
- // todo: fix this later. we're returning CredentialSupportedV1_0_08 as a list of CredentialSupported (for v09 onward)
400
- return supported as unknown as CredentialSupported[];
401
- }
402
- const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes()
403
- const credentialSupportedOverlap: CredentialSupported[] = []
404
- for (const supported of credentialsSupported) {
405
- const supportedTypeOverlap: string[] = []
406
- for (const type of supported.types) {
407
- initiationTypes.includes(type)
408
- supportedTypeOverlap.push(type)
409
- }
410
- if (supportedTypeOverlap.length > 0) {
411
- credentialSupportedOverlap.push({
412
- ...supported,
413
- types: supportedTypeOverlap
414
- })
415
- }
416
- }
417
- return credentialSupportedOverlap as CredentialSupported[]*/
418
379
  }
419
380
 
420
- getCredentialMetadata(type: string): CredentialSupported[] {
421
- return this.getCredentialsSupported(false, type);
422
- }
423
-
424
- // todo https://sphereon.atlassian.net/browse/VDX-184
425
- getCredentialTypes(): string[] {
381
+ getCredentialTypes(): string[][] {
426
382
  if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
427
- return typeof (this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type === 'string'
428
- ? [(this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string]
429
- : ((this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string[]);
383
+ const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
384
+ const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
385
+ const result: string[][] = [];
386
+ result[0] = types;
387
+ return result;
430
388
  } else {
431
- // FIXME: this for sure isn't correct. It would also include VerifiableCredential. The whole call to this getCredentialsTypes should be changed to begin with
432
- return this.credentialOffer.credential_offer.credentials.flatMap((c) => (typeof c === 'string' ? c : c.types));
389
+ return this.credentialOffer.credential_offer.credentials.map((c, index) => {
390
+ return typeof c === 'string' ? [c] : c.types;
391
+ });
433
392
  }
434
393
  }
435
394
 
@@ -449,7 +408,7 @@ export class OpenID4VCIClient {
449
408
  return this.credentialOffer.version;
450
409
  }
451
410
 
452
- public get endpointMetadata(): EndpointMetadata {
411
+ public get endpointMetadata(): EndpointMetadataResult {
453
412
  this.assertServerMetadata();
454
413
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
455
414
  return this._endpointMetadata!;
@@ -1,4 +1,11 @@
1
- import { AccessTokenRequest, AccessTokenRequestOpts, AccessTokenResponse, GrantTypes, OpenIDResponse } from '@sphereon/oid4vci-common';
1
+ import {
2
+ AccessTokenRequest,
3
+ AccessTokenRequestOpts,
4
+ AccessTokenResponse,
5
+ GrantTypes,
6
+ OpenIDResponse,
7
+ WellKnownEndpoints,
8
+ } from '@sphereon/oid4vci-common';
2
9
  import nock from 'nock';
3
10
 
4
11
  import { AccessTokenClient } from '../AccessTokenClient';
@@ -11,6 +18,8 @@ const MOCK_URL = 'https://sphereonjunit20221013.com/';
11
18
  describe('AccessTokenClient should', () => {
12
19
  beforeEach(() => {
13
20
  nock.cleanAll();
21
+ nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {});
22
+ nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {});
14
23
  });
15
24
 
16
25
  afterEach(() => {
@@ -6,6 +6,7 @@ import {
6
6
  Jwt,
7
7
  OpenId4VCIVersion,
8
8
  ProofOfPossession,
9
+ WellKnownEndpoints,
9
10
  } from '@sphereon/oid4vci-common';
10
11
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11
12
  // @ts-ignore
@@ -55,6 +56,7 @@ describe('OID4VCI-Client should', () => {
55
56
  function succeedWithAFullFlowWithClientSetup() {
56
57
  nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA));
57
58
  nock(IDENTIPROOF_AS_URL).get('/.well-known/oauth-authorization-server').reply(200, JSON.stringify(IDENTIPROOF_AS_METADATA));
59
+ nock(IDENTIPROOF_AS_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {});
58
60
  nock(IDENTIPROOF_AS_URL)
59
61
  .post(/oauth2\/token.*/)
60
62
  .reply(200, JSON.stringify(mockedAccessTokenResponse));
@@ -1,3 +1,5 @@
1
+ import { OpenId4VCIVersion } from '@sphereon/oid4vci-common';
2
+
1
3
  import { CredentialOfferClient } from '../CredentialOfferClient';
2
4
 
3
5
  import { INITIATION_TEST, INITIATION_TEST_HTTPS_URI, INITIATION_TEST_URI } from './MetadataMocks';
@@ -45,4 +47,15 @@ describe('Issuance Initiation', () => {
45
47
  const issuanceInitiationURI = INITIATION_TEST_HTTPS_URI.replace('?', '');
46
48
  await expect(async () => CredentialOfferClient.fromURI(issuanceInitiationURI)).rejects.toThrowError('Invalid Credential Offer Request');
47
49
  });
50
+
51
+ it('Should return Credential Offer', async () => {
52
+ const client = await CredentialOfferClient.fromURI(
53
+ 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
54
+ );
55
+ expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11);
56
+ expect(client.baseUrl).toEqual('openid-credential-offer://');
57
+ expect(client.scheme).toEqual('openid-credential-offer');
58
+ expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io');
59
+ expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X');
60
+ });
48
61
  });
@@ -0,0 +1,106 @@
1
+ import { Alg, AuthzFlowType, Jwt } from '@sphereon/oid4vci-common';
2
+ import { CredentialMapper } from '@sphereon/ssi-types';
3
+ import { fetch } from 'cross-fetch';
4
+ import { importJWK, JWK, SignJWT } from 'jose';
5
+
6
+ import { OpenID4VCIClient } from '..';
7
+
8
+ export const UNIT_TEST_TIMEOUT = 30000;
9
+
10
+ const ISSUER_URL = 'https://launchpad.vii.electron.mattrlabs.io';
11
+
12
+ const jwk: JWK = {
13
+ crv: 'Ed25519',
14
+ d: 'kTRm0aONHYwNPA-w_DtjMHUIWjE3K70qgCIhWojZ0eU',
15
+ x: 'NeA0d8sp86xRh3DczU4m5wPNIbl0HCSwOBcMN3sNmdk',
16
+ kty: 'OKP',
17
+ };
18
+
19
+ // pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
20
+ // priv hex: 913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5
21
+ const did = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
22
+ const kid = `${did}#z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
23
+ describe('OID4VCI-Client using Mattr issuer should', () => {
24
+ async function test(format: 'ldp_vc' | 'jwt_vc_json') {
25
+ const offer = await getCredentialOffer(format);
26
+ const client = await OpenID4VCIClient.fromURI({
27
+ uri: offer.offerUrl,
28
+ flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW,
29
+ kid,
30
+ alg: Alg.EdDSA,
31
+ });
32
+ expect(client.flowType).toEqual(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW);
33
+ expect(client.credentialOffer).toBeDefined();
34
+ expect(client.endpointMetadata).toBeDefined();
35
+ expect(client.getCredentialEndpoint()).toEqual(`${ISSUER_URL}/oidc/v1/auth/credential`);
36
+ expect(client.getAccessTokenEndpoint()).toEqual('https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token');
37
+
38
+ const accessToken = await client.acquireAccessToken();
39
+ console.log(accessToken);
40
+ expect(accessToken).toMatchObject({
41
+ expires_in: 3600,
42
+ scope: 'OpenBadgeCredential',
43
+ token_type: 'Bearer',
44
+ });
45
+
46
+ const credentialResponse = await client.acquireCredentials({
47
+ credentialTypes: 'OpenBadgeCredential',
48
+ format,
49
+ proofCallbacks: {
50
+ signCallback: proofOfPossessionCallbackFunction,
51
+ },
52
+ });
53
+ expect(credentialResponse.credential).toBeDefined();
54
+ const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!);
55
+ expect(format.startsWith(wrappedVC.format)).toEqual(true);
56
+ }
57
+
58
+ it(
59
+ 'succeed in a full flow with the client using OpenID4VCI version 11 and ldp_vc',
60
+ async () => {
61
+ await test('ldp_vc');
62
+ },
63
+ UNIT_TEST_TIMEOUT,
64
+ );
65
+ it(
66
+ 'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
67
+ async () => {
68
+ await test('jwt_vc_json');
69
+ },
70
+ UNIT_TEST_TIMEOUT,
71
+ );
72
+ });
73
+
74
+ interface CreateCredentialOfferResponse {
75
+ id: string;
76
+ offerUrl: string;
77
+ }
78
+
79
+ async function getCredentialOffer(format: 'ldp_vc' | 'jwt_vc_json'): Promise<CreateCredentialOfferResponse> {
80
+ const credentialOffer = await fetch('https://launchpad.mattrlabs.com/api/credential-offer', {
81
+ method: 'post',
82
+ headers: {
83
+ Accept: 'application/json',
84
+ 'Content-Type': 'application/json',
85
+ },
86
+
87
+ //make sure to serialize your JSON body
88
+ body: JSON.stringify({
89
+ format,
90
+ type: 'OpenBadgeCredential',
91
+ userId: '622a9f65-21c0-4c0b-9a6a-f7574c2a1549',
92
+ userAuthenticationRequired: false,
93
+ }),
94
+ });
95
+
96
+ return (await credentialOffer.json()) as CreateCredentialOfferResponse;
97
+ }
98
+
99
+ async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
100
+ const importedJwk = await importJWK(jwk, 'EdDSA');
101
+ return await new SignJWT({ ...args.payload })
102
+ .setProtectedHeader({ ...args.header })
103
+ .setIssuedAt()
104
+ .setExpirationTime('2h')
105
+ .sign(importedJwk);
106
+ }