@sphereon/oid4vci-client 0.10.4-unstable.2 → 0.10.4-unstable.21

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 (98) hide show
  1. package/dist/AccessTokenClient.d.ts +5 -5
  2. package/dist/AccessTokenClient.d.ts.map +1 -1
  3. package/dist/AccessTokenClient.js +42 -22
  4. package/dist/AccessTokenClient.js.map +1 -1
  5. package/dist/AccessTokenClientV1_0_11.d.ts +29 -0
  6. package/dist/AccessTokenClientV1_0_11.d.ts.map +1 -0
  7. package/dist/AccessTokenClientV1_0_11.js +212 -0
  8. package/dist/AccessTokenClientV1_0_11.js.map +1 -0
  9. package/dist/AuthorizationCodeClient.d.ts +4 -4
  10. package/dist/AuthorizationCodeClient.d.ts.map +1 -1
  11. package/dist/AuthorizationCodeClient.js +14 -3
  12. package/dist/AuthorizationCodeClient.js.map +1 -1
  13. package/dist/AuthorizationCodeClientV1_0_11.d.ts +9 -0
  14. package/dist/AuthorizationCodeClientV1_0_11.d.ts.map +1 -0
  15. package/dist/AuthorizationCodeClientV1_0_11.js +132 -0
  16. package/dist/AuthorizationCodeClientV1_0_11.js.map +1 -0
  17. package/dist/CredentialOfferClient.d.ts.map +1 -1
  18. package/dist/CredentialOfferClient.js +14 -25
  19. package/dist/CredentialOfferClient.js.map +1 -1
  20. package/dist/CredentialOfferClientV1_0_11.d.ts +10 -0
  21. package/dist/CredentialOfferClientV1_0_11.d.ts.map +1 -0
  22. package/dist/CredentialOfferClientV1_0_11.js +103 -0
  23. package/dist/CredentialOfferClientV1_0_11.js.map +1 -0
  24. package/dist/CredentialRequestClient.d.ts +1 -1
  25. package/dist/CredentialRequestClient.d.ts.map +1 -1
  26. package/dist/CredentialRequestClient.js +9 -6
  27. package/dist/CredentialRequestClient.js.map +1 -1
  28. package/dist/CredentialRequestClientBuilder.d.ts +3 -3
  29. package/dist/CredentialRequestClientBuilder.d.ts.map +1 -1
  30. package/dist/CredentialRequestClientBuilder.js +2 -2
  31. package/dist/CredentialRequestClientBuilder.js.map +1 -1
  32. package/dist/CredentialRequestClientBuilderV1_0_11.d.ts +46 -0
  33. package/dist/CredentialRequestClientBuilderV1_0_11.d.ts.map +1 -0
  34. package/dist/CredentialRequestClientBuilderV1_0_11.js +117 -0
  35. package/dist/CredentialRequestClientBuilderV1_0_11.js.map +1 -0
  36. package/dist/CredentialRequestClientV1_0_11.d.ts +44 -0
  37. package/dist/CredentialRequestClientV1_0_11.d.ts.map +1 -0
  38. package/dist/CredentialRequestClientV1_0_11.js +151 -0
  39. package/dist/CredentialRequestClientV1_0_11.js.map +1 -0
  40. package/dist/MetadataClient.d.ts +5 -15
  41. package/dist/MetadataClient.d.ts.map +1 -1
  42. package/dist/MetadataClient.js +13 -33
  43. package/dist/MetadataClient.js.map +1 -1
  44. package/dist/MetadataClientV1_0_11.d.ts +31 -0
  45. package/dist/MetadataClientV1_0_11.d.ts.map +1 -0
  46. package/dist/MetadataClientV1_0_11.js +182 -0
  47. package/dist/MetadataClientV1_0_11.js.map +1 -0
  48. package/dist/OpenID4VCIClient.d.ts +5 -17
  49. package/dist/OpenID4VCIClient.d.ts.map +1 -1
  50. package/dist/OpenID4VCIClient.js +3 -74
  51. package/dist/OpenID4VCIClient.js.map +1 -1
  52. package/dist/OpenID4VCIClientV1_0_11.d.ts +107 -0
  53. package/dist/OpenID4VCIClientV1_0_11.d.ts.map +1 -0
  54. package/dist/OpenID4VCIClientV1_0_11.js +462 -0
  55. package/dist/OpenID4VCIClientV1_0_11.js.map +1 -0
  56. package/dist/functions/OpenIDUtils.d.ts +12 -0
  57. package/dist/functions/OpenIDUtils.d.ts.map +1 -0
  58. package/dist/functions/OpenIDUtils.js +37 -0
  59. package/dist/functions/OpenIDUtils.js.map +1 -0
  60. package/dist/index.d.ts +8 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +8 -1
  63. package/dist/index.js.map +1 -1
  64. package/lib/AccessTokenClient.ts +54 -22
  65. package/lib/AccessTokenClientV1_0_11.ts +255 -0
  66. package/lib/AuthorizationCodeClient.ts +31 -11
  67. package/lib/AuthorizationCodeClientV1_0_11.ts +167 -0
  68. package/lib/CredentialOfferClient.ts +13 -25
  69. package/lib/CredentialOfferClientV1_0_11.ts +112 -0
  70. package/lib/CredentialRequestClient.ts +11 -7
  71. package/lib/CredentialRequestClientBuilder.ts +9 -8
  72. package/lib/CredentialRequestClientBuilderV1_0_11.ts +156 -0
  73. package/lib/CredentialRequestClientV1_0_11.ts +190 -0
  74. package/lib/MetadataClient.ts +26 -48
  75. package/lib/MetadataClientV1_0_11.ts +186 -0
  76. package/lib/OpenID4VCIClient.ts +10 -92
  77. package/lib/OpenID4VCIClientV1_0_11.ts +644 -0
  78. package/lib/__tests__/AccessTokenClient.spec.ts +34 -6
  79. package/lib/__tests__/CredentialRequestClient.spec.ts +56 -39
  80. package/lib/__tests__/CredentialRequestClientBuilder.spec.ts +4 -4
  81. package/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts +316 -0
  82. package/lib/__tests__/EBSIE2E.spec.test.ts +2 -2
  83. package/lib/__tests__/IT.spec.ts +222 -11
  84. package/lib/__tests__/IssuanceInitiation.spec.ts +32 -51
  85. package/lib/__tests__/IssuanceInitiationV1_0_11.spec.ts +62 -0
  86. package/lib/__tests__/MattrE2E.spec.test.ts +2 -2
  87. package/lib/__tests__/MetadataClient.spec.ts +70 -6
  88. package/lib/__tests__/MetadataMocks.ts +41 -2
  89. package/lib/__tests__/OpenID4VCIClient.spec.ts +1 -1
  90. package/lib/__tests__/{OpenID4VCIClientPAR.spec.ts → OpenID4VCIClientPARV1_0_11.spec.ts} +5 -5
  91. package/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts +202 -0
  92. package/lib/__tests__/ProofOfPossessionBuilder.spec.ts +1 -1
  93. package/lib/__tests__/SdJwt.spec.ts +31 -21
  94. package/lib/__tests__/SphereonE2E.spec.test.ts +3 -3
  95. package/lib/__tests__/data/VciDataFixtures.ts +664 -27
  96. package/lib/functions/OpenIDUtils.ts +25 -0
  97. package/lib/index.ts +8 -1
  98. package/package.json +3 -3
@@ -5,15 +5,20 @@ import {
5
5
  assertedUniformCredentialOffer,
6
6
  AuthorizationServerOpts,
7
7
  AuthzFlowType,
8
+ CredentialOfferPayloadV1_0_13,
9
+ CredentialOfferV1_0_13,
10
+ determineSpecVersionFromOffer,
8
11
  EndpointMetadata,
9
12
  getIssuerFromCredentialOfferPayload,
10
13
  GrantTypes,
11
14
  IssuerOpts,
12
15
  JsonURIMode,
16
+ OpenId4VCIVersion,
13
17
  OpenIDResponse,
14
18
  PRE_AUTH_CODE_LITERAL,
15
19
  TokenErrorResponse,
16
20
  toUniformCredentialOfferRequest,
21
+ TxCodeAndPinRequired,
17
22
  UniformCredentialOfferPayload,
18
23
  } from '@sphereon/oid4vci-common';
19
24
  import { ObjectUtils } from '@sphereon/ssi-types';
@@ -29,7 +34,7 @@ export class AccessTokenClient {
29
34
  const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts;
30
35
 
31
36
  const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined;
32
- const isPinRequired = credentialOffer && this.isPinRequiredValue(credentialOffer.credential_offer);
37
+ const pinMetadata: TxCodeAndPinRequired | undefined = credentialOffer && this.getPinMetadata(credentialOffer.credential_offer);
33
38
  const issuer =
34
39
  opts.credentialIssuer ??
35
40
  (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : (metadata?.issuer as string));
@@ -48,8 +53,9 @@ export class AccessTokenClient {
48
53
  code,
49
54
  redirectUri,
50
55
  pin,
56
+ pinMetadata,
51
57
  }),
52
- isPinRequired,
58
+ pinMetadata,
53
59
  metadata,
54
60
  asOpts,
55
61
  issuerOpts,
@@ -58,18 +64,18 @@ export class AccessTokenClient {
58
64
 
59
65
  public async acquireAccessTokenUsingRequest({
60
66
  accessTokenRequest,
61
- isPinRequired,
67
+ pinMetadata,
62
68
  metadata,
63
69
  asOpts,
64
70
  issuerOpts,
65
71
  }: {
66
72
  accessTokenRequest: AccessTokenRequest;
67
- isPinRequired?: boolean;
73
+ pinMetadata?: TxCodeAndPinRequired;
68
74
  metadata?: EndpointMetadata;
69
75
  asOpts?: AuthorizationServerOpts;
70
76
  issuerOpts?: IssuerOpts;
71
77
  }): Promise<OpenIDResponse<AccessTokenResponse>> {
72
- this.validate(accessTokenRequest, isPinRequired);
78
+ this.validate(accessTokenRequest, pinMetadata);
73
79
 
74
80
  const requestTokenURL = AccessTokenClient.determineTokenURL({
75
81
  asOpts,
@@ -86,7 +92,11 @@ export class AccessTokenClient {
86
92
 
87
93
  public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
88
94
  const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
89
- const credentialOfferRequest = opts.credentialOffer ? await toUniformCredentialOfferRequest(opts.credentialOffer) : undefined;
95
+ const credentialOfferRequest =
96
+ opts.credentialOffer &&
97
+ determineSpecVersionFromOffer(opts.credentialOffer as CredentialOfferPayloadV1_0_13).valueOf() <= OpenId4VCIVersion.VER_1_0_13.valueOf()
98
+ ? await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_13)
99
+ : undefined;
90
100
  const request: Partial<AccessTokenRequest> = {};
91
101
 
92
102
  if (asOpts?.clientId) {
@@ -94,7 +104,7 @@ export class AccessTokenClient {
94
104
  }
95
105
 
96
106
  if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
97
- this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin);
107
+ this.assertAlphanumericPin(opts.pinMetadata, pin);
98
108
  request.user_pin = pin;
99
109
 
100
110
  request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE;
@@ -132,28 +142,50 @@ export class AccessTokenClient {
132
142
  }
133
143
  }
134
144
 
135
- private isPinRequiredValue(requestPayload: UniformCredentialOfferPayload): boolean {
136
- let isPinRequired = false;
145
+ private getPinMetadata(requestPayload: UniformCredentialOfferPayload): TxCodeAndPinRequired {
137
146
  if (!requestPayload) {
138
147
  throw new Error(TokenErrorResponse.invalid_request);
139
148
  }
140
149
  const issuer = getIssuerFromCredentialOfferPayload(requestPayload);
141
- if (requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']) {
142
- isPinRequired = requestPayload.grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.user_pin_required ?? false;
143
- }
150
+
151
+ const grantDetails = requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code'];
152
+ const isPinRequired = !!grantDetails?.tx_code || !!grantDetails?.['pre-authorized_code'];
153
+
144
154
  debug(`Pin required for issuer ${issuer}: ${isPinRequired}`);
145
- return isPinRequired;
155
+ return {
156
+ txCode: grantDetails?.tx_code,
157
+ isPinRequired,
158
+ };
146
159
  }
147
160
 
148
- private assertNumericPin(isPinRequired?: boolean, pin?: string): void {
149
- if (isPinRequired) {
150
- if (!pin || !/^\d{1,8}$/.test(pin)) {
151
- debug(`Pin is not 1 to 8 digits long`);
152
- throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.');
161
+ private assertAlphanumericPin(pinMeta?: TxCodeAndPinRequired, pin?: string): void {
162
+ if (pinMeta?.isPinRequired) {
163
+ let regex;
164
+
165
+ if (pinMeta.txCode) {
166
+ const { input_mode, length } = pinMeta.txCode;
167
+
168
+ if (input_mode === 'numeric') {
169
+ // Create a regex for numeric input. If no length specified, allow any length of numeric input.
170
+ regex = length ? new RegExp(`^\\d{1,${length}}$`) : /^\d+$/;
171
+ } else if (input_mode === 'text') {
172
+ // Create a regex for text input. If no length specified, allow any length of alphanumeric input.
173
+ regex = length ? new RegExp(`^[a-zA-Z0-9]{1,${length}}$`) : /^[a-zA-Z0-9]+$/;
174
+ }
175
+ }
176
+
177
+ // Default regex for alphanumeric with no specific length limit if no input_mode is specified.
178
+ regex = regex || /^[a-zA-Z0-9]+$|^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/;
179
+
180
+ if (!pin || !regex.test(pin)) {
181
+ debug(
182
+ `Pin is not valid. Expected format: ${pinMeta?.txCode?.input_mode || 'alphanumeric'}, Length: up to ${pinMeta?.txCode?.length || 'any number of'} characters`,
183
+ );
184
+ throw new Error('A valid pin must be present according to the specified transaction code requirements.');
153
185
  }
154
186
  } else if (pin) {
155
- debug(`Pin set, whilst not required`);
156
- throw new Error('Cannot set a pin, when the pin is not required.');
187
+ debug('Pin set, whilst not required');
188
+ throw new Error('Cannot set a pin when the pin is not required.');
157
189
  }
158
190
  }
159
191
 
@@ -177,11 +209,11 @@ export class AccessTokenClient {
177
209
  throw new Error('Authorization flow requires the code to be present');
178
210
  }
179
211
  }
180
- private validate(accessTokenRequest: AccessTokenRequest, isPinRequired?: boolean): void {
212
+ private validate(accessTokenRequest: AccessTokenRequest, pinMeta?: TxCodeAndPinRequired): void {
181
213
  if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
182
214
  this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
183
215
  this.assertNonEmptyPreAuthorizedCode(accessTokenRequest);
184
- this.assertNumericPin(isPinRequired, accessTokenRequest.user_pin);
216
+ this.assertAlphanumericPin(pinMeta, accessTokenRequest['pre-authorized_code']);
185
217
  } else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) {
186
218
  this.assertAuthorizationGrantType(accessTokenRequest.grant_type);
187
219
  this.assertNonEmptyCodeVerifier(accessTokenRequest);
@@ -0,0 +1,255 @@
1
+ import {
2
+ AccessTokenRequest,
3
+ AccessTokenRequestOpts,
4
+ AccessTokenResponse,
5
+ assertedUniformCredentialOffer,
6
+ AuthorizationServerOpts,
7
+ AuthzFlowType,
8
+ CredentialOfferPayloadV1_0_11,
9
+ CredentialOfferV1_0_11,
10
+ CredentialOfferV1_0_13,
11
+ determineSpecVersionFromOffer,
12
+ EndpointMetadata,
13
+ getIssuerFromCredentialOfferPayload,
14
+ GrantTypes,
15
+ IssuerOpts,
16
+ JsonURIMode,
17
+ OpenId4VCIVersion,
18
+ OpenIDResponse,
19
+ PRE_AUTH_CODE_LITERAL,
20
+ TokenErrorResponse,
21
+ toUniformCredentialOfferRequest,
22
+ toUniformCredentialOfferRequestV1_0_11,
23
+ UniformCredentialOfferPayload,
24
+ } from '@sphereon/oid4vci-common';
25
+ import { ObjectUtils } from '@sphereon/ssi-types';
26
+ import Debug from 'debug';
27
+
28
+ import { MetadataClient } from './MetadataClient';
29
+ import { convertJsonToURI, formPost } from './functions';
30
+
31
+ const debug = Debug('sphereon:oid4vci:token');
32
+
33
+ export class AccessTokenClientV1_0_11 {
34
+ public async acquireAccessToken(opts: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse>> {
35
+ const { asOpts, pin, codeVerifier, code, redirectUri, metadata } = opts;
36
+
37
+ const credentialOffer = opts.credentialOffer ? await assertedUniformCredentialOffer(opts.credentialOffer) : undefined;
38
+ const isPinRequired = credentialOffer && this.isPinRequiredValue(credentialOffer.credential_offer);
39
+ const issuer =
40
+ opts.credentialIssuer ??
41
+ (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : (metadata?.issuer as string));
42
+ if (!issuer) {
43
+ throw Error('Issuer required at this point');
44
+ }
45
+ const issuerOpts = {
46
+ issuer,
47
+ };
48
+
49
+ return await this.acquireAccessTokenUsingRequest({
50
+ accessTokenRequest: await this.createAccessTokenRequest({
51
+ credentialOffer,
52
+ asOpts,
53
+ codeVerifier,
54
+ code,
55
+ redirectUri,
56
+ pin,
57
+ }),
58
+ isPinRequired,
59
+ metadata,
60
+ asOpts,
61
+ issuerOpts,
62
+ });
63
+ }
64
+
65
+ public async acquireAccessTokenUsingRequest({
66
+ accessTokenRequest,
67
+ isPinRequired,
68
+ metadata,
69
+ asOpts,
70
+ issuerOpts,
71
+ }: {
72
+ accessTokenRequest: AccessTokenRequest;
73
+ isPinRequired?: boolean;
74
+ metadata?: EndpointMetadata;
75
+ asOpts?: AuthorizationServerOpts;
76
+ issuerOpts?: IssuerOpts;
77
+ }): Promise<OpenIDResponse<AccessTokenResponse>> {
78
+ this.validate(accessTokenRequest, isPinRequired);
79
+
80
+ const requestTokenURL = AccessTokenClientV1_0_11.determineTokenURL({
81
+ asOpts,
82
+ issuerOpts,
83
+ metadata: metadata
84
+ ? metadata
85
+ : issuerOpts?.fetchMetadata
86
+ ? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
87
+ : undefined,
88
+ });
89
+
90
+ return this.sendAuthCode(requestTokenURL, accessTokenRequest);
91
+ }
92
+
93
+ public async createAccessTokenRequest(opts: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
94
+ const { asOpts, pin, codeVerifier, code, redirectUri } = opts;
95
+ const credentialOfferRequest = opts.credentialOffer
96
+ ? determineSpecVersionFromOffer(opts.credentialOffer as CredentialOfferPayloadV1_0_11).valueOf() <= OpenId4VCIVersion.VER_1_0_11.valueOf()
97
+ ? await toUniformCredentialOfferRequestV1_0_11(opts.credentialOffer as CredentialOfferV1_0_11)
98
+ : await toUniformCredentialOfferRequest(opts.credentialOffer as CredentialOfferV1_0_13)
99
+ : undefined;
100
+ const request: Partial<AccessTokenRequest> = {};
101
+
102
+ if (asOpts?.clientId) {
103
+ request.client_id = asOpts.clientId;
104
+ }
105
+
106
+ if (credentialOfferRequest?.supportedFlows.includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
107
+ this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest.credential_offer), pin);
108
+ request.user_pin = pin;
109
+
110
+ request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE;
111
+ // we actually know it is there because of the isPreAuthCode call
112
+ request[PRE_AUTH_CODE_LITERAL] =
113
+ credentialOfferRequest?.credential_offer.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.[PRE_AUTH_CODE_LITERAL];
114
+
115
+ return request as AccessTokenRequest;
116
+ }
117
+
118
+ if (!credentialOfferRequest || credentialOfferRequest.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW)) {
119
+ request.grant_type = GrantTypes.AUTHORIZATION_CODE;
120
+ request.code = code;
121
+ request.redirect_uri = redirectUri;
122
+
123
+ if (codeVerifier) {
124
+ request.code_verifier = codeVerifier;
125
+ }
126
+
127
+ return request as AccessTokenRequest;
128
+ }
129
+
130
+ throw new Error('Credential offer request does not follow neither pre-authorized code nor authorization code flow requirements.');
131
+ }
132
+
133
+ private assertPreAuthorizedGrantType(grantType: GrantTypes): void {
134
+ if (GrantTypes.PRE_AUTHORIZED_CODE !== grantType) {
135
+ throw new Error("grant type must be 'urn:ietf:params:oauth:grant-type:pre-authorized_code'");
136
+ }
137
+ }
138
+
139
+ private assertAuthorizationGrantType(grantType: GrantTypes): void {
140
+ if (GrantTypes.AUTHORIZATION_CODE !== grantType) {
141
+ throw new Error("grant type must be 'authorization_code'");
142
+ }
143
+ }
144
+
145
+ private isPinRequiredValue(requestPayload: UniformCredentialOfferPayload): boolean {
146
+ let isPinRequired = false;
147
+ if (!requestPayload) {
148
+ throw new Error(TokenErrorResponse.invalid_request);
149
+ }
150
+ const issuer = getIssuerFromCredentialOfferPayload(requestPayload);
151
+ if (requestPayload.grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']) {
152
+ isPinRequired = requestPayload.grants['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.user_pin_required ?? false;
153
+ }
154
+ debug(`Pin required for issuer ${issuer}: ${isPinRequired}`);
155
+ return isPinRequired;
156
+ }
157
+
158
+ private assertNumericPin(isPinRequired?: boolean, pin?: string): void {
159
+ if (isPinRequired) {
160
+ if (!pin || !/^\d{1,8}$/.test(pin)) {
161
+ debug(`Pin is not 1 to 8 digits long`);
162
+ throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.');
163
+ }
164
+ } else if (pin) {
165
+ debug(`Pin set, whilst not required`);
166
+ throw new Error('Cannot set a pin, when the pin is not required.');
167
+ }
168
+ }
169
+
170
+ private assertNonEmptyPreAuthorizedCode(accessTokenRequest: AccessTokenRequest): void {
171
+ if (!accessTokenRequest[PRE_AUTH_CODE_LITERAL]) {
172
+ debug(`No pre-authorized code present, whilst it is required`);
173
+ throw new Error('Pre-authorization must be proven by presenting the pre-authorized code. Code must be present.');
174
+ }
175
+ }
176
+
177
+ private assertNonEmptyCodeVerifier(accessTokenRequest: AccessTokenRequest): void {
178
+ if (!accessTokenRequest.code_verifier) {
179
+ debug('No code_verifier present, whilst it is required');
180
+ throw new Error('Authorization flow requires the code_verifier to be present');
181
+ }
182
+ }
183
+
184
+ private assertNonEmptyCode(accessTokenRequest: AccessTokenRequest): void {
185
+ if (!accessTokenRequest.code) {
186
+ debug('No code present, whilst it is required');
187
+ throw new Error('Authorization flow requires the code to be present');
188
+ }
189
+ }
190
+ private validate(accessTokenRequest: AccessTokenRequest, isPinRequired?: boolean): void {
191
+ if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
192
+ this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
193
+ this.assertNonEmptyPreAuthorizedCode(accessTokenRequest);
194
+ this.assertNumericPin(isPinRequired, accessTokenRequest.user_pin);
195
+ } else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) {
196
+ this.assertAuthorizationGrantType(accessTokenRequest.grant_type);
197
+ this.assertNonEmptyCodeVerifier(accessTokenRequest);
198
+ this.assertNonEmptyCode(accessTokenRequest);
199
+ } else {
200
+ this.throwNotSupportedFlow();
201
+ }
202
+ }
203
+
204
+ private async sendAuthCode(requestTokenURL: string, accessTokenRequest: AccessTokenRequest): Promise<OpenIDResponse<AccessTokenResponse>> {
205
+ return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest, { mode: JsonURIMode.X_FORM_WWW_URLENCODED }));
206
+ }
207
+
208
+ public static determineTokenURL({
209
+ asOpts,
210
+ issuerOpts,
211
+ metadata,
212
+ }: {
213
+ asOpts?: AuthorizationServerOpts;
214
+ issuerOpts?: IssuerOpts;
215
+ metadata?: EndpointMetadata;
216
+ }): string {
217
+ if (!asOpts && !metadata?.token_endpoint && !issuerOpts) {
218
+ throw new Error('Cannot determine token URL if no issuer, metadata and no Authorization Server values are present');
219
+ }
220
+ let url;
221
+ if (asOpts && asOpts.as) {
222
+ url = this.creatTokenURLFromURL(asOpts.as, asOpts?.allowInsecureEndpoints, asOpts.tokenEndpoint);
223
+ } else if (metadata?.token_endpoint) {
224
+ url = metadata.token_endpoint;
225
+ } else {
226
+ if (!issuerOpts?.issuer) {
227
+ throw Error('Either authorization server options, a token endpoint or issuer options are required at this point');
228
+ }
229
+ url = this.creatTokenURLFromURL(issuerOpts.issuer, asOpts?.allowInsecureEndpoints, issuerOpts.tokenEndpoint);
230
+ }
231
+
232
+ if (!url || !ObjectUtils.isString(url)) {
233
+ throw new Error('No authorization server token URL present. Cannot acquire access token');
234
+ }
235
+ debug(`Token endpoint determined to be ${url}`);
236
+ return url;
237
+ }
238
+
239
+ private static creatTokenURLFromURL(url: string, allowInsecureEndpoints?: boolean, tokenEndpoint?: string): string {
240
+ if (allowInsecureEndpoints !== true && url.startsWith('http:')) {
241
+ throw Error(
242
+ `Unprotected token endpoints are not allowed ${url}. Use the 'allowInsecureEndpoints' param if you really need this for dev/testing!`,
243
+ );
244
+ }
245
+ const hostname = url.replace(/https?:\/\//, '').replace(/\/$/, '');
246
+ const endpoint = tokenEndpoint ? (tokenEndpoint.startsWith('/') ? tokenEndpoint : tokenEndpoint.substring(1)) : '/token';
247
+ const scheme = url.split('://')[0];
248
+ return `${scheme ? scheme + '://' : 'https://'}${hostname}${endpoint}`;
249
+ }
250
+
251
+ private throwNotSupportedFlow(): void {
252
+ debug(`Only pre-authorized or authorization code flows supported.`);
253
+ throw new Error('Only pre-authorized-code or authorization code flows are supported');
254
+ }
255
+ }
@@ -3,32 +3,46 @@ import {
3
3
  AuthorizationRequestOpts,
4
4
  CodeChallengeMethod,
5
5
  convertJsonToURI,
6
+ CredentialOfferFormat,
7
+ CredentialOfferPayloadV1_0_13,
6
8
  CredentialOfferRequestWithBaseUrl,
7
9
  CredentialSupported,
8
- EndpointMetadataResult,
10
+ determineSpecVersionFromOffer,
11
+ EndpointMetadataResultV1_0_13,
9
12
  formPost,
10
13
  JsonURIMode,
14
+ OpenId4VCIVersion,
11
15
  PARMode,
12
16
  PKCEOpts,
13
- PushedAuthorizationResponse,
14
- ResponseType,
15
- } from '@sphereon/oid4vci-common';
17
+ PushedAuthorizationResponse,
18
+ ResponseType
19
+ } from '@sphereon/oid4vci-common'
16
20
  import Debug from 'debug';
17
21
 
18
22
  const debug = Debug('sphereon:oid4vci');
19
23
 
24
+ function filterSupportedCredentials(
25
+ credentialOffer: CredentialOfferPayloadV1_0_13,
26
+ credentialsSupported?: Record<string, CredentialSupported>,
27
+ ): CredentialSupported[] {
28
+ if (!credentialOffer.credential_configuration_ids || !credentialsSupported) {
29
+ return [];
30
+ }
31
+ return credentialOffer.credential_configuration_ids.map((id) => credentialsSupported[id]).filter((cred) => cred !== undefined);
32
+ }
33
+
20
34
  export const createAuthorizationRequestUrl = async ({
21
35
  pkce,
22
36
  endpointMetadata,
23
37
  authorizationRequest,
24
38
  credentialOffer,
25
- credentialsSupported,
39
+ credentialConfigurationSupported,
26
40
  }: {
27
41
  pkce: PKCEOpts;
28
- endpointMetadata: EndpointMetadataResult;
42
+ endpointMetadata: EndpointMetadataResultV1_0_13;
29
43
  authorizationRequest: AuthorizationRequestOpts;
30
44
  credentialOffer?: CredentialOfferRequestWithBaseUrl;
31
- credentialsSupported?: CredentialSupported[];
45
+ credentialConfigurationSupported?: Record<string, CredentialSupported>;
32
46
  }): Promise<string> => {
33
47
  const { redirectUri, clientId } = authorizationRequest;
34
48
  let { scope, authorizationDetails } = authorizationRequest;
@@ -41,13 +55,19 @@ export const createAuthorizationRequestUrl = async ({
41
55
  if (!credentialOffer) {
42
56
  throw Error('Please provide a scope or authorization_details if no credential offer is present');
43
57
  }
44
- const creds = credentialOffer.credential_offer.credentials;
58
+ if ('credentials' in credentialOffer.credential_offer) {
59
+ throw new Error('CredentialOffer format is wrong.');
60
+ }
61
+ const creds: (CredentialSupported | CredentialOfferFormat | string)[] =
62
+ determineSpecVersionFromOffer(credentialOffer.credential_offer) === OpenId4VCIVersion.VER_1_0_13
63
+ ? filterSupportedCredentials(credentialOffer.credential_offer as CredentialOfferPayloadV1_0_13, credentialConfigurationSupported)
64
+ : [];
45
65
 
46
66
  // FIXME: complains about VCT for sd-jwt
47
67
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
48
68
  // @ts-ignore
49
69
  authorizationDetails = creds
50
- .flatMap((cred) => (typeof cred === 'string' ? credentialsSupported : (cred as CredentialSupported)))
70
+ .flatMap((cred) => cred as CredentialSupported)
51
71
  .filter((cred) => !!cred)
52
72
  .map((cred) => {
53
73
  return {
@@ -124,7 +144,7 @@ export const createAuthorizationRequestUrl = async ({
124
144
  };
125
145
 
126
146
  const handleAuthorizationDetails = (
127
- endpointMetadata: EndpointMetadataResult,
147
+ endpointMetadata: EndpointMetadataResultV1_0_13,
128
148
  authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
129
149
  ): AuthorizationDetails | AuthorizationDetails[] | undefined => {
130
150
  if (authorizationDetails) {
@@ -143,7 +163,7 @@ const handleAuthorizationDetails = (
143
163
  return authorizationDetails;
144
164
  };
145
165
 
146
- const handleLocations = (endpointMetadata: EndpointMetadataResult, authorizationDetails: AuthorizationDetails) => {
166
+ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_13, authorizationDetails: AuthorizationDetails) => {
147
167
  if (typeof authorizationDetails === 'string') {
148
168
  // backwards compat for older versions of the lib
149
169
  return authorizationDetails;
@@ -0,0 +1,167 @@
1
+ import {
2
+ AuthorizationDetails,
3
+ AuthorizationRequestOpts,
4
+ CodeChallengeMethod,
5
+ convertJsonToURI,
6
+ CredentialSupported,
7
+ CredentialOfferFormat,
8
+ CredentialOfferPayloadV1_0_11,
9
+ CredentialOfferRequestWithBaseUrl,
10
+ EndpointMetadataResultV1_0_11,
11
+ formPost,
12
+ JsonURIMode,
13
+ PARMode,
14
+ PKCEOpts,
15
+ PushedAuthorizationResponse,
16
+ ResponseType,
17
+ } from '@sphereon/oid4vci-common';
18
+ import Debug from 'debug';
19
+
20
+ const debug = Debug('sphereon:oid4vci');
21
+
22
+ export const createAuthorizationRequestUrlV1_0_11 = async ({
23
+ pkce,
24
+ endpointMetadata,
25
+ authorizationRequest,
26
+ credentialOffer,
27
+ credentialsSupported,
28
+ }: {
29
+ pkce: PKCEOpts;
30
+ endpointMetadata: EndpointMetadataResultV1_0_11;
31
+ authorizationRequest: AuthorizationRequestOpts;
32
+ credentialOffer?: CredentialOfferRequestWithBaseUrl;
33
+ credentialsSupported?: CredentialSupported[];
34
+ }): Promise<string> => {
35
+ const { redirectUri, clientId } = authorizationRequest;
36
+ let { scope, authorizationDetails } = authorizationRequest;
37
+ const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
38
+ ? PARMode.REQUIRE
39
+ : authorizationRequest.parMode ?? PARMode.AUTO;
40
+ // Scope and authorization_details can be used in the same authorization request
41
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
42
+ if (!scope && !authorizationDetails) {
43
+ if (!credentialOffer) {
44
+ throw Error('Please provide a scope or authorization_details if no credential offer is present');
45
+ }
46
+ const creds: (CredentialOfferFormat | string)[] = (credentialOffer.credential_offer as CredentialOfferPayloadV1_0_11).credentials;
47
+
48
+ // FIXME: complains about VCT for sd-jwt
49
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
50
+ // @ts-ignore
51
+ authorizationDetails = creds
52
+ .flatMap((cred) =>
53
+ typeof cred === 'string' && credentialsSupported ? Object.values(credentialsSupported) : (cred as CredentialSupported),
54
+ )
55
+ .filter((cred) => !!cred)
56
+ .map((cred) => {
57
+ return {
58
+ ...cred,
59
+ type: 'openid_credential',
60
+ locations: [endpointMetadata.issuer],
61
+
62
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
63
+ // @ts-ignore
64
+ format: cred!.format,
65
+ } satisfies AuthorizationDetails;
66
+ });
67
+ if (!authorizationDetails || authorizationDetails.length === 0) {
68
+ throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
69
+ }
70
+ }
71
+ if (!endpointMetadata?.authorization_endpoint) {
72
+ throw Error('Server metadata does not contain authorization endpoint');
73
+ }
74
+ const parEndpoint = endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint;
75
+
76
+ // add 'openid' scope if not present
77
+ if (!scope?.includes('openid')) {
78
+ scope = ['openid', scope].filter((s) => !!s).join(' ');
79
+ }
80
+
81
+ let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
82
+ response_type: ResponseType.AUTH_CODE,
83
+ ...(!pkce.disabled && {
84
+ code_challenge_method: pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
85
+ code_challenge: pkce.codeChallenge,
86
+ }),
87
+ authorization_details: JSON.stringify(handleAuthorizationDetailsV1_0_11(endpointMetadata, authorizationDetails)),
88
+ ...(redirectUri && { redirect_uri: redirectUri }),
89
+ ...(clientId && { client_id: clientId }),
90
+ ...(credentialOffer?.issuerState && { issuer_state: credentialOffer.issuerState }),
91
+ scope,
92
+ };
93
+
94
+ if (!parEndpoint && parMode === PARMode.REQUIRE) {
95
+ throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`);
96
+ } else if (parEndpoint && parMode !== PARMode.NEVER) {
97
+ debug(`USING PAR with endpoint ${parEndpoint}`);
98
+ const parResponse = await formPost<PushedAuthorizationResponse>(
99
+ parEndpoint,
100
+ convertJsonToURI(queryObj, {
101
+ mode: JsonURIMode.X_FORM_WWW_URLENCODED,
102
+ uriTypeProperties: ['client_id', 'request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
103
+ }),
104
+ { contentType: 'application/x-www-form-urlencoded', accept: 'application/json' },
105
+ );
106
+ if (parResponse.errorBody || !parResponse.successBody) {
107
+ console.log(JSON.stringify(parResponse.errorBody));
108
+ console.log('Falling back to regular request URI, since PAR failed');
109
+ if (parMode === PARMode.REQUIRE) {
110
+ throw Error(`PAR error: ${parResponse.origResponse.statusText}`);
111
+ }
112
+ } else {
113
+ debug(`PAR response: ${(parResponse.successBody, null, 2)}`);
114
+ queryObj = { request_uri: parResponse.successBody.request_uri };
115
+ }
116
+ }
117
+
118
+ debug(`Object that will become query params: ` + JSON.stringify(queryObj, null, 2));
119
+ const url = convertJsonToURI(queryObj, {
120
+ baseUrl: endpointMetadata.authorization_endpoint,
121
+ uriTypeProperties: ['client_id', 'request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
122
+ // arrayTypeProperties: ['authorization_details'],
123
+ mode: JsonURIMode.X_FORM_WWW_URLENCODED,
124
+ // We do not add the version here, as this always needs to be form encoded
125
+ });
126
+ debug(`Authorization Request URL: ${url}`);
127
+ return url;
128
+ };
129
+
130
+ const handleAuthorizationDetailsV1_0_11 = (
131
+ endpointMetadata: EndpointMetadataResultV1_0_11,
132
+ authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
133
+ ): AuthorizationDetails | AuthorizationDetails[] | undefined => {
134
+ if (authorizationDetails) {
135
+ if (typeof authorizationDetails === 'string') {
136
+ // backwards compat for older versions of the lib
137
+ return authorizationDetails;
138
+ }
139
+ if (Array.isArray(authorizationDetails)) {
140
+ return authorizationDetails
141
+ .filter((value) => typeof value !== 'string')
142
+ .map((value) => handleLocations(endpointMetadata, typeof value === 'string' ? value : { ...value }));
143
+ } else {
144
+ return handleLocations(endpointMetadata, { ...authorizationDetails });
145
+ }
146
+ }
147
+ return authorizationDetails;
148
+ };
149
+
150
+ const handleLocations = (endpointMetadata: EndpointMetadataResultV1_0_11, authorizationDetails: AuthorizationDetails) => {
151
+ if (typeof authorizationDetails === 'string') {
152
+ // backwards compat for older versions of the lib
153
+ return authorizationDetails;
154
+ }
155
+ if (authorizationDetails && (endpointMetadata.credentialIssuerMetadata?.authorization_server || endpointMetadata.authorization_endpoint)) {
156
+ if (authorizationDetails.locations) {
157
+ if (Array.isArray(authorizationDetails.locations)) {
158
+ authorizationDetails.locations.push(endpointMetadata.issuer);
159
+ } else {
160
+ authorizationDetails.locations = [authorizationDetails.locations as string, endpointMetadata.issuer];
161
+ }
162
+ } else {
163
+ authorizationDetails.locations = [endpointMetadata.issuer];
164
+ }
165
+ }
166
+ return authorizationDetails;
167
+ };