@sphereon/oid4vci-client 0.2.0 → 0.4.1-next.285

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 (107) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +494 -371
  3. package/dist/AccessTokenClient.d.ts +30 -0
  4. package/dist/AccessTokenClient.d.ts.map +1 -0
  5. package/dist/AccessTokenClient.js +222 -0
  6. package/dist/AccessTokenClient.js.map +1 -0
  7. package/dist/AuthorizationDetailsBuilder.d.ts +11 -0
  8. package/dist/AuthorizationDetailsBuilder.d.ts.map +1 -0
  9. package/dist/AuthorizationDetailsBuilder.js +44 -0
  10. package/dist/AuthorizationDetailsBuilder.js.map +1 -0
  11. package/dist/CredentialOfferClient.d.ts +10 -0
  12. package/dist/CredentialOfferClient.d.ts.map +1 -0
  13. package/dist/CredentialOfferClient.js +101 -0
  14. package/dist/CredentialOfferClient.js.map +1 -0
  15. package/dist/CredentialRequestClient.d.ts +33 -0
  16. package/dist/CredentialRequestClient.d.ts.map +1 -0
  17. package/dist/CredentialRequestClient.js +118 -0
  18. package/dist/CredentialRequestClient.js.map +1 -0
  19. package/dist/CredentialRequestClientBuilder.d.ts +34 -0
  20. package/dist/CredentialRequestClientBuilder.d.ts.map +1 -0
  21. package/dist/CredentialRequestClientBuilder.js +87 -0
  22. package/dist/CredentialRequestClientBuilder.js.map +1 -0
  23. package/dist/{main/lib/MetadataClient.d.ts → MetadataClient.d.ts} +39 -38
  24. package/dist/MetadataClient.d.ts.map +1 -0
  25. package/dist/MetadataClient.js +148 -0
  26. package/dist/MetadataClient.js.map +1 -0
  27. package/dist/OpenID4VCIClient.d.ts +75 -0
  28. package/dist/OpenID4VCIClient.d.ts.map +1 -0
  29. package/dist/OpenID4VCIClient.js +403 -0
  30. package/dist/OpenID4VCIClient.js.map +1 -0
  31. package/dist/ProofOfPossessionBuilder.d.ts +38 -0
  32. package/dist/ProofOfPossessionBuilder.d.ts.map +1 -0
  33. package/dist/ProofOfPossessionBuilder.js +129 -0
  34. package/dist/ProofOfPossessionBuilder.js.map +1 -0
  35. package/dist/functions/ProofUtil.d.ts +29 -0
  36. package/dist/functions/ProofUtil.d.ts.map +1 -0
  37. package/dist/functions/ProofUtil.js +104 -0
  38. package/dist/functions/ProofUtil.js.map +1 -0
  39. package/dist/functions/index.d.ts +4 -0
  40. package/dist/functions/index.d.ts.map +1 -0
  41. package/dist/{main → functions}/index.js +20 -18
  42. package/dist/functions/index.js.map +1 -0
  43. package/dist/index.d.ts +9 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/{main/lib/index.js → index.js} +25 -24
  46. package/dist/index.js.map +1 -0
  47. package/lib/AccessTokenClient.ts +249 -0
  48. package/lib/AuthorizationDetailsBuilder.ts +46 -0
  49. package/lib/CredentialOfferClient.ts +108 -0
  50. package/lib/CredentialRequestClient.ts +137 -0
  51. package/lib/CredentialRequestClientBuilder.ts +110 -0
  52. package/lib/MetadataClient.ts +147 -0
  53. package/lib/OpenID4VCIClient.ts +523 -0
  54. package/lib/ProofOfPossessionBuilder.ts +181 -0
  55. package/lib/__tests__/AccessTokenClient.spec.ts +225 -0
  56. package/lib/__tests__/AuthorizationDetailsBuilder.spec.ts +65 -0
  57. package/lib/__tests__/AuthzFlowType.spec.ts +39 -0
  58. package/lib/__tests__/CredentialRequestClient.spec.ts +291 -0
  59. package/lib/__tests__/CredentialRequestClientBuilder.spec.ts +121 -0
  60. package/lib/__tests__/HttpUtils.spec.ts +37 -0
  61. package/lib/__tests__/IT.spec.ts +173 -0
  62. package/lib/__tests__/IssuanceInitiation.spec.ts +48 -0
  63. package/lib/__tests__/JsonURIConversions.spec.ts +146 -0
  64. package/lib/__tests__/MetadataClient.spec.ts +203 -0
  65. package/lib/__tests__/MetadataMocks.ts +444 -0
  66. package/lib/__tests__/OpenID4VCIClient.spec.ts +166 -0
  67. package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +112 -0
  68. package/lib/__tests__/ProofOfPossessionBuilder.spec.ts +110 -0
  69. package/lib/__tests__/data/VciDataFixtures.ts +744 -0
  70. package/lib/functions/ProofUtil.ts +120 -0
  71. package/lib/functions/index.ts +3 -0
  72. package/{dist/main/lib/index.d.ts → lib/index.ts} +8 -7
  73. package/package.json +68 -71
  74. package/CHANGELOG.md +0 -21
  75. package/dist/main/index.d.ts +0 -1
  76. package/dist/main/lib/AccessTokenClient.d.ts +0 -20
  77. package/dist/main/lib/AccessTokenClient.js +0 -141
  78. package/dist/main/lib/CredentialRequestClient.d.ts +0 -31
  79. package/dist/main/lib/CredentialRequestClient.js +0 -66
  80. package/dist/main/lib/CredentialRequestClientBuilder.d.ts +0 -21
  81. package/dist/main/lib/CredentialRequestClientBuilder.js +0 -56
  82. package/dist/main/lib/IssuanceInitiation.d.ts +0 -5
  83. package/dist/main/lib/IssuanceInitiation.js +0 -29
  84. package/dist/main/lib/MetadataClient.js +0 -127
  85. package/dist/main/lib/functions/Encoding.d.ts +0 -17
  86. package/dist/main/lib/functions/Encoding.js +0 -138
  87. package/dist/main/lib/functions/HttpUtils.d.ts +0 -17
  88. package/dist/main/lib/functions/HttpUtils.js +0 -133
  89. package/dist/main/lib/functions/ProofUtil.d.ts +0 -9
  90. package/dist/main/lib/functions/ProofUtil.js +0 -76
  91. package/dist/main/lib/functions/index.d.ts +0 -3
  92. package/dist/main/lib/functions/index.js +0 -20
  93. package/dist/main/lib/types/Authorization.types.d.ts +0 -66
  94. package/dist/main/lib/types/Authorization.types.js +0 -35
  95. package/dist/main/lib/types/CredentialIssuance.types.d.ts +0 -88
  96. package/dist/main/lib/types/CredentialIssuance.types.js +0 -8
  97. package/dist/main/lib/types/Generic.types.d.ts +0 -19
  98. package/dist/main/lib/types/Generic.types.js +0 -11
  99. package/dist/main/lib/types/OAuth2ASMetadata.d.ts +0 -37
  100. package/dist/main/lib/types/OAuth2ASMetadata.js +0 -3
  101. package/dist/main/lib/types/OID4VCIServerMetadata.d.ts +0 -65
  102. package/dist/main/lib/types/OID4VCIServerMetadata.js +0 -3
  103. package/dist/main/lib/types/Oidc4vciErrors.d.ts +0 -3
  104. package/dist/main/lib/types/Oidc4vciErrors.js +0 -7
  105. package/dist/main/lib/types/index.d.ts +0 -6
  106. package/dist/main/lib/types/index.js +0 -23
  107. package/dist/main/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,523 @@
1
+ import {
2
+ AccessTokenResponse,
3
+ Alg,
4
+ AuthorizationRequestV1_0_09,
5
+ AuthzFlowType,
6
+ CodeChallengeMethod,
7
+ CredentialOfferPayloadV1_0_08,
8
+ CredentialOfferRequestWithBaseUrl,
9
+ CredentialResponse,
10
+ CredentialSupported,
11
+ EndpointMetadata,
12
+ OID4VCICredentialFormat,
13
+ OpenId4VCIVersion,
14
+ OpenIDResponse,
15
+ ProofOfPossessionCallbacks,
16
+ PushedAuthorizationResponse,
17
+ ResponseType,
18
+ } from '@sphereon/oid4vci-common';
19
+ import { getSupportedCredentials } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
20
+ import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types';
21
+ import { CredentialFormat } from '@sphereon/ssi-types';
22
+ import Debug from 'debug';
23
+
24
+ import { AccessTokenClient } from './AccessTokenClient';
25
+ import { CredentialOfferClient } from './CredentialOfferClient';
26
+ import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
27
+ import { MetadataClient } from './MetadataClient';
28
+ import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
29
+ import { convertJsonToURI, formPost } from './functions';
30
+
31
+ const debug = Debug('sphereon:oid4vci');
32
+
33
+ interface AuthDetails {
34
+ type: 'openid_credential' | string;
35
+ locations?: string | string[];
36
+ format: CredentialFormat | CredentialFormat[];
37
+
38
+ [s: string]: unknown;
39
+ }
40
+
41
+ interface AuthRequestOpts {
42
+ clientId: string;
43
+ codeChallenge: string;
44
+ codeChallengeMethod: CodeChallengeMethod;
45
+ authorizationDetails?: AuthDetails | AuthDetails[];
46
+ redirectUri: string;
47
+ scope?: string;
48
+ }
49
+
50
+ export class OpenID4VCIClient {
51
+ private readonly _flowType: AuthzFlowType;
52
+ private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl;
53
+ private _clientId?: string;
54
+ private _kid: string | undefined;
55
+ private _alg: Alg | string | undefined;
56
+ private _endpointMetadata: EndpointMetadata | undefined;
57
+ private _accessTokenResponse: AccessTokenResponse | undefined;
58
+
59
+ private constructor(
60
+ credentialOffer: CredentialOfferRequestWithBaseUrl,
61
+ flowType: AuthzFlowType,
62
+ kid?: string,
63
+ alg?: Alg | string,
64
+ clientId?: string
65
+ ) {
66
+ if (!credentialOffer.supportedFlows.includes(flowType)) {
67
+ throw Error(`Flows ${flowType} is not supported by issuer ${credentialOffer.credential_offer_uri}`);
68
+ }
69
+ this._flowType = flowType;
70
+ this._credentialOffer = credentialOffer;
71
+ this._kid = kid;
72
+ this._alg = alg;
73
+ this._clientId = clientId;
74
+ }
75
+
76
+ public static async fromURI({
77
+ uri,
78
+ flowType,
79
+ kid,
80
+ alg,
81
+ retrieveServerMetadata,
82
+ clientId,
83
+ resolveOfferUri,
84
+ }: {
85
+ uri: string;
86
+ flowType: AuthzFlowType;
87
+ kid?: string;
88
+ alg?: Alg | string;
89
+ retrieveServerMetadata?: boolean;
90
+ resolveOfferUri?: boolean;
91
+ clientId?: string;
92
+ }): Promise<OpenID4VCIClient> {
93
+ const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), flowType, kid, alg, clientId);
94
+
95
+ if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
96
+ await client.retrieveServerMetadata();
97
+ }
98
+ return client;
99
+ }
100
+
101
+ public async retrieveServerMetadata(): Promise<EndpointMetadata> {
102
+ this.assertIssuerData();
103
+ if (!this._endpointMetadata) {
104
+ this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
105
+ }
106
+ return this.endpointMetadata;
107
+ }
108
+
109
+ public createAuthorizationRequestUrl({
110
+ clientId,
111
+ codeChallengeMethod,
112
+ codeChallenge,
113
+ authorizationDetails,
114
+ redirectUri,
115
+ scope,
116
+ }: AuthRequestOpts): string {
117
+ // Scope and authorization_details can be used in the same authorization request
118
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
119
+ if (!scope && !authorizationDetails) {
120
+ throw Error('Please provide a scope or authorization_details');
121
+ }
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;
125
+ }
126
+ if (!this._endpointMetadata?.authorization_endpoint) {
127
+ throw Error('Server metadata does not contain authorization endpoint');
128
+ }
129
+
130
+ // add 'openid' scope if not present
131
+ if (scope && !scope.includes('openid')) {
132
+ scope = `openid ${scope}`;
133
+ }
134
+
135
+ //fixme: handle this for v11
136
+ const queryObj = {
137
+ response_type: ResponseType.AUTH_CODE,
138
+ client_id: clientId,
139
+ code_challenge_method: codeChallengeMethod,
140
+ code_challenge: codeChallenge,
141
+ authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
142
+ redirect_uri: redirectUri,
143
+ scope: scope,
144
+ } as AuthorizationRequestV1_0_09;
145
+
146
+ return convertJsonToURI(queryObj, {
147
+ baseUrl: this._endpointMetadata.authorization_endpoint,
148
+ uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details'],
149
+ version: this.version(),
150
+ });
151
+ }
152
+
153
+ public async acquirePushedAuthorizationRequestURI({
154
+ clientId,
155
+ codeChallengeMethod,
156
+ codeChallenge,
157
+ authorizationDetails,
158
+ redirectUri,
159
+ scope,
160
+ }: AuthRequestOpts): Promise<OpenIDResponse<PushedAuthorizationResponse>> {
161
+ // Scope and authorization_details can be used in the same authorization request
162
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
163
+ if (!scope && !authorizationDetails) {
164
+ throw Error('Please provide a scope or authorization_details');
165
+ }
166
+
167
+ // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document
168
+ // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow.
169
+ // What happens if it doesn't ???
170
+ // let parEndpoint: string
171
+ if (
172
+ !this._endpointMetadata?.issuerMetadata ||
173
+ !('pushed_authorization_request_endpoint' in this._endpointMetadata.issuerMetadata) ||
174
+ typeof this._endpointMetadata.issuerMetadata.pushed_authorization_request_endpoint !== 'string'
175
+ ) {
176
+ throw Error('Server metadata does not contain pushed authorization request endpoint');
177
+ }
178
+ const parEndpoint: string = this._endpointMetadata.issuerMetadata.pushed_authorization_request_endpoint;
179
+
180
+ // add 'openid' scope if not present
181
+ if (scope && !scope.includes('openid')) {
182
+ scope = `openid ${scope}`;
183
+ }
184
+
185
+ //fixme: handle this for v11
186
+ const queryObj: AuthorizationRequestV1_0_09 = {
187
+ response_type: ResponseType.AUTH_CODE,
188
+ client_id: clientId,
189
+ code_challenge_method: codeChallengeMethod,
190
+ code_challenge: codeChallenge,
191
+ authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
192
+ redirect_uri: redirectUri,
193
+ scope: scope,
194
+ };
195
+ return await formPost(parEndpoint, JSON.stringify(queryObj));
196
+ }
197
+
198
+ public handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined {
199
+ if (authorizationDetails) {
200
+ if (Array.isArray(authorizationDetails)) {
201
+ return authorizationDetails.map((value) => this.handleLocations({ ...value }));
202
+ } else {
203
+ return this.handleLocations({ ...authorizationDetails });
204
+ }
205
+ }
206
+ return authorizationDetails;
207
+ }
208
+
209
+ private handleLocations(authorizationDetails: AuthDetails) {
210
+ if (authorizationDetails && (this.endpointMetadata.issuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)) {
211
+ if (authorizationDetails.locations) {
212
+ if (Array.isArray(authorizationDetails.locations)) {
213
+ (authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
214
+ } else {
215
+ authorizationDetails.locations = [authorizationDetails.locations as string, this.endpointMetadata.issuer];
216
+ }
217
+ } else {
218
+ authorizationDetails.locations = this.endpointMetadata.issuer;
219
+ }
220
+ }
221
+ return authorizationDetails;
222
+ }
223
+
224
+ public async acquireAccessToken(opts?: {
225
+ pin?: string;
226
+ clientId?: string;
227
+ codeVerifier?: string;
228
+ code?: string;
229
+ redirectUri?: string;
230
+ }): Promise<AccessTokenResponse> {
231
+ const { pin, clientId, codeVerifier, code, redirectUri } = opts ?? {};
232
+ this.assertIssuerData();
233
+ if (clientId) {
234
+ this._clientId = clientId;
235
+ }
236
+ if (!this._accessTokenResponse) {
237
+ const accessTokenClient = new AccessTokenClient();
238
+
239
+ const response = await accessTokenClient.acquireAccessToken({
240
+ credentialOffer: this.credentialOffer,
241
+ metadata: this.endpointMetadata,
242
+ pin,
243
+ codeVerifier,
244
+ code,
245
+ redirectUri,
246
+ asOpts: { clientId },
247
+ });
248
+
249
+ if (response.errorBody) {
250
+ debug(`Access token error:\r\n${response.errorBody}`);
251
+ throw Error(
252
+ `Retrieving an access token from ${this._endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${
253
+ response.origResponse.status
254
+ }`
255
+ );
256
+ } else if (!response.successBody) {
257
+ debug(`Access token error. No success body`);
258
+ throw Error(
259
+ `Retrieving an access token from ${
260
+ this._endpointMetadata?.token_endpoint
261
+ } for issuer ${this.getIssuer()} failed as there was no success response body`
262
+ );
263
+ }
264
+ this._accessTokenResponse = response.successBody;
265
+ }
266
+
267
+ return this.accessTokenResponse;
268
+ }
269
+
270
+ public async acquireCredentials({
271
+ credentialTypes,
272
+ proofCallbacks,
273
+ format,
274
+ kid,
275
+ alg,
276
+ jti,
277
+ }: {
278
+ credentialTypes: string | string[];
279
+ proofCallbacks: ProofOfPossessionCallbacks;
280
+ format?: CredentialFormat | OID4VCICredentialFormat;
281
+ kid?: string;
282
+ alg?: Alg | string;
283
+ jti?: string;
284
+ }): Promise<CredentialResponse> {
285
+ if (alg) {
286
+ this._alg = alg;
287
+ }
288
+ if (kid) {
289
+ this._kid = kid;
290
+ }
291
+
292
+ const requestBuilder = CredentialRequestClientBuilder.fromCredentialOffer({
293
+ credentialOffer: this.credentialOffer,
294
+ metadata: this.endpointMetadata,
295
+ });
296
+ requestBuilder.withTokenFromResponse(this.accessTokenResponse);
297
+ if (this.endpointMetadata?.issuerMetadata) {
298
+ const metadata = this.endpointMetadata.issuerMetadata;
299
+ const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
300
+ if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
301
+ for (const type of types) {
302
+ let typeSupported = false;
303
+ for (const credentialSupported of metadata.credentials_supported) {
304
+ if (!credentialSupported.types || credentialSupported.types.length === 0) {
305
+ throw Error('types is required in the credentials supported');
306
+ }
307
+ if (credentialSupported.types.indexOf(type) != -1) {
308
+ typeSupported = true;
309
+ }
310
+ }
311
+ if (!typeSupported) {
312
+ throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
313
+ }
314
+ }
315
+ } else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) {
316
+ const credentialsSupported = metadata.credentials_supported as CredentialSupportedTypeV1_0_08;
317
+ if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) {
318
+ throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
319
+ }
320
+ }
321
+ // todo: Format check? We might end up with some disjoint type / format combinations supported by the server
322
+ }
323
+ const credentialRequestClient = requestBuilder.build();
324
+ const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({
325
+ accessTokenResponse: this.accessTokenResponse,
326
+ callbacks: proofCallbacks,
327
+ version: this.version(),
328
+ })
329
+ .withIssuer(this.getIssuer())
330
+ .withAlg(this.alg)
331
+ .withKid(this.kid);
332
+
333
+ if (this.clientId) {
334
+ proofBuilder.withClientId(this.clientId);
335
+ }
336
+ if (jti) {
337
+ proofBuilder.withJti(jti);
338
+ }
339
+ const response = await credentialRequestClient.acquireCredentialsUsingProof({
340
+ proofInput: proofBuilder,
341
+ credentialTypes: credentialTypes,
342
+ format,
343
+ });
344
+ if (response.errorBody) {
345
+ debug(`Credential request error:\r\n${response.errorBody}`);
346
+ throw Error(
347
+ `Retrieving a credential from ${this._endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
348
+ response.origResponse.status
349
+ }`
350
+ );
351
+ } else if (!response.successBody) {
352
+ debug(`Credential request error. No success body`);
353
+ throw Error(
354
+ `Retrieving a credential from ${
355
+ this._endpointMetadata?.credential_endpoint
356
+ } for issuer ${this.getIssuer()} failed as there was no success response body`
357
+ );
358
+ }
359
+ return response.successBody;
360
+ }
361
+
362
+ getCredentialsSupported(restrictToInitiationTypes: boolean, supportedType?: string): CredentialSupported[] {
363
+ return getSupportedCredentials({
364
+ issuerMetadata: this.endpointMetadata.issuerMetadata,
365
+ version: this.version(),
366
+ supportedType,
367
+ credentialTypes: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
368
+ });
369
+ /*//FIXME: delegate to getCredentialsSupported from IssuerMetadataUtils
370
+ let credentialsSupported = this.endpointMetadata?.issuerMetadata?.credentials_supported
371
+
372
+ if (this.version() === OpenId4VCIVersion.VER_1_0_08 || typeof credentialsSupported === 'object') {
373
+ const issuerMetadata = this.endpointMetadata.issuerMetadata as IssuerMetadataV1_0_08
374
+ const v8CredentialsSupported = issuerMetadata.credentials_supported
375
+ credentialsSupported = []
376
+ credentialsSupported = Object.entries(v8CredentialsSupported).map((key, value) => )
377
+
378
+ }
379
+
380
+
381
+ if (!credentialsSupported) {
382
+ return []
383
+ } else if (!restrictToInitiationTypes) {
384
+ return credentialsSupported
385
+ }
386
+
387
+
388
+
389
+ /!**
390
+ * the following (not array part is a legacy code from version 1_0-08 which jff implementors used)
391
+ *!/
392
+ if (!Array.isArray(credentialsSupported)) {
393
+ const credentialsSupportedV8: CredentialSupportedV1_0_08 = credentialsSupported as CredentialSupportedV1_0_08;
394
+ const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes();
395
+ const supported: IssuerCredentialSubject = {};
396
+ for (const [key, value] of Object.entries(credentialsSupportedV8)) {
397
+ if (initiationTypes.includes(key)) {
398
+ supported[key] = value;
399
+ }
400
+ }
401
+ // todo: fix this later. we're returning CredentialSupportedV1_0_08 as a list of CredentialSupported (for v09 onward)
402
+ return supported as unknown as CredentialSupported[];
403
+ }
404
+ const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes()
405
+ const credentialSupportedOverlap: CredentialSupported[] = []
406
+ for (const supported of credentialsSupported) {
407
+ const supportedTypeOverlap: string[] = []
408
+ for (const type of supported.types) {
409
+ initiationTypes.includes(type)
410
+ supportedTypeOverlap.push(type)
411
+ }
412
+ if (supportedTypeOverlap.length > 0) {
413
+ credentialSupportedOverlap.push({
414
+ ...supported,
415
+ types: supportedTypeOverlap
416
+ })
417
+ }
418
+ }
419
+ return credentialSupportedOverlap as CredentialSupported[]*/
420
+ }
421
+
422
+ getCredentialMetadata(type: string): CredentialSupported[] {
423
+ return this.getCredentialsSupported(false, type);
424
+ }
425
+
426
+ // todo https://sphereon.atlassian.net/browse/VDX-184
427
+ getCredentialTypes(): string[] {
428
+ if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
429
+ return typeof (this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type === 'string'
430
+ ? [(this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string]
431
+ : ((this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string[]);
432
+ } else {
433
+ // FIXME: this for sure isn't correct. It would also include VerifiableCredential. The whole call to this getCredentialsTypes should be changed to begin with
434
+ return this.credentialOffer.credential_offer.credentials.flatMap((c) => (typeof c === 'string' ? c : c.types));
435
+ }
436
+ }
437
+
438
+ get flowType(): AuthzFlowType {
439
+ return this._flowType;
440
+ }
441
+
442
+ issuerSupportedFlowTypes(): AuthzFlowType[] {
443
+ return this.credentialOffer.supportedFlows;
444
+ }
445
+
446
+ get credentialOffer(): CredentialOfferRequestWithBaseUrl {
447
+ return this._credentialOffer;
448
+ }
449
+
450
+ public version(): OpenId4VCIVersion {
451
+ return this.credentialOffer.version;
452
+ }
453
+
454
+ public get endpointMetadata(): EndpointMetadata {
455
+ this.assertServerMetadata();
456
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
457
+ return this._endpointMetadata!;
458
+ }
459
+
460
+ get kid(): string {
461
+ this.assertIssuerData();
462
+ if (!this._kid) {
463
+ throw new Error('No value for kid is supplied');
464
+ }
465
+ return this._kid;
466
+ }
467
+
468
+ get alg(): string {
469
+ this.assertIssuerData();
470
+ if (!this._alg) {
471
+ throw new Error('No value for alg is supplied');
472
+ }
473
+ return this._alg;
474
+ }
475
+
476
+ get clientId(): string | undefined {
477
+ /*if (!this._clientId) {
478
+ throw Error('No client id present');
479
+ }*/
480
+ return this._clientId;
481
+ }
482
+
483
+ get accessTokenResponse(): AccessTokenResponse {
484
+ this.assertAccessToken();
485
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
486
+ return this._accessTokenResponse!;
487
+ }
488
+
489
+ public getIssuer(): string {
490
+ this.assertIssuerData();
491
+ return this._endpointMetadata ? this.endpointMetadata.issuer : this.getIssuer();
492
+ }
493
+
494
+ public getAccessTokenEndpoint(): string {
495
+ this.assertIssuerData();
496
+ return this.endpointMetadata
497
+ ? this.endpointMetadata.token_endpoint
498
+ : AccessTokenClient.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } });
499
+ }
500
+
501
+ public getCredentialEndpoint(): string {
502
+ this.assertIssuerData();
503
+ return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
504
+ }
505
+
506
+ private assertIssuerData(): void {
507
+ if (!this._credentialOffer) {
508
+ throw Error(`No issuance initiation or credential offer present`);
509
+ }
510
+ }
511
+
512
+ private assertServerMetadata(): void {
513
+ if (!this._endpointMetadata) {
514
+ throw Error('No server metadata');
515
+ }
516
+ }
517
+
518
+ private assertAccessToken(): void {
519
+ if (!this._accessTokenResponse) {
520
+ throw Error(`No access token present`);
521
+ }
522
+ }
523
+ }
@@ -0,0 +1,181 @@
1
+ import {
2
+ AccessTokenResponse,
3
+ Alg,
4
+ EndpointMetadata,
5
+ Jwt,
6
+ NO_JWT_PROVIDED,
7
+ OpenId4VCIVersion,
8
+ PROOF_CANT_BE_CONSTRUCTED,
9
+ ProofOfPossession,
10
+ ProofOfPossessionCallbacks,
11
+ Typ,
12
+ } from '@sphereon/oid4vci-common';
13
+
14
+ import { createProofOfPossession } from './functions';
15
+
16
+ export class ProofOfPossessionBuilder {
17
+ private readonly proof?: ProofOfPossession;
18
+ private readonly callbacks?: ProofOfPossessionCallbacks;
19
+
20
+ private version: OpenId4VCIVersion;
21
+
22
+ private kid?: string;
23
+ private clientId?: string;
24
+ private issuer?: string;
25
+ private jwt?: Jwt;
26
+ private alg?: string;
27
+ private jti?: string;
28
+ private cNonce?: string;
29
+ private typ?: Typ;
30
+
31
+ private constructor({
32
+ proof,
33
+ callbacks,
34
+ jwt,
35
+ accessTokenResponse,
36
+ version,
37
+ }: {
38
+ proof?: ProofOfPossession;
39
+ callbacks?: ProofOfPossessionCallbacks;
40
+ accessTokenResponse?: AccessTokenResponse;
41
+ jwt?: Jwt;
42
+ version: OpenId4VCIVersion;
43
+ }) {
44
+ this.proof = proof;
45
+ this.callbacks = callbacks;
46
+ if (jwt) {
47
+ this.withJwt(jwt);
48
+ }
49
+ if (accessTokenResponse) {
50
+ this.withAccessTokenResponse(accessTokenResponse);
51
+ }
52
+ this.version = version;
53
+ }
54
+
55
+ static fromJwt({
56
+ jwt,
57
+ callbacks,
58
+ version,
59
+ }: {
60
+ jwt: Jwt;
61
+ callbacks: ProofOfPossessionCallbacks;
62
+ version: OpenId4VCIVersion;
63
+ }): ProofOfPossessionBuilder {
64
+ return new ProofOfPossessionBuilder({ callbacks, jwt, version });
65
+ }
66
+
67
+ static fromAccessTokenResponse({
68
+ accessTokenResponse,
69
+ callbacks,
70
+ version,
71
+ }: {
72
+ accessTokenResponse: AccessTokenResponse;
73
+ callbacks: ProofOfPossessionCallbacks;
74
+ version: OpenId4VCIVersion;
75
+ }): ProofOfPossessionBuilder {
76
+ return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version });
77
+ }
78
+
79
+ static fromProof(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder {
80
+ return new ProofOfPossessionBuilder({ proof, version });
81
+ }
82
+
83
+ withClientId(clientId: string): ProofOfPossessionBuilder {
84
+ this.clientId = clientId;
85
+ return this;
86
+ }
87
+
88
+ withKid(kid: string): ProofOfPossessionBuilder {
89
+ this.kid = kid;
90
+ return this;
91
+ }
92
+
93
+ withIssuer(issuer: string): ProofOfPossessionBuilder {
94
+ this.issuer = issuer;
95
+ return this;
96
+ }
97
+
98
+ withAlg(alg: Alg | string): ProofOfPossessionBuilder {
99
+ this.alg = alg;
100
+ return this;
101
+ }
102
+
103
+ withJti(jti: string): ProofOfPossessionBuilder {
104
+ this.jti = jti;
105
+ return this;
106
+ }
107
+
108
+ withTyp(typ: Typ): ProofOfPossessionBuilder {
109
+ this.typ = typ;
110
+ return this;
111
+ }
112
+
113
+ withAccessTokenNonce(cNonce: string): ProofOfPossessionBuilder {
114
+ this.cNonce = cNonce;
115
+ return this;
116
+ }
117
+
118
+ withAccessTokenResponse(accessToken: AccessTokenResponse): ProofOfPossessionBuilder {
119
+ if (accessToken.c_nonce) {
120
+ this.withAccessTokenNonce(accessToken.c_nonce);
121
+ }
122
+ return this;
123
+ }
124
+
125
+ withEndpointMetadata(endpointMetadata: EndpointMetadata): ProofOfPossessionBuilder {
126
+ this.withIssuer(endpointMetadata.issuer);
127
+ return this;
128
+ }
129
+
130
+ withJwt(jwt: Jwt): ProofOfPossessionBuilder {
131
+ if (!jwt) {
132
+ throw new Error(NO_JWT_PROVIDED);
133
+ }
134
+ this.jwt = jwt;
135
+ if (!jwt.header) {
136
+ throw Error(`No JWT header present`);
137
+ } else if (!jwt.payload) {
138
+ throw Error(`No JWT payload present`);
139
+ }
140
+
141
+ if (jwt.header.kid) {
142
+ this.withKid(jwt.header.kid);
143
+ }
144
+ if (jwt.header.typ) {
145
+ this.withTyp(jwt.header.typ as Typ);
146
+ }
147
+ if (this.version >= OpenId4VCIVersion.VER_1_0_11) {
148
+ this.withTyp('openid4vci-proof+jwt');
149
+ }
150
+ this.withAlg(jwt.header.alg);
151
+
152
+ if (jwt.payload) {
153
+ if (jwt.payload.iss) this.withClientId(jwt.payload.iss);
154
+ if (jwt.payload.aud) this.withIssuer(jwt.payload.aud);
155
+ if (jwt.payload.jti) this.withJti(jwt.payload.jti);
156
+ if (jwt.payload.nonce) this.withAccessTokenNonce(jwt.payload.nonce);
157
+ }
158
+ return this;
159
+ }
160
+
161
+ public async build(): Promise<ProofOfPossession> {
162
+ if (this.proof) {
163
+ return Promise.resolve(this.proof);
164
+ } else if (this.callbacks) {
165
+ return await createProofOfPossession(
166
+ this.callbacks,
167
+ {
168
+ typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'),
169
+ kid: this.kid,
170
+ jti: this.jti,
171
+ alg: this.alg,
172
+ issuer: this.issuer,
173
+ clientId: this.clientId,
174
+ nonce: this.cNonce,
175
+ },
176
+ this.jwt
177
+ );
178
+ }
179
+ throw new Error(PROOF_CANT_BE_CONSTRUCTED);
180
+ }
181
+ }