@sphereon/oid4vci-client 0.2.0 → 0.4.1-unstable.247

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 (116) 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 +226 -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/CredentialOffer.d.ts +6 -0
  12. package/dist/CredentialOffer.d.ts.map +1 -0
  13. package/dist/CredentialOffer.js +49 -0
  14. package/dist/CredentialOffer.js.map +1 -0
  15. package/dist/CredentialRequestClient.d.ts +29 -0
  16. package/dist/CredentialRequestClient.d.ts.map +1 -0
  17. package/dist/CredentialRequestClient.js +63 -0
  18. package/dist/CredentialRequestClient.js.map +1 -0
  19. package/dist/CredentialRequestClientBuilderV1_0_09.d.ts +29 -0
  20. package/dist/CredentialRequestClientBuilderV1_0_09.d.ts.map +1 -0
  21. package/dist/CredentialRequestClientBuilderV1_0_09.js +63 -0
  22. package/dist/CredentialRequestClientBuilderV1_0_09.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 +72 -0
  28. package/dist/OpenID4VCIClient.d.ts.map +1 -0
  29. package/dist/OpenID4VCIClient.js +361 -0
  30. package/dist/OpenID4VCIClient.js.map +1 -0
  31. package/dist/ProofOfPossessionBuilder.d.ts +35 -0
  32. package/dist/ProofOfPossessionBuilder.d.ts.map +1 -0
  33. package/dist/ProofOfPossessionBuilder.js +120 -0
  34. package/dist/ProofOfPossessionBuilder.js.map +1 -0
  35. package/dist/{main/lib/functions → functions}/Encoding.d.ts +20 -17
  36. package/dist/functions/Encoding.d.ts.map +1 -0
  37. package/dist/functions/Encoding.js +144 -0
  38. package/dist/functions/Encoding.js.map +1 -0
  39. package/dist/functions/HttpUtils.d.ts +24 -0
  40. package/dist/functions/HttpUtils.d.ts.map +1 -0
  41. package/dist/functions/HttpUtils.js +93 -0
  42. package/dist/functions/HttpUtils.js.map +1 -0
  43. package/dist/functions/ProofUtil.d.ts +29 -0
  44. package/dist/functions/ProofUtil.d.ts.map +1 -0
  45. package/dist/functions/ProofUtil.js +103 -0
  46. package/dist/functions/ProofUtil.js.map +1 -0
  47. package/dist/functions/index.d.ts +4 -0
  48. package/dist/functions/index.d.ts.map +1 -0
  49. package/dist/{main/lib/functions → functions}/index.js +20 -20
  50. package/dist/functions/index.js.map +1 -0
  51. package/dist/index.d.ts +9 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/{main/lib/index.js → index.js} +25 -24
  54. package/dist/index.js.map +1 -0
  55. package/lib/AccessTokenClient.ts +270 -0
  56. package/lib/AuthorizationDetailsBuilder.ts +46 -0
  57. package/lib/CredentialOffer.ts +55 -0
  58. package/lib/CredentialRequestClient.ts +77 -0
  59. package/lib/CredentialRequestClientBuilderV1_0_09.ts +99 -0
  60. package/lib/MetadataClient.ts +147 -0
  61. package/lib/OpenID4VCIClient.ts +477 -0
  62. package/lib/ProofOfPossessionBuilder.ts +156 -0
  63. package/lib/__tests__/AccessTokenClient.spec.ts +221 -0
  64. package/lib/__tests__/AuthorizationDetailsBuilder.spec.ts +65 -0
  65. package/lib/__tests__/AuthzFlowType.spec.ts +39 -0
  66. package/lib/__tests__/CredentialRequestClient.spec.ts +261 -0
  67. package/lib/__tests__/CredentialRequestClientBuilder.spec.ts +103 -0
  68. package/lib/__tests__/HttpUtils.spec.ts +37 -0
  69. package/lib/__tests__/IT.spec.ts +155 -0
  70. package/lib/__tests__/IssuanceInitiation.spec.ts +37 -0
  71. package/lib/__tests__/JsonURIConversions.spec.ts +86 -0
  72. package/lib/__tests__/MetadataClient.spec.ts +198 -0
  73. package/lib/__tests__/MetadataMocks.ts +428 -0
  74. package/lib/__tests__/OpenID4VCIClient.spec.ts +166 -0
  75. package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +112 -0
  76. package/lib/__tests__/ProofOfPossessionBuilder.spec.ts +109 -0
  77. package/lib/__tests__/data/VciDataFixtures.ts +744 -0
  78. package/lib/functions/Encoding.ts +138 -0
  79. package/lib/functions/HttpUtils.ts +106 -0
  80. package/lib/functions/ProofUtil.ts +128 -0
  81. package/{dist/main/lib/functions/index.d.ts → lib/functions/index.ts} +3 -3
  82. package/lib/index.ts +8 -0
  83. package/package.json +68 -71
  84. package/CHANGELOG.md +0 -21
  85. package/dist/main/index.d.ts +0 -1
  86. package/dist/main/index.js +0 -18
  87. package/dist/main/lib/AccessTokenClient.d.ts +0 -20
  88. package/dist/main/lib/AccessTokenClient.js +0 -141
  89. package/dist/main/lib/CredentialRequestClient.d.ts +0 -31
  90. package/dist/main/lib/CredentialRequestClient.js +0 -66
  91. package/dist/main/lib/CredentialRequestClientBuilder.d.ts +0 -21
  92. package/dist/main/lib/CredentialRequestClientBuilder.js +0 -56
  93. package/dist/main/lib/IssuanceInitiation.d.ts +0 -5
  94. package/dist/main/lib/IssuanceInitiation.js +0 -29
  95. package/dist/main/lib/MetadataClient.js +0 -127
  96. package/dist/main/lib/functions/Encoding.js +0 -138
  97. package/dist/main/lib/functions/HttpUtils.d.ts +0 -17
  98. package/dist/main/lib/functions/HttpUtils.js +0 -133
  99. package/dist/main/lib/functions/ProofUtil.d.ts +0 -9
  100. package/dist/main/lib/functions/ProofUtil.js +0 -76
  101. package/dist/main/lib/index.d.ts +0 -7
  102. package/dist/main/lib/types/Authorization.types.d.ts +0 -66
  103. package/dist/main/lib/types/Authorization.types.js +0 -35
  104. package/dist/main/lib/types/CredentialIssuance.types.d.ts +0 -88
  105. package/dist/main/lib/types/CredentialIssuance.types.js +0 -8
  106. package/dist/main/lib/types/Generic.types.d.ts +0 -19
  107. package/dist/main/lib/types/Generic.types.js +0 -11
  108. package/dist/main/lib/types/OAuth2ASMetadata.d.ts +0 -37
  109. package/dist/main/lib/types/OAuth2ASMetadata.js +0 -3
  110. package/dist/main/lib/types/OID4VCIServerMetadata.d.ts +0 -65
  111. package/dist/main/lib/types/OID4VCIServerMetadata.js +0 -3
  112. package/dist/main/lib/types/Oidc4vciErrors.d.ts +0 -3
  113. package/dist/main/lib/types/Oidc4vciErrors.js +0 -7
  114. package/dist/main/lib/types/index.d.ts +0 -6
  115. package/dist/main/lib/types/index.js +0 -23
  116. package/dist/main/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,270 @@
1
+ import {
2
+ AccessTokenRequest,
3
+ AccessTokenRequestOpts,
4
+ AccessTokenResponse,
5
+ AuthorizationServerOpts,
6
+ CredentialOfferPayload,
7
+ CredentialOfferPayloadV1_0_09,
8
+ CredentialOfferPayloadV1_0_11,
9
+ EndpointMetadata,
10
+ getIssuerFromCredentialOfferPayload,
11
+ GrantTypes,
12
+ isCredentialOfferV1_0_09,
13
+ isCredentialOfferV1_0_11,
14
+ IssuerOpts,
15
+ OpenIDResponse,
16
+ PRE_AUTH_CODE_LITERAL,
17
+ TokenErrorResponse,
18
+ } from '@sphereon/oid4vci-common';
19
+ import { ObjectUtils } from '@sphereon/ssi-types';
20
+ import Debug from 'debug';
21
+
22
+ import { MetadataClient } from './MetadataClient';
23
+ import { convertJsonToURI, formPost } from './functions';
24
+
25
+ const debug = Debug('sphereon:openid4vci:token');
26
+
27
+ export class AccessTokenClient {
28
+ public async acquireAccessToken({
29
+ credentialOffer,
30
+ asOpts,
31
+ pin,
32
+ codeVerifier,
33
+ code,
34
+ redirectUri,
35
+ metadata,
36
+ }: AccessTokenRequestOpts): Promise<OpenIDResponse<AccessTokenResponse>> {
37
+ const { request } = credentialOffer;
38
+
39
+ const isPinRequired = this.isPinRequiredValue(request);
40
+ const issuerOpts = {
41
+ issuer: getIssuerFromCredentialOfferPayload(request) ? (getIssuerFromCredentialOfferPayload(request) as string) : (metadata?.issuer as string),
42
+ };
43
+
44
+ return await this.acquireAccessTokenUsingRequest({
45
+ accessTokenRequest: await this.createAccessTokenRequest({
46
+ credentialOffer,
47
+ asOpts,
48
+ codeVerifier,
49
+ code,
50
+ redirectUri,
51
+ pin,
52
+ }),
53
+ isPinRequired,
54
+ metadata,
55
+ asOpts,
56
+ issuerOpts,
57
+ });
58
+ }
59
+
60
+ public async acquireAccessTokenUsingRequest({
61
+ accessTokenRequest,
62
+ isPinRequired,
63
+ metadata,
64
+ asOpts,
65
+ issuerOpts,
66
+ }: {
67
+ accessTokenRequest: AccessTokenRequest;
68
+ isPinRequired?: boolean;
69
+ metadata?: EndpointMetadata;
70
+ asOpts?: AuthorizationServerOpts;
71
+ issuerOpts?: IssuerOpts;
72
+ }): Promise<OpenIDResponse<AccessTokenResponse>> {
73
+ this.validate(accessTokenRequest, isPinRequired);
74
+ const requestTokenURL = AccessTokenClient.determineTokenURL({
75
+ asOpts,
76
+ issuerOpts,
77
+ metadata: metadata
78
+ ? metadata
79
+ : issuerOpts?.fetchMetadata
80
+ ? await MetadataClient.retrieveAllMetadata(issuerOpts.issuer, { errorOnNotFound: false })
81
+ : undefined,
82
+ });
83
+ return this.sendAuthCode(requestTokenURL, accessTokenRequest);
84
+ }
85
+
86
+ public async createAccessTokenRequest({
87
+ credentialOffer,
88
+ asOpts,
89
+ pin,
90
+ codeVerifier,
91
+ code,
92
+ redirectUri,
93
+ }: AccessTokenRequestOpts): Promise<AccessTokenRequest> {
94
+ const credentialOfferRequest = credentialOffer.request;
95
+ const request: Partial<AccessTokenRequest> = {};
96
+ if (asOpts?.clientId) {
97
+ request.client_id = asOpts.clientId;
98
+ }
99
+
100
+ this.assertNumericPin(this.isPinRequiredValue(credentialOfferRequest), pin);
101
+ request.user_pin = pin;
102
+
103
+ if (credentialOfferRequest[PRE_AUTH_CODE_LITERAL as keyof CredentialOfferPayload]) {
104
+ if (codeVerifier) {
105
+ throw new Error('Cannot pass a code_verifier when flow type is pre-authorized');
106
+ }
107
+ request.grant_type = GrantTypes.PRE_AUTHORIZED_CODE;
108
+ //todo: handle this for v11
109
+ request[PRE_AUTH_CODE_LITERAL] = (credentialOfferRequest as CredentialOfferPayloadV1_0_09)[PRE_AUTH_CODE_LITERAL];
110
+ }
111
+ if ('op_state' in credentialOfferRequest || 'issuer_state' in credentialOfferRequest) {
112
+ this.throwNotSupportedFlow();
113
+ request.grant_type = GrantTypes.AUTHORIZATION_CODE;
114
+ }
115
+ if (codeVerifier) {
116
+ request.code_verifier = codeVerifier;
117
+ request.code = code;
118
+ request.redirect_uri = redirectUri;
119
+ request.grant_type = GrantTypes.AUTHORIZATION_CODE;
120
+ }
121
+ //todo: handle this for v11
122
+ if (request.grant_type === GrantTypes.AUTHORIZATION_CODE && (credentialOfferRequest as CredentialOfferPayloadV1_0_09)[PRE_AUTH_CODE_LITERAL]) {
123
+ throw Error('A pre_authorized_code flow cannot have an op_state in the initiation request');
124
+ }
125
+
126
+ return request as AccessTokenRequest;
127
+ }
128
+
129
+ private assertPreAuthorizedGrantType(grantType: GrantTypes): void {
130
+ if (GrantTypes.PRE_AUTHORIZED_CODE !== grantType) {
131
+ throw new Error("grant type must be 'urn:ietf:params:oauth:grant-type:pre-authorized_code'");
132
+ }
133
+ }
134
+
135
+ private assertAuthorizationGrantType(grantType: GrantTypes): void {
136
+ if (GrantTypes.AUTHORIZATION_CODE !== grantType) {
137
+ throw new Error("grant type must be 'authorization_code'");
138
+ }
139
+ }
140
+
141
+ private isPinRequiredValue(requestPayload: CredentialOfferPayload): boolean {
142
+ let isPinRequired = false;
143
+ if (!requestPayload) {
144
+ throw new Error(TokenErrorResponse.invalid_request);
145
+ }
146
+ const issuer = getIssuerFromCredentialOfferPayload(requestPayload);
147
+ if (isCredentialOfferV1_0_09(requestPayload)) {
148
+ requestPayload = requestPayload as CredentialOfferPayloadV1_0_09;
149
+ if (typeof requestPayload.user_pin_required === 'string') {
150
+ isPinRequired = requestPayload.user_pin_required.toLowerCase() === 'true';
151
+ } else if (typeof requestPayload.user_pin_required === 'boolean') {
152
+ isPinRequired = requestPayload.user_pin_required;
153
+ }
154
+ } else if (isCredentialOfferV1_0_11(requestPayload)) {
155
+ requestPayload = requestPayload as CredentialOfferPayloadV1_0_11;
156
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
157
+ if ('grants' in requestPayload && 'urn:ietf:params:oauth:grant-type:pre-authorized_code' in requestPayload.grants!) {
158
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159
+ isPinRequired = requestPayload!.grants!['urn:ietf:params:oauth:grant-type:pre-authorized_code']!.user_pin_required;
160
+ }
161
+ }
162
+ debug(`Pin required for issuer ${issuer}: ${isPinRequired}`);
163
+ return isPinRequired;
164
+ }
165
+
166
+ private assertNumericPin(isPinRequired?: boolean, pin?: string): void {
167
+ if (isPinRequired) {
168
+ if (!pin || !/^\d{1,8}$/.test(pin)) {
169
+ debug(`Pin is not 1 to 8 digits long`);
170
+ throw new Error('A valid pin consisting of maximal 8 numeric characters must be present.');
171
+ }
172
+ } else if (pin) {
173
+ debug(`Pin set, whilst not required`);
174
+ throw new Error('Cannot set a pin, when the pin is not required.');
175
+ }
176
+ }
177
+
178
+ private assertNonEmptyPreAuthorizedCode(accessTokenRequest: AccessTokenRequest): void {
179
+ if (!accessTokenRequest[PRE_AUTH_CODE_LITERAL]) {
180
+ debug(`No pre-authorized code present, whilst it is required`);
181
+ throw new Error('Pre-authorization must be proven by presenting the pre-authorized code. Code must be present.');
182
+ }
183
+ }
184
+
185
+ private assertNonEmptyCodeVerifier(accessTokenRequest: AccessTokenRequest): void {
186
+ if (!accessTokenRequest.code_verifier) {
187
+ debug('No code_verifier present, whilst it is required');
188
+ throw new Error('Authorization flow requires the code_verifier to be present');
189
+ }
190
+ }
191
+
192
+ private assertNonEmptyCode(accessTokenRequest: AccessTokenRequest): void {
193
+ if (!accessTokenRequest.code) {
194
+ debug('No code present, whilst it is required');
195
+ throw new Error('Authorization flow requires the code to be present');
196
+ }
197
+ }
198
+
199
+ private assertNonEmptyRedirectUri(accessTokenRequest: AccessTokenRequest): void {
200
+ if (!accessTokenRequest.redirect_uri) {
201
+ debug('No redirect_uri present, whilst it is required');
202
+ throw new Error('Authorization flow requires the redirect_uri to be present');
203
+ }
204
+ }
205
+
206
+ private validate(accessTokenRequest: AccessTokenRequest, isPinRequired?: boolean): void {
207
+ if (accessTokenRequest.grant_type === GrantTypes.PRE_AUTHORIZED_CODE) {
208
+ this.assertPreAuthorizedGrantType(accessTokenRequest.grant_type);
209
+ this.assertNonEmptyPreAuthorizedCode(accessTokenRequest);
210
+ this.assertNumericPin(isPinRequired, accessTokenRequest.user_pin);
211
+ } else if (accessTokenRequest.grant_type === GrantTypes.AUTHORIZATION_CODE) {
212
+ this.assertAuthorizationGrantType(accessTokenRequest.grant_type);
213
+ this.assertNonEmptyCodeVerifier(accessTokenRequest);
214
+ this.assertNonEmptyCode(accessTokenRequest);
215
+ this.assertNonEmptyRedirectUri(accessTokenRequest);
216
+ } else {
217
+ this.throwNotSupportedFlow;
218
+ }
219
+ }
220
+
221
+ private async sendAuthCode(requestTokenURL: string, accessTokenRequest: AccessTokenRequest): Promise<OpenIDResponse<AccessTokenResponse>> {
222
+ return await formPost(requestTokenURL, convertJsonToURI(accessTokenRequest));
223
+ }
224
+
225
+ public static determineTokenURL({
226
+ asOpts,
227
+ issuerOpts,
228
+ metadata,
229
+ }: {
230
+ asOpts?: AuthorizationServerOpts;
231
+ issuerOpts?: IssuerOpts;
232
+ metadata?: EndpointMetadata;
233
+ }): string {
234
+ if (!asOpts && !metadata?.token_endpoint && !issuerOpts) {
235
+ throw new Error('Cannot determine token URL if no issuer, metadata and no Authorization Server values are present');
236
+ }
237
+ let url;
238
+ if (asOpts && asOpts.as) {
239
+ url = this.creatTokenURLFromURL(asOpts.as, asOpts?.allowInsecureEndpoints, asOpts.tokenEndpoint);
240
+ } else if (metadata?.token_endpoint) {
241
+ url = metadata.token_endpoint;
242
+ } else {
243
+ if (!issuerOpts) {
244
+ throw Error('Either authorization server options, a token endpoint or issuer options are required at this point');
245
+ }
246
+ url = this.creatTokenURLFromURL(issuerOpts.issuer, asOpts?.allowInsecureEndpoints, issuerOpts.tokenEndpoint);
247
+ }
248
+
249
+ if (!url || !ObjectUtils.isString(url)) {
250
+ throw new Error('No authorization server token URL present. Cannot acquire access token');
251
+ }
252
+ debug(`Token endpoint determined to be ${url}`);
253
+ return url;
254
+ }
255
+
256
+ private static creatTokenURLFromURL(url: string, allowInsecureEndpoints?: boolean, tokenEndpoint?: string): string {
257
+ if (allowInsecureEndpoints !== true && url.startsWith('http://')) {
258
+ throw Error(`Unprotected token endpoints are not allowed ${url}`);
259
+ }
260
+ const hostname = url.replace(/https?:\/\//, '').replace(/\/$/, '');
261
+ const endpoint = tokenEndpoint ? (tokenEndpoint.startsWith('/') ? tokenEndpoint : tokenEndpoint.substring(1)) : '/token';
262
+ // We always require https
263
+ return `https://${hostname}${endpoint}`;
264
+ }
265
+
266
+ private throwNotSupportedFlow(): void {
267
+ debug(`Only pre-authorized flow supported.`);
268
+ throw new Error('Only pre-authorized-code flow is supported');
269
+ }
270
+ }
@@ -0,0 +1,46 @@
1
+ import { AuthorizationDetailsJwtVcJson, CredentialFormatEnum } from '@sphereon/oid4vci-common';
2
+
3
+ //todo: refactor this builder to be able to create ldp details as well
4
+ export class AuthorizationDetailsBuilder {
5
+ private readonly authorizationDetails: Partial<AuthorizationDetailsJwtVcJson>;
6
+
7
+ constructor() {
8
+ this.authorizationDetails = {};
9
+ }
10
+
11
+ withType(type: string): AuthorizationDetailsBuilder {
12
+ this.authorizationDetails.type = type;
13
+ return this;
14
+ }
15
+
16
+ withFormats(format: CredentialFormatEnum): AuthorizationDetailsBuilder {
17
+ this.authorizationDetails.format = format;
18
+ return this;
19
+ }
20
+
21
+ withLocations(locations: string[]): AuthorizationDetailsBuilder {
22
+ if (this.authorizationDetails.locations) {
23
+ this.authorizationDetails.locations.push(...locations);
24
+ } else {
25
+ this.authorizationDetails.locations = locations;
26
+ }
27
+ return this;
28
+ }
29
+
30
+ addLocation(location: string): AuthorizationDetailsBuilder {
31
+ if (this.authorizationDetails.locations) {
32
+ this.authorizationDetails.locations.push(location);
33
+ } else {
34
+ this.authorizationDetails.locations = [location];
35
+ }
36
+ return this;
37
+ }
38
+
39
+ //todo: we have to consider one thing, if this is a general purpose builder, we want to support ldp types here as well. and for that we need a few checks.
40
+ buildJwtVcJson(): AuthorizationDetailsJwtVcJson {
41
+ if (this.authorizationDetails.format && this.authorizationDetails.type) {
42
+ return this.authorizationDetails as AuthorizationDetailsJwtVcJson;
43
+ }
44
+ throw new Error('Type and format are required properties');
45
+ }
46
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ CredentialOfferPayload,
3
+ CredentialOfferPayloadV1_0_09,
4
+ CredentialOfferPayloadV1_0_11,
5
+ CredentialOfferRequestWithBaseUrl,
6
+ OpenId4VCIVersion,
7
+ } from '@sphereon/oid4vci-common';
8
+ import { determineSpecVersionFromURI } from '@sphereon/oid4vci-common';
9
+ import Debug from 'debug';
10
+
11
+ import { convertJsonToURI, convertURIToJsonObject } from './functions';
12
+
13
+ const debug = Debug('sphereon:openid4vci:initiation');
14
+
15
+ export class CredentialOffer {
16
+ public static fromURI(uri: string): CredentialOfferRequestWithBaseUrl {
17
+ debug(`issuance initiation URI: ${uri}`);
18
+ if (!uri.includes('?')) {
19
+ debug(`Invalid issuance initiation URI: ${uri}`);
20
+ throw new Error('Invalid Issuance Initiation Request Payload');
21
+ }
22
+ const baseUrl = uri.split('?')[0];
23
+ const version = determineSpecVersionFromURI(uri);
24
+ const issuanceInitiationRequest: CredentialOfferPayload =
25
+ version < OpenId4VCIVersion.VER_1_0_11
26
+ ? (convertURIToJsonObject(uri, {
27
+ arrayTypeProperties: ['credential_type'],
28
+ requiredProperties: ['issuer', 'credential_type'],
29
+ }) as CredentialOfferPayloadV1_0_09)
30
+ : (convertURIToJsonObject(uri, {
31
+ arrayTypeProperties: ['credentials'],
32
+ requiredProperties: ['credentials', 'credential_issuer'],
33
+ }) as CredentialOfferPayloadV1_0_11);
34
+
35
+ const request =
36
+ version < OpenId4VCIVersion.VER_1_0_11.valueOf()
37
+ ? (issuanceInitiationRequest as CredentialOfferPayloadV1_0_09)
38
+ : (issuanceInitiationRequest as CredentialOfferPayloadV1_0_11);
39
+
40
+ return {
41
+ baseUrl,
42
+ request,
43
+ version,
44
+ };
45
+ }
46
+
47
+ public static toURI(uri: CredentialOfferRequestWithBaseUrl): string {
48
+ const request = uri.request;
49
+ return convertJsonToURI(request, {
50
+ baseUrl: uri.baseUrl,
51
+ arrayTypeProperties: ['credential_type'],
52
+ uriTypeProperties: ['issuer', 'credential_type'],
53
+ });
54
+ }
55
+ }
@@ -0,0 +1,77 @@
1
+ import { CredentialRequest, CredentialResponse, OpenIDResponse, ProofOfPossession, URL_NOT_VALID } from '@sphereon/oid4vci-common';
2
+ import { CredentialFormat } from '@sphereon/ssi-types';
3
+ import Debug from 'debug';
4
+
5
+ import { CredentialRequestClientBuilderV1_0_09 } from './CredentialRequestClientBuilderV1_0_09';
6
+ import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
7
+ import { isValidURL, post } from './functions';
8
+
9
+ const debug = Debug('sphereon:openid4vci:credential');
10
+
11
+ export interface CredentialRequestOpts {
12
+ credentialEndpoint: string;
13
+ credentialType: string | string[];
14
+ format: CredentialFormat | CredentialFormat[];
15
+ proof: ProofOfPossession;
16
+ token: string;
17
+ }
18
+
19
+ export class CredentialRequestClient {
20
+ private readonly _credentialRequestOpts: Partial<CredentialRequestOpts>;
21
+
22
+ get credentialRequestOpts(): CredentialRequestOpts {
23
+ return this._credentialRequestOpts as CredentialRequestOpts;
24
+ }
25
+
26
+ public getCredentialEndpoint(): string {
27
+ return this.credentialRequestOpts.credentialEndpoint;
28
+ }
29
+
30
+ public constructor(builder: CredentialRequestClientBuilderV1_0_09) {
31
+ this._credentialRequestOpts = { ...builder };
32
+ }
33
+
34
+ public async acquireCredentialsUsingProof({
35
+ proofInput,
36
+ credentialType,
37
+ format,
38
+ }: {
39
+ proofInput: ProofOfPossessionBuilder | ProofOfPossession;
40
+ credentialType?: string | string[];
41
+ format?: CredentialFormat | CredentialFormat[];
42
+ }): Promise<OpenIDResponse<CredentialResponse>> {
43
+ const request = await this.createCredentialRequest({ proofInput, credentialType, format });
44
+ return await this.acquireCredentialsUsingRequest(request);
45
+ }
46
+
47
+ public async acquireCredentialsUsingRequest(request: CredentialRequest): Promise<OpenIDResponse<CredentialResponse>> {
48
+ const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
49
+ if (!isValidURL(credentialEndpoint)) {
50
+ debug(`Invalid credential endpoint: ${credentialEndpoint}`);
51
+ throw new Error(URL_NOT_VALID);
52
+ }
53
+ debug(`Acquiring credential(s) from: ${credentialEndpoint}`);
54
+ const requestToken: string = this.credentialRequestOpts.token;
55
+ const response: OpenIDResponse<CredentialResponse> = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken });
56
+ debug(`Credential endpoint ${credentialEndpoint} response:\r\n${response}`);
57
+ return response;
58
+ }
59
+
60
+ public async createCredentialRequest({
61
+ proofInput,
62
+ credentialType,
63
+ format,
64
+ }: {
65
+ proofInput: ProofOfPossessionBuilder | ProofOfPossession;
66
+ credentialType?: string | string[];
67
+ format?: CredentialFormat | CredentialFormat[];
68
+ }): Promise<CredentialRequest> {
69
+ const proof =
70
+ 'proof_type' in proofInput ? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession).build() : await proofInput.build();
71
+ return {
72
+ type: credentialType ? credentialType : this.credentialRequestOpts.credentialType,
73
+ format: format ? (format as string) : (this.credentialRequestOpts.format as string),
74
+ proof,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,99 @@
1
+ import {
2
+ AccessTokenResponse,
3
+ CredentialOfferPayload,
4
+ CredentialOfferPayloadV1_0_09,
5
+ CredentialOfferRequestWithBaseUrl,
6
+ EndpointMetadata,
7
+ getIssuerFromCredentialOfferPayload,
8
+ IssuerMetadata,
9
+ } from '@sphereon/oid4vci-common';
10
+ import { CredentialFormat } from '@sphereon/ssi-types';
11
+
12
+ import { CredentialRequestClient } from './CredentialRequestClient';
13
+ import { convertURIToJsonObject } from './functions';
14
+
15
+ export class CredentialRequestClientBuilderV1_0_09 {
16
+ credentialEndpoint?: string;
17
+ credentialType?: string | string[];
18
+ format?: CredentialFormat | CredentialFormat[];
19
+ token?: string;
20
+
21
+ public static fromURI({ uri, metadata }: { uri: string; metadata?: EndpointMetadata }): CredentialRequestClientBuilderV1_0_09 {
22
+ return CredentialRequestClientBuilderV1_0_09.fromCredentialOfferRequest({
23
+ request: convertURIToJsonObject(uri, {
24
+ arrayTypeProperties: ['credential_type'],
25
+ requiredProperties: ['issuer', 'credential_type'],
26
+ }) as CredentialOfferPayload,
27
+ metadata,
28
+ });
29
+ }
30
+
31
+ public static fromCredentialOfferRequest({
32
+ request,
33
+ metadata,
34
+ }: {
35
+ request: CredentialOfferPayload;
36
+ metadata?: EndpointMetadata;
37
+ }): CredentialRequestClientBuilderV1_0_09 {
38
+ const builder = new CredentialRequestClientBuilderV1_0_09();
39
+ const issuer = getIssuerFromCredentialOfferPayload(request)
40
+ ? (getIssuerFromCredentialOfferPayload(request) as string)
41
+ : (metadata?.issuer as string);
42
+ builder.withCredentialEndpoint(
43
+ metadata?.credential_endpoint ? metadata.credential_endpoint : issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`
44
+ );
45
+
46
+ //todo: This basically sets all types available during initiation. Probably the user only wants a subset. So do we want to do this?
47
+ //todo: handle this for v11
48
+ builder.withCredentialType((request as CredentialOfferPayloadV1_0_09).credential_type);
49
+
50
+ return builder;
51
+ }
52
+
53
+ public static fromCredentialOffer({
54
+ credentialOffer,
55
+ metadata,
56
+ }: {
57
+ credentialOffer: CredentialOfferRequestWithBaseUrl;
58
+ metadata?: EndpointMetadata;
59
+ }): CredentialRequestClientBuilderV1_0_09 {
60
+ return CredentialRequestClientBuilderV1_0_09.fromCredentialOfferRequest({
61
+ request: credentialOffer.request,
62
+ metadata,
63
+ });
64
+ }
65
+
66
+ public withCredentialEndpointFromMetadata(metadata: IssuerMetadata): CredentialRequestClientBuilderV1_0_09 {
67
+ this.credentialEndpoint = metadata.credential_endpoint;
68
+ return this;
69
+ }
70
+
71
+ public withCredentialEndpoint(credentialEndpoint: string): CredentialRequestClientBuilderV1_0_09 {
72
+ this.credentialEndpoint = credentialEndpoint;
73
+ return this;
74
+ }
75
+
76
+ public withCredentialType(credentialType: string | string[]): CredentialRequestClientBuilderV1_0_09 {
77
+ this.credentialType = credentialType;
78
+ return this;
79
+ }
80
+
81
+ public withFormat(format: CredentialFormat | CredentialFormat[]): CredentialRequestClientBuilderV1_0_09 {
82
+ this.format = format;
83
+ return this;
84
+ }
85
+
86
+ public withToken(accessToken: string): CredentialRequestClientBuilderV1_0_09 {
87
+ this.token = accessToken;
88
+ return this;
89
+ }
90
+
91
+ public withTokenFromResponse(response: AccessTokenResponse): CredentialRequestClientBuilderV1_0_09 {
92
+ this.token = response.access_token;
93
+ return this;
94
+ }
95
+
96
+ public build(): CredentialRequestClient {
97
+ return new CredentialRequestClient(this);
98
+ }
99
+ }
@@ -0,0 +1,147 @@
1
+ import {
2
+ CredentialOfferPayload,
3
+ CredentialOfferRequestWithBaseUrl,
4
+ EndpointMetadata,
5
+ getIssuerFromCredentialOfferPayload,
6
+ IssuerMetadata,
7
+ OAuth2ASMetadata,
8
+ Oauth2ASWithOID4VCIMetadata,
9
+ OpenIDResponse,
10
+ WellKnownEndpoints,
11
+ } from '@sphereon/oid4vci-common';
12
+ import Debug from 'debug';
13
+
14
+ import { getJson } from './functions';
15
+
16
+ const debug = Debug('sphereon:openid4vci:metadata');
17
+
18
+ export class MetadataClient {
19
+ /**
20
+ * Retrieve metadata using the Initiation obtained from a previous step
21
+ *
22
+ * @param credentialOffer
23
+ */
24
+ public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<EndpointMetadata> {
25
+ return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.request);
26
+ }
27
+
28
+ /**
29
+ * Retrieve the metada using the initiation request obtained from a previous step
30
+ * @param request
31
+ */
32
+ public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise<EndpointMetadata> {
33
+ if (getIssuerFromCredentialOfferPayload(request)) {
34
+ return MetadataClient.retrieveAllMetadata(getIssuerFromCredentialOfferPayload(request) as string);
35
+ }
36
+ throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present");
37
+ }
38
+
39
+ /**
40
+ * Retrieve all metadata from an issuer
41
+ * @param issuer The issuer URL
42
+ * @param opts
43
+ */
44
+ public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadata> {
45
+ let token_endpoint;
46
+ let credential_endpoint;
47
+ const response = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuer);
48
+ let issuerMetadata = response?.successBody;
49
+ if (issuerMetadata) {
50
+ debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${issuerMetadata}`);
51
+ credential_endpoint = issuerMetadata.credential_endpoint;
52
+ token_endpoint = issuerMetadata.token_endpoint;
53
+ if (!token_endpoint && issuerMetadata.authorization_server) {
54
+ debug(
55
+ `Issuer ${issuer} OID4VCI metadata has separate authorization_server ${issuerMetadata.authorization_server} that contains the token endpoint`
56
+ );
57
+ // Crossword uses this to separate the AS metadata. We fail when not found, since we now have no way of getting the token endpoint
58
+ const response: OpenIDResponse<OAuth2ASMetadata> = await this.retrieveWellknown(
59
+ issuerMetadata.authorization_server,
60
+ WellKnownEndpoints.OAUTH_AS,
61
+ {
62
+ errorOnNotFound: true,
63
+ }
64
+ );
65
+ token_endpoint = response.successBody?.token_endpoint;
66
+ }
67
+ } else {
68
+ // No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OpenID IDP. Let's start with OIDC first
69
+ let response: OpenIDResponse<Oauth2ASWithOID4VCIMetadata> = await MetadataClient.retrieveWellknown(
70
+ issuer,
71
+ WellKnownEndpoints.OPENID_CONFIGURATION,
72
+ {
73
+ errorOnNotFound: false,
74
+ }
75
+ );
76
+ let asConfig = response.successBody;
77
+ if (asConfig) {
78
+ debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`);
79
+ } else {
80
+ // Now oAuth2
81
+ response = await MetadataClient.retrieveWellknown(issuer, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
82
+ asConfig = response.successBody;
83
+ }
84
+ if (asConfig) {
85
+ debug(`Issuer ${issuer} has oAuth2 Server metadata in well-known location`);
86
+ issuerMetadata = asConfig;
87
+ credential_endpoint = issuerMetadata.credential_endpoint;
88
+ token_endpoint = issuerMetadata.token_endpoint;
89
+ }
90
+ }
91
+ if (!token_endpoint) {
92
+ debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`);
93
+ if (opts?.errorOnNotFound) {
94
+ throw new Error(`Could not deduce the token endpoint for ${issuer}`);
95
+ } else {
96
+ token_endpoint = `${issuer}${issuer.endsWith('/') ? '' : '/'}token`;
97
+ }
98
+ }
99
+ if (!credential_endpoint) {
100
+ debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`);
101
+ if (opts?.errorOnNotFound) {
102
+ throw new Error(`Could not deduce the credential endpoint for ${issuer}`);
103
+ } else {
104
+ credential_endpoint = `${issuer}${issuer.endsWith('/') ? '' : '/'}credential`;
105
+ }
106
+ }
107
+ debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`);
108
+ return {
109
+ issuer,
110
+ token_endpoint,
111
+ credential_endpoint,
112
+ issuerMetadata,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Retrieve only the OID4VCI metadata for the issuer. So no OIDC/OAuth2 metadata
118
+ *
119
+ * @param issuerHost The issuer hostname
120
+ */
121
+ public static async retrieveOpenID4VCIServerMetadata(issuerHost: string): Promise<OpenIDResponse<IssuerMetadata> | undefined> {
122
+ // Since the server metadata endpoint is optional we are not going to throw an error.
123
+ return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, { errorOnNotFound: false });
124
+ }
125
+
126
+ /**
127
+ * Allows to retrieve information from a well-known location
128
+ *
129
+ * @param host The host
130
+ * @param endpointType The endpoint type, currently supports OID4VCI, OIDC and OAuth2 endpoint types
131
+ * @param opts Options, like for instance whether an error should be thrown in case the endpoint doesn't exist
132
+ */
133
+ public static async retrieveWellknown<T>(
134
+ host: string,
135
+ endpointType: WellKnownEndpoints,
136
+ opts?: { errorOnNotFound?: boolean }
137
+ ): Promise<OpenIDResponse<T>> {
138
+ const result: OpenIDResponse<T> = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, {
139
+ exceptionOnHttpErrorStatus: opts?.errorOnNotFound,
140
+ });
141
+ if (result.origResponse.status === 404) {
142
+ // We only get here when error on not found is false
143
+ debug(`host ${host} with endpoint type ${endpointType} was not found (404)`);
144
+ }
145
+ return result;
146
+ }
147
+ }