@sphereon/oid4vci-client 0.8.2-next.6 → 0.8.2-next.88

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