@sphereon/oid4vci-client 0.10.3 → 0.10.4-next.119

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