@sphereon/oid4vci-client 0.8.2-next.48 → 0.8.2-next.87

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 (47) hide show
  1. package/dist/AccessTokenClient.d.ts +0 -1
  2. package/dist/AccessTokenClient.d.ts.map +1 -1
  3. package/dist/AccessTokenClient.js +2 -9
  4. package/dist/AccessTokenClient.js.map +1 -1
  5. package/dist/AuthorizationCodeClient.d.ts +9 -0
  6. package/dist/AuthorizationCodeClient.d.ts.map +1 -0
  7. package/dist/AuthorizationCodeClient.js +124 -0
  8. package/dist/AuthorizationCodeClient.js.map +1 -0
  9. package/dist/CredentialOfferClient.d.ts.map +1 -1
  10. package/dist/CredentialOfferClient.js +3 -1
  11. package/dist/CredentialOfferClient.js.map +1 -1
  12. package/dist/CredentialRequestClient.d.ts +2 -0
  13. package/dist/CredentialRequestClient.d.ts.map +1 -1
  14. package/dist/CredentialRequestClient.js +9 -7
  15. package/dist/CredentialRequestClient.js.map +1 -1
  16. package/dist/OpenID4VCIClient.d.ts +50 -29
  17. package/dist/OpenID4VCIClient.d.ts.map +1 -1
  18. package/dist/OpenID4VCIClient.js +191 -190
  19. package/dist/OpenID4VCIClient.js.map +1 -1
  20. package/dist/functions/AuthorizationUtil.d.ts +3 -0
  21. package/dist/functions/AuthorizationUtil.d.ts.map +1 -0
  22. package/dist/functions/AuthorizationUtil.js +22 -0
  23. package/dist/functions/AuthorizationUtil.js.map +1 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/types/index.d.ts +1 -0
  29. package/dist/types/index.d.ts.map +1 -0
  30. package/dist/types/index.js +2 -0
  31. package/dist/types/index.js.map +1 -0
  32. package/lib/AccessTokenClient.ts +5 -11
  33. package/lib/AuthorizationCodeClient.ts +151 -0
  34. package/lib/CredentialOfferClient.ts +4 -1
  35. package/lib/CredentialRequestClient.ts +13 -4
  36. package/lib/OpenID4VCIClient.ts +250 -228
  37. package/lib/__tests__/AccessTokenClient.spec.ts +2 -0
  38. package/lib/__tests__/CredentialRequestClient.spec.ts +10 -2
  39. package/lib/__tests__/EBSIE2E.spec.test.ts +8 -6
  40. package/lib/__tests__/OpenID4VCIClient.spec.ts +115 -79
  41. package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +59 -49
  42. package/lib/__tests__/SdJwt.spec.ts +2 -0
  43. package/lib/__tests__/SphereonE2E.spec.test.ts +2 -2
  44. package/lib/functions/AuthorizationUtil.ts +18 -0
  45. package/lib/index.ts +1 -0
  46. package/lib/types/index.ts +0 -0
  47. package/package.json +3 -3
@@ -1,62 +1,57 @@
1
1
  import {
2
2
  AccessTokenResponse,
3
3
  Alg,
4
+ AuthorizationRequestOpts,
5
+ AuthorizationResponse,
4
6
  AuthzFlowType,
5
7
  CodeChallengeMethod,
6
8
  CredentialOfferPayloadV1_0_08,
7
9
  CredentialOfferRequestWithBaseUrl,
8
10
  CredentialResponse,
9
11
  CredentialSupported,
12
+ DefaultURISchemes,
10
13
  EndpointMetadataResult,
14
+ getClientIdFromCredentialOfferPayload,
11
15
  getIssuerFromCredentialOfferPayload,
12
16
  getSupportedCredentials,
13
17
  getTypesFromCredentialSupported,
14
- JsonURIMode,
15
18
  JWK,
16
19
  KID_JWK_X5C_ERROR,
17
20
  OID4VCICredentialFormat,
18
21
  OpenId4VCIVersion,
22
+ PKCEOpts,
19
23
  ProofOfPossessionCallbacks,
20
- PushedAuthorizationResponse,
21
- ResponseType,
24
+ toAuthorizationResponsePayload,
22
25
  } from '@sphereon/oid4vci-common';
23
26
  import { CredentialFormat } from '@sphereon/ssi-types';
24
27
  import Debug from 'debug';
25
28
 
26
29
  import { AccessTokenClient } from './AccessTokenClient';
30
+ import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
27
31
  import { CredentialOfferClient } from './CredentialOfferClient';
28
32
  import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
29
33
  import { MetadataClient } from './MetadataClient';
30
34
  import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
31
- import { convertJsonToURI, formPost } from './functions';
35
+ import { generateMissingPKCEOpts } from './functions/AuthorizationUtil';
32
36
 
33
37
  const debug = Debug('sphereon:oid4vci');
34
38
 
35
- interface AuthDetails {
36
- type: 'openid_credential' | string;
37
- locations?: string | string[];
38
- format: CredentialFormat | CredentialFormat[];
39
-
40
- [s: string]: unknown;
41
- }
42
-
43
- interface AuthRequestOpts {
44
- codeChallenge: string;
45
- codeChallengeMethod?: CodeChallengeMethod;
46
- authorizationDetails?: AuthDetails | AuthDetails[];
47
- redirectUri: string;
48
- scope?: string;
39
+ export interface OpenID4VCIClientState {
40
+ credentialIssuer: string;
41
+ credentialOffer?: CredentialOfferRequestWithBaseUrl;
42
+ clientId?: string;
43
+ kid?: string;
44
+ jwk?: JWK;
45
+ alg?: Alg | string;
46
+ endpointMetadata?: EndpointMetadataResult;
47
+ accessTokenResponse?: AccessTokenResponse;
48
+ authorizationRequestOpts?: AuthorizationRequestOpts;
49
+ pkce: PKCEOpts;
50
+ authorizationURL?: string;
49
51
  }
50
52
 
51
53
  export class OpenID4VCIClient {
52
- private readonly _credentialOffer?: CredentialOfferRequestWithBaseUrl;
53
- private _credentialIssuer: string;
54
- private _clientId?: string;
55
- private _kid: string | undefined;
56
- private _jwk: JWK | undefined;
57
- private _alg: Alg | string | undefined;
58
- private _endpointMetadata: EndpointMetadataResult | undefined;
59
- private _accessTokenResponse: AccessTokenResponse | undefined;
54
+ private readonly _state: OpenID4VCIClientState;
60
55
 
61
56
  private constructor({
62
57
  credentialOffer,
@@ -64,22 +59,50 @@ export class OpenID4VCIClient {
64
59
  kid,
65
60
  alg,
66
61
  credentialIssuer,
62
+ pkce,
63
+ authorizationRequest,
64
+ jwk,
65
+ endpointMetadata,
66
+ accessTokenResponse,
67
+ authorizationRequestOpts,
68
+ authorizationURL,
67
69
  }: {
68
70
  credentialOffer?: CredentialOfferRequestWithBaseUrl;
69
71
  kid?: string;
70
72
  alg?: Alg | string;
71
73
  clientId?: string;
72
74
  credentialIssuer?: string;
75
+ pkce?: PKCEOpts;
76
+ authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
77
+ jwk?: JWK;
78
+ endpointMetadata?: EndpointMetadataResult;
79
+ accessTokenResponse?: AccessTokenResponse;
80
+ authorizationRequestOpts?: AuthorizationRequestOpts;
81
+ authorizationURL?: string;
73
82
  }) {
74
- this._credentialOffer = credentialOffer;
75
83
  const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
76
84
  if (!issuer) {
77
85
  throw Error('No credential issuer supplied or deduced from offer');
78
86
  }
79
- this._credentialIssuer = issuer;
80
- this._kid = kid;
81
- this._alg = alg;
82
- this._clientId = clientId;
87
+ this._state = {
88
+ credentialOffer,
89
+ credentialIssuer: issuer,
90
+ kid,
91
+ alg,
92
+ // TODO: We need to refactor this and always explicitly call createAuthorizationRequestUrl, so we can have a credential selection first and use the kid as a default for the client id
93
+ clientId: clientId ?? (credentialOffer && getClientIdFromCredentialOfferPayload(credentialOffer.credential_offer)) ?? kid?.split('#')[0],
94
+ pkce: { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256, ...pkce },
95
+ authorizationRequestOpts,
96
+ jwk,
97
+ endpointMetadata,
98
+ accessTokenResponse,
99
+ authorizationURL,
100
+ };
101
+ // Running syncAuthorizationRequestOpts later as it is using the state
102
+ if (!this._state.authorizationRequestOpts) {
103
+ this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(authorizationRequest);
104
+ }
105
+ debug(`Authorization req options: ${JSON.stringify(this._state.authorizationRequestOpts, null, 2)}`);
83
106
  }
84
107
 
85
108
  public static async fromCredentialIssuer({
@@ -88,263 +111,197 @@ export class OpenID4VCIClient {
88
111
  retrieveServerMetadata,
89
112
  clientId,
90
113
  credentialIssuer,
114
+ pkce,
115
+ authorizationRequest,
116
+ createAuthorizationRequestURL,
91
117
  }: {
92
118
  credentialIssuer: string;
93
119
  kid?: string;
94
120
  alg?: Alg | string;
95
121
  retrieveServerMetadata?: boolean;
96
122
  clientId?: string;
123
+ createAuthorizationRequestURL?: boolean;
124
+ authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
125
+ pkce?: PKCEOpts;
97
126
  }) {
98
- const client = new OpenID4VCIClient({ kid, alg, clientId, credentialIssuer });
127
+ const client = new OpenID4VCIClient({
128
+ kid,
129
+ alg,
130
+ clientId: clientId ?? authorizationRequest?.clientId,
131
+ credentialIssuer,
132
+ pkce,
133
+ authorizationRequest,
134
+ });
99
135
  if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
100
136
  await client.retrieveServerMetadata();
101
137
  }
138
+ if (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) {
139
+ await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
140
+ }
102
141
  return client;
103
142
  }
104
143
 
144
+ public static async fromState({ state }: { state: OpenID4VCIClientState | string }): Promise<OpenID4VCIClient> {
145
+ const clientState = typeof state === 'string' ? JSON.parse(state) : state;
146
+
147
+ return new OpenID4VCIClient(clientState);
148
+ }
149
+
105
150
  public static async fromURI({
106
151
  uri,
107
152
  kid,
108
153
  alg,
109
154
  retrieveServerMetadata,
110
155
  clientId,
156
+ pkce,
157
+ createAuthorizationRequestURL,
158
+ authorizationRequest,
111
159
  resolveOfferUri,
112
160
  }: {
113
161
  uri: string;
114
162
  kid?: string;
115
163
  alg?: Alg | string;
116
164
  retrieveServerMetadata?: boolean;
165
+ createAuthorizationRequestURL?: boolean;
117
166
  resolveOfferUri?: boolean;
167
+ pkce?: PKCEOpts;
118
168
  clientId?: string;
169
+ authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
119
170
  }): Promise<OpenID4VCIClient> {
171
+ const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri });
120
172
  const client = new OpenID4VCIClient({
121
- credentialOffer: await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }),
173
+ credentialOffer: credentialOfferClient,
122
174
  kid,
123
175
  alg,
124
- clientId,
176
+ clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId,
177
+ pkce,
178
+ authorizationRequest,
125
179
  });
126
180
 
127
181
  if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
128
182
  await client.retrieveServerMetadata();
129
183
  }
130
- return client;
131
- }
132
-
133
- public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
134
- this.assertIssuerData();
135
- if (!this._endpointMetadata) {
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
- }
143
- }
144
- return this.endpointMetadata;
145
- }
146
-
147
- // todo: Unify this method with the par method
148
-
149
- public createAuthorizationRequestUrl({ codeChallengeMethod, codeChallenge, authorizationDetails, redirectUri, scope }: AuthRequestOpts): string {
150
- // Scope and authorization_details can be used in the same authorization request
151
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
152
- if (!scope && !authorizationDetails) {
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
- }
171
- }
172
- // todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
173
- // handling this because of the support for v1_0-08
174
184
  if (
175
- this._endpointMetadata &&
176
- this._endpointMetadata.credentialIssuerMetadata &&
177
- 'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
185
+ credentialOfferClient.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
186
+ (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL)
178
187
  ) {
179
- this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
180
- }
181
- if (!this._endpointMetadata?.authorization_endpoint) {
182
- throw Error('Server metadata does not contain authorization endpoint');
183
- }
184
-
185
- // add 'openid' scope if not present
186
- if (!scope?.includes('openid')) {
187
- scope = ['openid', scope].filter((s) => !!s).join(' ');
188
+ await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
189
+ debug(`Authorization Request URL: ${client._state.authorizationURL}`);
188
190
  }
189
191
 
190
- const queryObj: { [key: string]: string } = {
191
- response_type: ResponseType.AUTH_CODE,
192
- code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
193
- code_challenge: codeChallenge,
194
- authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
195
- redirect_uri: redirectUri,
196
- scope: scope,
197
- };
198
-
199
- if (this.clientId) {
200
- queryObj['client_id'] = this.clientId;
201
- }
202
-
203
- if (this.credentialOffer?.issuerState) {
204
- queryObj['issuer_state'] = this.credentialOffer.issuerState;
205
- }
206
-
207
- return convertJsonToURI(queryObj, {
208
- baseUrl: this._endpointMetadata.authorization_endpoint,
209
- uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
210
- mode: JsonURIMode.X_FORM_WWW_URLENCODED,
211
- // We do not add the version here, as this always needs to be form encoded
212
- });
192
+ return client;
213
193
  }
214
194
 
215
- // todo: Unify this method with the create auth request url method
216
- public async acquirePushedAuthorizationRequestURI({
217
- codeChallengeMethod,
218
- codeChallenge,
219
- authorizationDetails,
220
- redirectUri,
221
- scope,
222
- }: AuthRequestOpts): Promise<string> {
223
- // Scope and authorization_details can be used in the same authorization request
224
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
225
- if (!scope && !authorizationDetails) {
226
- throw Error('Please provide a scope or authorization_details');
227
- }
228
-
229
- // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document
230
- // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow.
231
- // What happens if it doesn't ???
232
- // let parEndpoint: string
233
- if (
234
- !this._endpointMetadata?.credentialIssuerMetadata ||
235
- !('pushed_authorization_request_endpoint' in this._endpointMetadata.credentialIssuerMetadata) ||
236
- typeof this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint !== 'string'
237
- ) {
238
- throw Error('Server metadata does not contain pushed authorization request endpoint');
239
- }
240
- const parEndpoint: string = this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint;
241
-
242
- // add 'openid' scope if not present
243
- if (!scope?.includes('openid')) {
244
- scope = ['openid', scope].filter((s) => !!s).join(' ');
245
- }
246
-
247
- const queryObj: { [key: string]: string } = {
248
- response_type: ResponseType.AUTH_CODE,
249
- code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
250
- code_challenge: codeChallenge,
251
- authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
252
- redirect_uri: redirectUri,
253
- scope: scope,
254
- };
255
-
256
- if (this.clientId) {
257
- queryObj['client_id'] = this.clientId;
258
- }
195
+ /**
196
+ * Allows you to create an Authorization Request URL when using an Authorization Code flow. This URL needs to be accessed using the front channel (browser)
197
+ *
198
+ * The Identity provider would present a login screen typically; after you authenticated, it would redirect to the provided redirectUri; which can be same device or cross-device
199
+ * @param opts
200
+ */
201
+ public async createAuthorizationRequestUrl(opts?: { authorizationRequest?: AuthorizationRequestOpts; pkce?: PKCEOpts }): Promise<string> {
202
+ if (!this._state.authorizationURL) {
203
+ this.calculatePKCEOpts(opts?.pkce);
204
+ this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(opts?.authorizationRequest);
205
+ if (!this._state.authorizationRequestOpts) {
206
+ throw Error(`No Authorization Request options present or provided in this call`);
207
+ }
259
208
 
260
- if (this.credentialOffer?.issuerState) {
261
- queryObj['issuer_state'] = this.credentialOffer.issuerState;
209
+ // todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
210
+ // handling this because of the support for v1_0-08
211
+ if (
212
+ this._state.endpointMetadata?.credentialIssuerMetadata &&
213
+ 'authorization_endpoint' in this._state.endpointMetadata.credentialIssuerMetadata
214
+ ) {
215
+ this._state.endpointMetadata.authorization_endpoint = this._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
216
+ }
217
+ this._state.authorizationURL = await createAuthorizationRequestUrl({
218
+ pkce: this._state.pkce,
219
+ endpointMetadata: this.endpointMetadata,
220
+ authorizationRequest: this._state.authorizationRequestOpts,
221
+ credentialOffer: this.credentialOffer,
222
+ credentialsSupported: this.getCredentialsSupported(true),
223
+ });
262
224
  }
263
-
264
- const response = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));
265
-
266
- return convertJsonToURI(
267
- { request_uri: response.successBody?.request_uri },
268
- {
269
- baseUrl: this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint,
270
- uriTypeProperties: ['request_uri'],
271
- mode: JsonURIMode.X_FORM_WWW_URLENCODED,
272
- },
273
- );
225
+ return this._state.authorizationURL;
274
226
  }
275
227
 
276
- public handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined {
277
- if (authorizationDetails) {
278
- if (Array.isArray(authorizationDetails)) {
279
- return authorizationDetails.map((value) => this.handleLocations({ ...value }));
228
+ public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
229
+ this.assertIssuerData();
230
+ if (!this._state.endpointMetadata) {
231
+ if (this.credentialOffer) {
232
+ this._state.endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
233
+ } else if (this._state.credentialIssuer) {
234
+ this._state.endpointMetadata = await MetadataClient.retrieveAllMetadata(this._state.credentialIssuer);
280
235
  } else {
281
- return this.handleLocations({ ...authorizationDetails });
236
+ throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`);
282
237
  }
283
238
  }
284
- return authorizationDetails;
239
+
240
+ return this.endpointMetadata;
285
241
  }
286
242
 
287
- private handleLocations(authorizationDetails: AuthDetails) {
288
- if (
289
- authorizationDetails &&
290
- (this.endpointMetadata.credentialIssuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)
291
- ) {
292
- if (authorizationDetails.locations) {
293
- if (Array.isArray(authorizationDetails.locations)) {
294
- (authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
295
- } else {
296
- authorizationDetails.locations = [authorizationDetails.locations as string, this.endpointMetadata.issuer];
297
- }
298
- } else {
299
- authorizationDetails.locations = this.endpointMetadata.issuer;
300
- }
301
- }
302
- return authorizationDetails;
243
+ private calculatePKCEOpts(pkce?: PKCEOpts) {
244
+ this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce });
303
245
  }
304
246
 
305
247
  public async acquireAccessToken(opts?: {
306
248
  pin?: string;
307
249
  clientId?: string;
308
250
  codeVerifier?: string;
309
- code?: string;
251
+ authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
252
+ code?: string; // Directly pass in a code from an auth response
310
253
  redirectUri?: string;
311
254
  }): Promise<AccessTokenResponse> {
312
- const { pin, clientId, codeVerifier, code, redirectUri } = opts ?? {};
255
+ const { pin, clientId } = opts ?? {};
256
+ let { redirectUri } = opts ?? {};
257
+ const code = opts?.code ?? (opts?.authorizationResponse ? toAuthorizationResponsePayload(opts.authorizationResponse).code : undefined);
313
258
 
259
+ if (opts?.codeVerifier) {
260
+ this._state.pkce.codeVerifier = opts.codeVerifier;
261
+ }
314
262
  this.assertIssuerData();
315
263
 
316
264
  if (clientId) {
317
- this._clientId = clientId;
265
+ this._state.clientId = clientId;
318
266
  }
319
- if (!this._accessTokenResponse) {
267
+ if (!this._state.accessTokenResponse) {
320
268
  const accessTokenClient = new AccessTokenClient();
321
269
 
270
+ if (redirectUri && redirectUri !== this._state.authorizationRequestOpts?.redirectUri) {
271
+ console.log(
272
+ `Redirect URI mismatch between access-token (${redirectUri}) and authorization request (${this._state.authorizationRequestOpts?.redirectUri}). According to the specification that is not allowed.`,
273
+ );
274
+ }
275
+ if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) {
276
+ redirectUri = this._state.authorizationRequestOpts.redirectUri;
277
+ }
278
+
322
279
  const response = await accessTokenClient.acquireAccessToken({
323
280
  credentialOffer: this.credentialOffer,
324
281
  metadata: this.endpointMetadata,
325
282
  credentialIssuer: this.getIssuer(),
326
283
  pin,
327
- codeVerifier,
284
+ ...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
328
285
  code,
329
286
  redirectUri,
330
- asOpts: { clientId },
287
+ asOpts: { clientId: this.clientId },
331
288
  });
332
289
 
333
290
  if (response.errorBody) {
334
291
  debug(`Access token error:\r\n${response.errorBody}`);
335
292
  throw Error(
336
- `Retrieving an access token from ${this._endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${
293
+ `Retrieving an access token from ${this._state.endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${
337
294
  response.origResponse.status
338
295
  }`,
339
296
  );
340
297
  } else if (!response.successBody) {
341
298
  debug(`Access token error. No success body`);
342
299
  throw Error(
343
- `Retrieving an access token from ${this._endpointMetadata
300
+ `Retrieving an access token from ${this._state.endpointMetadata
344
301
  ?.token_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`,
345
302
  );
346
303
  }
347
- this._accessTokenResponse = response.successBody;
304
+ this._state.accessTokenResponse = response.successBody;
348
305
  }
349
306
 
350
307
  return this.accessTokenResponse;
@@ -352,6 +309,7 @@ export class OpenID4VCIClient {
352
309
 
353
310
  public async acquireCredentials({
354
311
  credentialTypes,
312
+ context,
355
313
  proofCallbacks,
356
314
  format,
357
315
  kid,
@@ -362,6 +320,7 @@ export class OpenID4VCIClient {
362
320
  deferredCredentialIntervalInMS,
363
321
  }: {
364
322
  credentialTypes: string | string[];
323
+ context?: string[];
365
324
  proofCallbacks: ProofOfPossessionCallbacks<any>;
366
325
  format?: CredentialFormat | OID4VCICredentialFormat;
367
326
  kid?: string;
@@ -375,9 +334,9 @@ export class OpenID4VCIClient {
375
334
  throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
376
335
  }
377
336
 
378
- if (alg) this._alg = alg;
379
- if (jwk) this._jwk = jwk;
380
- if (kid) this._kid = kid;
337
+ if (alg) this._state.alg = alg;
338
+ if (jwk) this._state.jwk = jwk;
339
+ if (kid) this._state.kid = kid;
381
340
 
382
341
  const requestBuilder = this.credentialOffer
383
342
  ? CredentialRequestClientBuilder.fromCredentialOffer({
@@ -395,7 +354,7 @@ export class OpenID4VCIClient {
395
354
  requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);
396
355
  if (this.endpointMetadata?.credentialIssuerMetadata) {
397
356
  const metadata = this.endpointMetadata.credentialIssuerMetadata;
398
- const types = Array.isArray(credentialTypes) ? [...credentialTypes].sort() : [credentialTypes];
357
+ const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
399
358
 
400
359
  if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
401
360
  let typeSupported = false;
@@ -403,7 +362,7 @@ export class OpenID4VCIClient {
403
362
  metadata.credentials_supported.forEach((supportedCredential) => {
404
363
  const subTypes = getTypesFromCredentialSupported(supportedCredential);
405
364
  if (
406
- subTypes.sort().every((t, i) => types[i] === t) ||
365
+ subTypes.every((t, i) => types[i] === t) ||
407
366
  (types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0])))
408
367
  ) {
409
368
  typeSupported = true;
@@ -431,11 +390,11 @@ export class OpenID4VCIClient {
431
390
  .withIssuer(this.getIssuer())
432
391
  .withAlg(this.alg);
433
392
 
434
- if (this._jwk) {
435
- proofBuilder.withJWK(this._jwk);
393
+ if (this._state.jwk) {
394
+ proofBuilder.withJWK(this._state.jwk);
436
395
  }
437
- if (this._kid) {
438
- proofBuilder.withKid(this._kid);
396
+ if (this._state.kid) {
397
+ proofBuilder.withKid(this._state.kid);
439
398
  }
440
399
 
441
400
  if (this.clientId) {
@@ -446,26 +405,31 @@ export class OpenID4VCIClient {
446
405
  }
447
406
  const response = await credentialRequestClient.acquireCredentialsUsingProof({
448
407
  proofInput: proofBuilder,
449
- credentialTypes: credentialTypes,
408
+ credentialTypes,
409
+ context,
450
410
  format,
451
411
  });
452
412
  if (response.errorBody) {
453
413
  debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
454
414
  throw Error(
455
- `Retrieving a credential from ${this._endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
415
+ `Retrieving a credential from ${this._state.endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
456
416
  response.origResponse.status
457
417
  }`,
458
418
  );
459
419
  } else if (!response.successBody) {
460
420
  debug(`Credential request error. No success body`);
461
421
  throw Error(
462
- `Retrieving a credential from ${this._endpointMetadata
422
+ `Retrieving a credential from ${this._state.endpointMetadata
463
423
  ?.credential_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`,
464
424
  );
465
425
  }
466
426
  return response.successBody;
467
427
  }
468
428
 
429
+ public async exportState(): Promise<string> {
430
+ return JSON.stringify(this._state);
431
+ }
432
+
469
433
  // FIXME: We really should convert <v11 to v12 objects first. Right now the logic doesn't map nicely and is brittle.
470
434
  // We should resolve IDs to objects first in case of strings.
471
435
  // When < v11 convert into a v12 object. When v12 object retain it.
@@ -507,11 +471,26 @@ export class OpenID4VCIClient {
507
471
  }
508
472
 
509
473
  issuerSupportedFlowTypes(): AuthzFlowType[] {
510
- return this.credentialOffer?.supportedFlows ?? [AuthzFlowType.AUTHORIZATION_CODE_FLOW];
474
+ return (
475
+ this.credentialOffer?.supportedFlows ??
476
+ (this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : [])
477
+ );
478
+ }
479
+
480
+ isFlowTypeSupported(flowType: AuthzFlowType): boolean {
481
+ return this.issuerSupportedFlowTypes().includes(flowType);
482
+ }
483
+
484
+ get authorizationURL(): string | undefined {
485
+ return this._state.authorizationURL;
486
+ }
487
+
488
+ public hasAuthorizationURL(): boolean {
489
+ return !!this.authorizationURL;
511
490
  }
512
491
 
513
492
  get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
514
- return this._credentialOffer;
493
+ return this._state.credentialOffer;
515
494
  }
516
495
 
517
496
  public version(): OpenId4VCIVersion {
@@ -521,38 +500,46 @@ export class OpenID4VCIClient {
521
500
  public get endpointMetadata(): EndpointMetadataResult {
522
501
  this.assertServerMetadata();
523
502
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
524
- return this._endpointMetadata!;
503
+ return this._state.endpointMetadata!;
525
504
  }
526
505
 
527
506
  get kid(): string {
528
507
  this.assertIssuerData();
529
- if (!this._kid) {
508
+ if (!this._state.kid) {
530
509
  throw new Error('No value for kid is supplied');
531
510
  }
532
- return this._kid;
511
+ return this._state.kid;
533
512
  }
534
513
 
535
514
  get alg(): string {
536
515
  this.assertIssuerData();
537
- if (!this._alg) {
516
+ if (!this._state.alg) {
538
517
  throw new Error('No value for alg is supplied');
539
518
  }
540
- return this._alg;
519
+ return this._state.alg;
520
+ }
521
+
522
+ set clientId(value: string | undefined) {
523
+ this._state.clientId = value;
541
524
  }
542
525
 
543
526
  get clientId(): string | undefined {
544
- return this._clientId;
527
+ return this._state.clientId;
528
+ }
529
+
530
+ public hasAccessTokenResponse(): boolean {
531
+ return !!this._state.accessTokenResponse;
545
532
  }
546
533
 
547
534
  get accessTokenResponse(): AccessTokenResponse {
548
535
  this.assertAccessToken();
549
536
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
550
- return this._accessTokenResponse!;
537
+ return this._state.accessTokenResponse!;
551
538
  }
552
539
 
553
540
  public getIssuer(): string {
554
541
  this.assertIssuerData();
555
- return this._credentialIssuer!;
542
+ return this._state.credentialIssuer;
556
543
  }
557
544
 
558
545
  public getAccessTokenEndpoint(): string {
@@ -570,27 +557,62 @@ export class OpenID4VCIClient {
570
557
  public hasDeferredCredentialEndpoint(): boolean {
571
558
  return !!this.getAccessTokenEndpoint();
572
559
  }
560
+
573
561
  public getDeferredCredentialEndpoint(): string {
574
562
  this.assertIssuerData();
575
563
  return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
576
564
  }
565
+
566
+ /**
567
+ * Too bad we need a method like this, but EBSI is not exposing metadata
568
+ */
569
+ public isEBSI() {
570
+ if (
571
+ this.credentialOffer?.credential_offer.credentials.find(
572
+ (cred) =>
573
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
574
+ // @ts-ignore
575
+ typeof cred !== 'string' && 'trust_framework' in cred && 'name' in cred.trust_framework && cred.trust_framework.name.includes('ebsi'),
576
+ )
577
+ ) {
578
+ return true;
579
+ }
580
+ this.assertIssuerData();
581
+ return this.endpointMetadata.credentialIssuerMetadata?.authorization_endpoint?.includes('ebsi.eu');
582
+ }
583
+
577
584
  private assertIssuerData(): void {
578
- if (!this._credentialOffer && this.issuerSupportedFlowTypes().includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
579
- throw Error(`No issuance initiation or credential offer present`);
580
- } else if (!this._credentialIssuer) {
585
+ if (!this._state.credentialIssuer) {
581
586
  throw Error(`No credential issuer value present`);
587
+ } else if (!this._state.credentialOffer && this._state.endpointMetadata && this.issuerSupportedFlowTypes().length === 0) {
588
+ throw Error(`No issuance initiation or credential offer present`);
582
589
  }
583
590
  }
584
591
 
585
592
  private assertServerMetadata(): void {
586
- if (!this._endpointMetadata) {
593
+ if (!this._state.endpointMetadata) {
587
594
  throw Error('No server metadata');
588
595
  }
589
596
  }
590
597
 
591
598
  private assertAccessToken(): void {
592
- if (!this._accessTokenResponse) {
599
+ if (!this._state.accessTokenResponse) {
593
600
  throw Error(`No access token present`);
594
601
  }
595
602
  }
603
+
604
+ private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts): AuthorizationRequestOpts {
605
+ let authorizationRequestOpts = { ...this._state?.authorizationRequestOpts, ...opts } as AuthorizationRequestOpts;
606
+ if (!authorizationRequestOpts) {
607
+ // We only set a redirectUri if no options are provided.
608
+ // Note that this only works for mobile apps, that can handle a code query param on the default openid-credential-offer deeplink.
609
+ // Provide your own options if that is not desired!
610
+ authorizationRequestOpts = { redirectUri: `${DefaultURISchemes.CREDENTIAL_OFFER}://` };
611
+ }
612
+ const clientId = authorizationRequestOpts.clientId ?? this._state.clientId;
613
+ // sync clientId
614
+ this._state.clientId = clientId;
615
+ authorizationRequestOpts.clientId = clientId;
616
+ return authorizationRequestOpts;
617
+ }
596
618
  }