@sphereon/oid4vci-client 0.10.4-unstable.98 → 0.12.0
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.
- package/README.md +7 -7
- package/dist/AccessTokenClient.d.ts.map +1 -1
- package/dist/AccessTokenClient.js +5 -6
- package/dist/AccessTokenClient.js.map +1 -1
- package/dist/AccessTokenClientV1_0_11.d.ts.map +1 -1
- package/dist/AccessTokenClientV1_0_11.js +3 -5
- package/dist/AccessTokenClientV1_0_11.js.map +1 -1
- package/dist/AuthorizationCodeClient.d.ts +4 -1
- package/dist/AuthorizationCodeClient.d.ts.map +1 -1
- package/dist/AuthorizationCodeClient.js +47 -8
- package/dist/AuthorizationCodeClient.js.map +1 -1
- package/dist/AuthorizationCodeClientV1_0_11.d.ts.map +1 -1
- package/dist/AuthorizationCodeClientV1_0_11.js +5 -3
- package/dist/AuthorizationCodeClientV1_0_11.js.map +1 -1
- package/dist/CredentialOfferClient.d.ts.map +1 -1
- package/dist/CredentialOfferClient.js +26 -12
- package/dist/CredentialOfferClient.js.map +1 -1
- package/dist/CredentialOfferClientV1_0_11.js +6 -7
- package/dist/CredentialOfferClientV1_0_11.js.map +1 -1
- package/dist/CredentialOfferClientV1_0_13.d.ts +10 -0
- package/dist/CredentialOfferClientV1_0_13.d.ts.map +1 -0
- package/dist/CredentialOfferClientV1_0_13.js +94 -0
- package/dist/CredentialOfferClientV1_0_13.js.map +1 -0
- package/dist/CredentialRequestClient.js +1 -1
- package/dist/CredentialRequestClient.js.map +1 -1
- package/dist/CredentialRequestClientBuilderV1_0_11.d.ts +3 -1
- package/dist/CredentialRequestClientBuilderV1_0_11.d.ts.map +1 -1
- package/dist/CredentialRequestClientBuilderV1_0_11.js +4 -0
- package/dist/CredentialRequestClientBuilderV1_0_11.js.map +1 -1
- package/dist/CredentialRequestClientV1_0_11.d.ts +9 -3
- package/dist/CredentialRequestClientV1_0_11.d.ts.map +1 -1
- package/dist/CredentialRequestClientV1_0_11.js +2 -1
- package/dist/CredentialRequestClientV1_0_11.js.map +1 -1
- package/dist/MetadataClient.d.ts +5 -5
- package/dist/MetadataClient.d.ts.map +1 -1
- package/dist/MetadataClient.js +30 -13
- package/dist/MetadataClient.js.map +1 -1
- package/dist/MetadataClientV1_0_13.d.ts +31 -0
- package/dist/MetadataClientV1_0_13.d.ts.map +1 -0
- package/dist/MetadataClientV1_0_13.js +181 -0
- package/dist/MetadataClientV1_0_13.js.map +1 -0
- package/dist/OpenID4VCIClient.d.ts +13 -24
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +126 -103
- package/dist/OpenID4VCIClient.js.map +1 -1
- package/dist/OpenID4VCIClientV1_0_11.d.ts +3 -2
- package/dist/OpenID4VCIClientV1_0_11.d.ts.map +1 -1
- package/dist/OpenID4VCIClientV1_0_11.js +5 -18
- package/dist/OpenID4VCIClientV1_0_11.js.map +1 -1
- package/dist/OpenID4VCIClientV1_0_13.d.ts +112 -0
- package/dist/OpenID4VCIClientV1_0_13.d.ts.map +1 -0
- package/dist/OpenID4VCIClientV1_0_13.js +478 -0
- package/dist/OpenID4VCIClientV1_0_13.js.map +1 -0
- package/dist/ProofOfPossessionBuilder.d.ts +14 -3
- package/dist/ProofOfPossessionBuilder.d.ts.map +1 -1
- package/dist/ProofOfPossessionBuilder.js +20 -20
- package/dist/ProofOfPossessionBuilder.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/lib/AccessTokenClient.ts +5 -11
- package/lib/AccessTokenClientV1_0_11.ts +3 -9
- package/lib/AuthorizationCodeClient.ts +47 -8
- package/lib/AuthorizationCodeClientV1_0_11.ts +8 -6
- package/lib/CredentialOfferClient.ts +31 -9
- package/lib/CredentialOfferClientV1_0_11.ts +6 -6
- package/lib/CredentialOfferClientV1_0_13.ts +103 -0
- package/lib/CredentialRequestClient.ts +1 -1
- package/lib/CredentialRequestClientBuilderV1_0_11.ts +7 -0
- package/lib/CredentialRequestClientV1_0_11.ts +9 -4
- package/lib/MetadataClient.ts +49 -14
- package/lib/MetadataClientV1_0_13.ts +188 -0
- package/lib/OpenID4VCIClient.ts +131 -115
- package/lib/OpenID4VCIClientV1_0_11.ts +9 -19
- package/lib/OpenID4VCIClientV1_0_13.ts +677 -0
- package/lib/ProofOfPossessionBuilder.ts +40 -9
- package/lib/__tests__/CredentialRequestClientV1_0_11.spec.ts +2 -2
- package/lib/__tests__/MetadataClient.spec.ts +3 -4
- package/lib/__tests__/MetadataMocks.ts +1 -0
- package/lib/__tests__/OpenID4VCIClient.spec.ts +42 -9
- package/lib/__tests__/OpenID4VCIClientV1_0_11.spec.ts +24 -0
- package/lib/__tests__/OpenID4VCIClientV1_0_13.spec.ts +204 -0
- package/lib/__tests__/SdJwt.spec.ts +2 -2
- package/lib/__tests__/SphereonE2E.spec.test.ts +4 -3
- package/lib/index.ts +8 -0
- package/package.json +4 -4
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessTokenResponse,
|
|
3
|
+
Alg,
|
|
4
|
+
AuthorizationRequestOpts,
|
|
5
|
+
AuthorizationResponse,
|
|
6
|
+
AuthzFlowType,
|
|
7
|
+
CodeChallengeMethod,
|
|
8
|
+
CredentialConfigurationSupportedV1_0_13,
|
|
9
|
+
CredentialOfferPayloadV1_0_13,
|
|
10
|
+
CredentialOfferRequestWithBaseUrl,
|
|
11
|
+
CredentialResponse,
|
|
12
|
+
DefaultURISchemes,
|
|
13
|
+
EndpointMetadataResultV1_0_13,
|
|
14
|
+
ExperimentalSubjectIssuance,
|
|
15
|
+
getClientIdFromCredentialOfferPayload,
|
|
16
|
+
getIssuerFromCredentialOfferPayload,
|
|
17
|
+
getSupportedCredentials,
|
|
18
|
+
getTypesFromCredentialSupported,
|
|
19
|
+
JWK,
|
|
20
|
+
KID_JWK_X5C_ERROR,
|
|
21
|
+
NotificationRequest,
|
|
22
|
+
NotificationResult,
|
|
23
|
+
OID4VCICredentialFormat,
|
|
24
|
+
OpenId4VCIVersion,
|
|
25
|
+
PKCEOpts,
|
|
26
|
+
ProofOfPossessionCallbacks,
|
|
27
|
+
toAuthorizationResponsePayload,
|
|
28
|
+
} from '@sphereon/oid4vci-common';
|
|
29
|
+
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
30
|
+
import Debug from 'debug';
|
|
31
|
+
|
|
32
|
+
import { AccessTokenClient } from './AccessTokenClient';
|
|
33
|
+
import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
|
|
34
|
+
import { CredentialOfferClient } from './CredentialOfferClient';
|
|
35
|
+
import { CredentialRequestOpts } from './CredentialRequestClient';
|
|
36
|
+
import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
|
|
37
|
+
import { MetadataClientV1_0_13 } from './MetadataClientV1_0_13';
|
|
38
|
+
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
|
|
39
|
+
import { generateMissingPKCEOpts } from './functions';
|
|
40
|
+
import { sendNotification } from './functions';
|
|
41
|
+
|
|
42
|
+
const debug = Debug('sphereon:oid4vci');
|
|
43
|
+
|
|
44
|
+
export interface OpenID4VCIClientStateV1_0_13 {
|
|
45
|
+
credentialIssuer: string;
|
|
46
|
+
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
47
|
+
clientId?: string;
|
|
48
|
+
kid?: string;
|
|
49
|
+
jwk?: JWK;
|
|
50
|
+
alg?: Alg | string;
|
|
51
|
+
endpointMetadata?: EndpointMetadataResultV1_0_13;
|
|
52
|
+
accessTokenResponse?: AccessTokenResponse;
|
|
53
|
+
authorizationRequestOpts?: AuthorizationRequestOpts;
|
|
54
|
+
authorizationCodeResponse?: AuthorizationResponse;
|
|
55
|
+
pkce: PKCEOpts;
|
|
56
|
+
accessToken?: string;
|
|
57
|
+
authorizationURL?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class OpenID4VCIClientV1_0_13 {
|
|
61
|
+
private readonly _state: OpenID4VCIClientStateV1_0_13;
|
|
62
|
+
|
|
63
|
+
private constructor({
|
|
64
|
+
credentialOffer,
|
|
65
|
+
clientId,
|
|
66
|
+
kid,
|
|
67
|
+
alg,
|
|
68
|
+
credentialIssuer,
|
|
69
|
+
pkce,
|
|
70
|
+
authorizationRequest,
|
|
71
|
+
accessToken,
|
|
72
|
+
jwk,
|
|
73
|
+
endpointMetadata,
|
|
74
|
+
accessTokenResponse,
|
|
75
|
+
authorizationRequestOpts,
|
|
76
|
+
authorizationCodeResponse,
|
|
77
|
+
authorizationURL,
|
|
78
|
+
}: {
|
|
79
|
+
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
80
|
+
kid?: string;
|
|
81
|
+
alg?: Alg | string;
|
|
82
|
+
clientId?: string;
|
|
83
|
+
credentialIssuer?: string;
|
|
84
|
+
pkce?: PKCEOpts;
|
|
85
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
86
|
+
jwk?: JWK;
|
|
87
|
+
accessToken?: string;
|
|
88
|
+
endpointMetadata?: EndpointMetadataResultV1_0_13;
|
|
89
|
+
accessTokenResponse?: AccessTokenResponse;
|
|
90
|
+
authorizationRequestOpts?: AuthorizationRequestOpts;
|
|
91
|
+
authorizationCodeResponse?: AuthorizationResponse;
|
|
92
|
+
authorizationURL?: string;
|
|
93
|
+
}) {
|
|
94
|
+
const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
|
|
95
|
+
if (!issuer) {
|
|
96
|
+
throw Error('No credential issuer supplied or deduced from offer');
|
|
97
|
+
}
|
|
98
|
+
this._state = {
|
|
99
|
+
credentialOffer,
|
|
100
|
+
credentialIssuer: issuer,
|
|
101
|
+
kid,
|
|
102
|
+
alg,
|
|
103
|
+
// TODO: We need to refactor this and always explicitly call createAuthorizationRequestUrl, so we can have a credential selection first and use the kid as a default for the client id
|
|
104
|
+
clientId: clientId ?? (credentialOffer && getClientIdFromCredentialOfferPayload(credentialOffer.credential_offer)) ?? kid?.split('#')[0],
|
|
105
|
+
pkce: { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256, ...pkce },
|
|
106
|
+
authorizationRequestOpts,
|
|
107
|
+
authorizationCodeResponse,
|
|
108
|
+
accessToken,
|
|
109
|
+
jwk,
|
|
110
|
+
endpointMetadata,
|
|
111
|
+
accessTokenResponse,
|
|
112
|
+
authorizationURL,
|
|
113
|
+
};
|
|
114
|
+
// Running syncAuthorizationRequestOpts later as it is using the state
|
|
115
|
+
if (!this._state.authorizationRequestOpts) {
|
|
116
|
+
this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(authorizationRequest);
|
|
117
|
+
}
|
|
118
|
+
debug(`Authorization req options: ${JSON.stringify(this._state.authorizationRequestOpts, null, 2)}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public static async fromCredentialIssuer({
|
|
122
|
+
kid,
|
|
123
|
+
alg,
|
|
124
|
+
retrieveServerMetadata,
|
|
125
|
+
clientId,
|
|
126
|
+
credentialIssuer,
|
|
127
|
+
pkce,
|
|
128
|
+
authorizationRequest,
|
|
129
|
+
createAuthorizationRequestURL,
|
|
130
|
+
}: {
|
|
131
|
+
credentialIssuer: string;
|
|
132
|
+
kid?: string;
|
|
133
|
+
alg?: Alg | string;
|
|
134
|
+
retrieveServerMetadata?: boolean;
|
|
135
|
+
clientId?: string;
|
|
136
|
+
createAuthorizationRequestURL?: boolean;
|
|
137
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
138
|
+
pkce?: PKCEOpts;
|
|
139
|
+
}) {
|
|
140
|
+
const client = new OpenID4VCIClientV1_0_13({
|
|
141
|
+
kid,
|
|
142
|
+
alg,
|
|
143
|
+
clientId: clientId ?? authorizationRequest?.clientId,
|
|
144
|
+
credentialIssuer,
|
|
145
|
+
pkce,
|
|
146
|
+
authorizationRequest,
|
|
147
|
+
});
|
|
148
|
+
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
149
|
+
await client.retrieveServerMetadata();
|
|
150
|
+
}
|
|
151
|
+
if (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) {
|
|
152
|
+
await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
|
|
153
|
+
}
|
|
154
|
+
return client;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public static async fromState({ state }: { state: OpenID4VCIClientStateV1_0_13 | string }): Promise<OpenID4VCIClientV1_0_13> {
|
|
158
|
+
const clientState = typeof state === 'string' ? JSON.parse(state) : state;
|
|
159
|
+
|
|
160
|
+
return new OpenID4VCIClientV1_0_13(clientState);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public static async fromURI({
|
|
164
|
+
uri,
|
|
165
|
+
kid,
|
|
166
|
+
alg,
|
|
167
|
+
retrieveServerMetadata,
|
|
168
|
+
clientId,
|
|
169
|
+
pkce,
|
|
170
|
+
createAuthorizationRequestURL,
|
|
171
|
+
authorizationRequest,
|
|
172
|
+
resolveOfferUri,
|
|
173
|
+
}: {
|
|
174
|
+
uri: string;
|
|
175
|
+
kid?: string;
|
|
176
|
+
alg?: Alg | string;
|
|
177
|
+
retrieveServerMetadata?: boolean;
|
|
178
|
+
createAuthorizationRequestURL?: boolean;
|
|
179
|
+
resolveOfferUri?: boolean;
|
|
180
|
+
pkce?: PKCEOpts;
|
|
181
|
+
clientId?: string;
|
|
182
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
183
|
+
}): Promise<OpenID4VCIClientV1_0_13> {
|
|
184
|
+
const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri });
|
|
185
|
+
const client = new OpenID4VCIClientV1_0_13({
|
|
186
|
+
credentialOffer: credentialOfferClient,
|
|
187
|
+
kid,
|
|
188
|
+
alg,
|
|
189
|
+
clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId,
|
|
190
|
+
pkce,
|
|
191
|
+
authorizationRequest,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
195
|
+
await client.retrieveServerMetadata();
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
credentialOfferClient.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
|
|
199
|
+
(createAuthorizationRequestURL === undefined || createAuthorizationRequestURL)
|
|
200
|
+
) {
|
|
201
|
+
await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
|
|
202
|
+
debug(`Authorization Request URL: ${client._state.authorizationURL}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return client;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Allows you to create an Authorization Request URL when using an Authorization Code flow. This URL needs to be accessed using the front channel (browser)
|
|
210
|
+
*
|
|
211
|
+
* The Identity provider would present a login screen typically; after you authenticated, it would redirect to the provided redirectUri; which can be same device or cross-device
|
|
212
|
+
* @param opts
|
|
213
|
+
*/
|
|
214
|
+
public async createAuthorizationRequestUrl(opts?: { authorizationRequest?: AuthorizationRequestOpts; pkce?: PKCEOpts }): Promise<string> {
|
|
215
|
+
if (!this._state.authorizationURL) {
|
|
216
|
+
this.calculatePKCEOpts(opts?.pkce);
|
|
217
|
+
this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(opts?.authorizationRequest);
|
|
218
|
+
if (!this._state.authorizationRequestOpts) {
|
|
219
|
+
throw Error(`No Authorization Request options present or provided in this call`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// todo: Probably can go with current logic in MetadataClientV1_0_13 who will always set the authorization_endpoint when found
|
|
223
|
+
// handling this because of the support for v1_0-08
|
|
224
|
+
if (
|
|
225
|
+
this._state.endpointMetadata?.credentialIssuerMetadata &&
|
|
226
|
+
'authorization_endpoint' in this._state.endpointMetadata.credentialIssuerMetadata
|
|
227
|
+
) {
|
|
228
|
+
this._state.endpointMetadata.authorization_endpoint = this._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
|
|
229
|
+
}
|
|
230
|
+
this._state.authorizationURL = await createAuthorizationRequestUrl({
|
|
231
|
+
pkce: this._state.pkce,
|
|
232
|
+
endpointMetadata: this.endpointMetadata,
|
|
233
|
+
authorizationRequest: this._state.authorizationRequestOpts,
|
|
234
|
+
credentialOffer: this.credentialOffer,
|
|
235
|
+
credentialConfigurationSupported: this.getCredentialsSupported(),
|
|
236
|
+
version: this.version(),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return this._state.authorizationURL;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public async retrieveServerMetadata(): Promise<EndpointMetadataResultV1_0_13> {
|
|
243
|
+
this.assertIssuerData();
|
|
244
|
+
if (!this._state.endpointMetadata) {
|
|
245
|
+
if (this.credentialOffer) {
|
|
246
|
+
this._state.endpointMetadata = await MetadataClientV1_0_13.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
|
|
247
|
+
} else if (this._state.credentialIssuer) {
|
|
248
|
+
this._state.endpointMetadata = await MetadataClientV1_0_13.retrieveAllMetadata(this._state.credentialIssuer);
|
|
249
|
+
} else {
|
|
250
|
+
throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return this.endpointMetadata;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private calculatePKCEOpts(pkce?: PKCEOpts) {
|
|
258
|
+
this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public async acquireAccessToken(opts?: {
|
|
262
|
+
pin?: string;
|
|
263
|
+
clientId?: string;
|
|
264
|
+
codeVerifier?: string;
|
|
265
|
+
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
|
|
266
|
+
code?: string; // Directly pass in a code from an auth response
|
|
267
|
+
redirectUri?: string;
|
|
268
|
+
}): Promise<AccessTokenResponse> {
|
|
269
|
+
const { pin, clientId } = opts ?? {};
|
|
270
|
+
let { redirectUri } = opts ?? {};
|
|
271
|
+
if (opts?.authorizationResponse) {
|
|
272
|
+
this._state.authorizationCodeResponse = { ...toAuthorizationResponsePayload(opts.authorizationResponse) };
|
|
273
|
+
} else if (opts?.code) {
|
|
274
|
+
this._state.authorizationCodeResponse = { code: opts.code };
|
|
275
|
+
}
|
|
276
|
+
const code = this._state.authorizationCodeResponse?.code;
|
|
277
|
+
|
|
278
|
+
if (opts?.codeVerifier) {
|
|
279
|
+
this._state.pkce.codeVerifier = opts.codeVerifier;
|
|
280
|
+
}
|
|
281
|
+
this.assertIssuerData();
|
|
282
|
+
|
|
283
|
+
if (clientId) {
|
|
284
|
+
this._state.clientId = clientId;
|
|
285
|
+
}
|
|
286
|
+
if (!this._state.accessTokenResponse) {
|
|
287
|
+
const accessTokenClient = new AccessTokenClient();
|
|
288
|
+
|
|
289
|
+
if (redirectUri && redirectUri !== this._state.authorizationRequestOpts?.redirectUri) {
|
|
290
|
+
console.log(
|
|
291
|
+
`Redirect URI mismatch between access-token (${redirectUri}) and authorization request (${this._state.authorizationRequestOpts?.redirectUri}). According to the specification that is not allowed.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) {
|
|
295
|
+
redirectUri = this._state.authorizationRequestOpts.redirectUri;
|
|
296
|
+
}
|
|
297
|
+
const response = await accessTokenClient.acquireAccessToken({
|
|
298
|
+
credentialOffer: this.credentialOffer,
|
|
299
|
+
metadata: this.endpointMetadata,
|
|
300
|
+
credentialIssuer: this.getIssuer(),
|
|
301
|
+
pin,
|
|
302
|
+
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
|
|
303
|
+
code,
|
|
304
|
+
redirectUri,
|
|
305
|
+
asOpts: { clientId: this.clientId },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (response.errorBody) {
|
|
309
|
+
debug(`Access token error:\r\n${JSON.stringify(response.errorBody)}`);
|
|
310
|
+
throw Error(
|
|
311
|
+
`Retrieving an access token from ${this._state.endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
312
|
+
response.origResponse.status
|
|
313
|
+
}`,
|
|
314
|
+
);
|
|
315
|
+
} else if (!response.successBody) {
|
|
316
|
+
debug(`Access token error. No success body`);
|
|
317
|
+
throw Error(
|
|
318
|
+
`Retrieving an access token from ${
|
|
319
|
+
this._state.endpointMetadata?.token_endpoint
|
|
320
|
+
} for issuer ${this.getIssuer()} failed as there was no success response body`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
this._state.accessTokenResponse = response.successBody;
|
|
324
|
+
this._state.accessToken = response.successBody.access_token;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return this.accessTokenResponse;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
public async acquireCredentials({
|
|
331
|
+
credentialIdentifier,
|
|
332
|
+
credentialTypes,
|
|
333
|
+
context,
|
|
334
|
+
proofCallbacks,
|
|
335
|
+
format,
|
|
336
|
+
kid,
|
|
337
|
+
jwk,
|
|
338
|
+
alg,
|
|
339
|
+
jti,
|
|
340
|
+
deferredCredentialAwait,
|
|
341
|
+
deferredCredentialIntervalInMS,
|
|
342
|
+
}: {
|
|
343
|
+
credentialIdentifier?: string;
|
|
344
|
+
credentialTypes?: string | string[];
|
|
345
|
+
context?: string[];
|
|
346
|
+
proofCallbacks: ProofOfPossessionCallbacks<any>;
|
|
347
|
+
format?: CredentialFormat | OID4VCICredentialFormat;
|
|
348
|
+
kid?: string;
|
|
349
|
+
jwk?: JWK;
|
|
350
|
+
alg?: Alg | string;
|
|
351
|
+
jti?: string;
|
|
352
|
+
deferredCredentialAwait?: boolean;
|
|
353
|
+
deferredCredentialIntervalInMS?: number;
|
|
354
|
+
experimentalHolderIssuanceSupported?: boolean;
|
|
355
|
+
}): Promise<CredentialResponse & { access_token: string }> {
|
|
356
|
+
if ([jwk, kid].filter((v) => v !== undefined).length > 1) {
|
|
357
|
+
throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (alg) this._state.alg = alg;
|
|
361
|
+
if (jwk) this._state.jwk = jwk;
|
|
362
|
+
if (kid) this._state.kid = kid;
|
|
363
|
+
|
|
364
|
+
const requestBuilder = this.credentialOffer
|
|
365
|
+
? CredentialRequestClientBuilder.fromCredentialOffer({
|
|
366
|
+
credentialOffer: this.credentialOffer,
|
|
367
|
+
metadata: this.endpointMetadata,
|
|
368
|
+
})
|
|
369
|
+
: CredentialRequestClientBuilder.fromCredentialIssuer({
|
|
370
|
+
credentialIssuer: this.getIssuer(),
|
|
371
|
+
credentialIdentifier: credentialIdentifier,
|
|
372
|
+
metadata: this.endpointMetadata,
|
|
373
|
+
version: this.version(),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
|
|
377
|
+
requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);
|
|
378
|
+
let subjectIssuance: ExperimentalSubjectIssuance | undefined;
|
|
379
|
+
if (this.endpointMetadata?.credentialIssuerMetadata) {
|
|
380
|
+
const metadata = this.endpointMetadata.credentialIssuerMetadata;
|
|
381
|
+
const types = credentialTypes ? (Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes]) : undefined;
|
|
382
|
+
|
|
383
|
+
if (credentialIdentifier) {
|
|
384
|
+
if (typeof metadata.credential_configurations_supported !== 'object') {
|
|
385
|
+
throw Error(
|
|
386
|
+
`Credentials_supported should be an object, current ${typeof metadata.credential_configurations_supported} when credential_identifier is used`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const credentialsSupported = metadata.credential_configurations_supported;
|
|
390
|
+
if (!metadata.credential_configurations_supported || !credentialsSupported[credentialIdentifier]) {
|
|
391
|
+
throw new Error(`Credential type ${credentialIdentifier} is not supported by issuer ${this.getIssuer()}`);
|
|
392
|
+
}
|
|
393
|
+
} else if (!types) {
|
|
394
|
+
throw Error(`If no credential_identifier is used, we expect types`);
|
|
395
|
+
} else if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
396
|
+
let typeSupported = false;
|
|
397
|
+
|
|
398
|
+
metadata.credentials_supported.forEach((supportedCredential) => {
|
|
399
|
+
const subTypes = getTypesFromCredentialSupported(supportedCredential);
|
|
400
|
+
if (
|
|
401
|
+
subTypes.every((t, i) => types[i] === t) ||
|
|
402
|
+
(types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0])))
|
|
403
|
+
) {
|
|
404
|
+
typeSupported = true;
|
|
405
|
+
if (supportedCredential.credential_subject_issuance) {
|
|
406
|
+
subjectIssuance = { credential_subject_issuance: supportedCredential.credential_subject_issuance };
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!typeSupported) {
|
|
412
|
+
console.log(`Not all credential types ${JSON.stringify(credentialTypes)} are present in metadata for ${this.getIssuer()}`);
|
|
413
|
+
// throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
414
|
+
}
|
|
415
|
+
} else if (metadata.credential_configurations_supported && !Array.isArray(metadata.credential_configurations_supported)) {
|
|
416
|
+
const credentialsSupported = metadata.credential_configurations_supported;
|
|
417
|
+
if (types.some((type) => !metadata.credential_configurations_supported || !credentialsSupported[type])) {
|
|
418
|
+
throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// todo: Format check? We might end up with some disjoint type / format combinations supported by the server
|
|
422
|
+
}
|
|
423
|
+
if (subjectIssuance) {
|
|
424
|
+
requestBuilder.withSubjectIssuance(subjectIssuance);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const credentialRequestClient = requestBuilder.build();
|
|
428
|
+
const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({
|
|
429
|
+
accessTokenResponse: this.accessTokenResponse,
|
|
430
|
+
callbacks: proofCallbacks,
|
|
431
|
+
version: this.version(),
|
|
432
|
+
})
|
|
433
|
+
.withIssuer(this.getIssuer())
|
|
434
|
+
.withAlg(this.alg);
|
|
435
|
+
|
|
436
|
+
if (this._state.jwk) {
|
|
437
|
+
proofBuilder.withJWK(this._state.jwk);
|
|
438
|
+
}
|
|
439
|
+
if (this._state.kid) {
|
|
440
|
+
proofBuilder.withKid(this._state.kid);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (this.clientId) {
|
|
444
|
+
proofBuilder.withClientId(this.clientId);
|
|
445
|
+
}
|
|
446
|
+
if (jti) {
|
|
447
|
+
proofBuilder.withJti(jti);
|
|
448
|
+
}
|
|
449
|
+
const response = await credentialRequestClient.acquireCredentialsUsingProof({
|
|
450
|
+
proofInput: proofBuilder,
|
|
451
|
+
...(credentialIdentifier ? { credentialIdentifier, subjectIssuance } : { format, context, credentialTypes, subjectIssuance }),
|
|
452
|
+
});
|
|
453
|
+
if (response.errorBody) {
|
|
454
|
+
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
|
|
455
|
+
throw Error(
|
|
456
|
+
`Retrieving a credential from ${this._state.endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
457
|
+
response.origResponse.status
|
|
458
|
+
}`,
|
|
459
|
+
);
|
|
460
|
+
} else if (!response.successBody) {
|
|
461
|
+
debug(`Credential request error. No success body`);
|
|
462
|
+
throw Error(
|
|
463
|
+
`Retrieving a credential from ${
|
|
464
|
+
this._state.endpointMetadata?.credential_endpoint
|
|
465
|
+
} for issuer ${this.getIssuer()} failed as there was no success response body`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
return { ...response.successBody, access_token: response.access_token };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
public async exportState(): Promise<string> {
|
|
472
|
+
return JSON.stringify(this._state);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
getCredentialsSupported(
|
|
476
|
+
format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[],
|
|
477
|
+
): Record<string, CredentialConfigurationSupportedV1_0_13> {
|
|
478
|
+
return getSupportedCredentials({
|
|
479
|
+
issuerMetadata: this.endpointMetadata.credentialIssuerMetadata,
|
|
480
|
+
version: this.version(),
|
|
481
|
+
format: format,
|
|
482
|
+
types: undefined,
|
|
483
|
+
}) as Record<string, CredentialConfigurationSupportedV1_0_13>;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
public async sendNotification(
|
|
487
|
+
credentialRequestOpts: Partial<CredentialRequestOpts>,
|
|
488
|
+
request: NotificationRequest,
|
|
489
|
+
accessToken?: string,
|
|
490
|
+
): Promise<NotificationResult> {
|
|
491
|
+
return sendNotification(credentialRequestOpts, request, accessToken ?? this._state.accessToken ?? this._state.accessTokenResponse?.access_token);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* getCredentialOfferTypes(): string[][] {
|
|
495
|
+
if (!this.credentialOffer) {
|
|
496
|
+
return [];
|
|
497
|
+
} else if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
498
|
+
const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
|
|
499
|
+
const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
|
|
500
|
+
const result: string[][] = [];
|
|
501
|
+
result[0] = types;
|
|
502
|
+
return result;
|
|
503
|
+
} else {
|
|
504
|
+
return this.credentialOffer.credential_offer.credentials.map((c) => {
|
|
505
|
+
if (typeof c === 'string') {
|
|
506
|
+
return [c];
|
|
507
|
+
} else if ('types' in c) {
|
|
508
|
+
return c.types;
|
|
509
|
+
} else if ('vct' in c) {
|
|
510
|
+
return [c.vct];
|
|
511
|
+
} else {
|
|
512
|
+
return c.credential_definition.types;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}*/
|
|
517
|
+
|
|
518
|
+
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
519
|
+
return (
|
|
520
|
+
this.credentialOffer?.supportedFlows ??
|
|
521
|
+
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : [])
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
isFlowTypeSupported(flowType: AuthzFlowType): boolean {
|
|
526
|
+
return this.issuerSupportedFlowTypes().includes(flowType);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
public hasAuthorizationURL(): boolean {
|
|
530
|
+
return !!this.authorizationURL;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
get authorizationURL(): string | undefined {
|
|
534
|
+
return this._state.authorizationURL;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
|
|
538
|
+
return this._state.credentialOffer;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
public version(): OpenId4VCIVersion {
|
|
542
|
+
return this.credentialOffer?.version ?? OpenId4VCIVersion.VER_1_0_13;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
public get endpointMetadata(): EndpointMetadataResultV1_0_13 {
|
|
546
|
+
this.assertServerMetadata();
|
|
547
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
548
|
+
return this._state.endpointMetadata!;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
get kid(): string {
|
|
552
|
+
this.assertIssuerData();
|
|
553
|
+
if (!this._state.kid) {
|
|
554
|
+
throw new Error('No value for kid is supplied');
|
|
555
|
+
}
|
|
556
|
+
return this._state.kid;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
get alg(): string {
|
|
560
|
+
this.assertIssuerData();
|
|
561
|
+
if (!this._state.alg) {
|
|
562
|
+
throw new Error('No value for alg is supplied');
|
|
563
|
+
}
|
|
564
|
+
return this._state.alg;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
set clientId(value: string | undefined) {
|
|
568
|
+
this._state.clientId = value;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
get clientId(): string | undefined {
|
|
572
|
+
return this._state.clientId;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
public hasAccessTokenResponse(): boolean {
|
|
576
|
+
return !!this._state.accessTokenResponse;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
get accessTokenResponse(): AccessTokenResponse {
|
|
580
|
+
this.assertAccessToken();
|
|
581
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
582
|
+
return this._state.accessTokenResponse!;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
public getIssuer(): string {
|
|
586
|
+
this.assertIssuerData();
|
|
587
|
+
return this._state.credentialIssuer;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
public getAccessTokenEndpoint(): string {
|
|
591
|
+
this.assertIssuerData();
|
|
592
|
+
return this.endpointMetadata
|
|
593
|
+
? this.endpointMetadata.token_endpoint
|
|
594
|
+
: AccessTokenClient.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
public getCredentialEndpoint(): string {
|
|
598
|
+
this.assertIssuerData();
|
|
599
|
+
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
public hasDeferredCredentialEndpoint(): boolean {
|
|
603
|
+
return !!this.getAccessTokenEndpoint();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
public getDeferredCredentialEndpoint(): string {
|
|
607
|
+
this.assertIssuerData();
|
|
608
|
+
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Too bad we need a method like this, but EBSI is not exposing metadata
|
|
613
|
+
*/
|
|
614
|
+
public isEBSI(): boolean {
|
|
615
|
+
const credentialOffer = this.credentialOffer?.credential_offer as CredentialOfferPayloadV1_0_13;
|
|
616
|
+
|
|
617
|
+
if (credentialOffer?.credential_configuration_ids) {
|
|
618
|
+
const credentialConfigurations = this.endpointMetadata.credentialIssuerMetadata?.credential_configurations_supported;
|
|
619
|
+
|
|
620
|
+
if (credentialConfigurations) {
|
|
621
|
+
const isEBSITrustFramework = credentialOffer.credential_configuration_ids
|
|
622
|
+
.map((id) => credentialConfigurations[id])
|
|
623
|
+
.filter(
|
|
624
|
+
(config): config is CredentialConfigurationSupportedV1_0_13 =>
|
|
625
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
626
|
+
// @ts-ignore
|
|
627
|
+
config !== undefined && 'trust_framework' in config && 'name' in config.trust_framework,
|
|
628
|
+
)
|
|
629
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
630
|
+
// @ts-ignore
|
|
631
|
+
.some((config) => config.trust_framework.name.includes('ebsi'));
|
|
632
|
+
|
|
633
|
+
if (isEBSITrustFramework) {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
this.assertIssuerData();
|
|
640
|
+
return this.endpointMetadata.credentialIssuerMetadata?.authorization_endpoint?.includes('ebsi.eu') ?? false;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private assertIssuerData(): void {
|
|
644
|
+
if (!this._state.credentialIssuer) {
|
|
645
|
+
throw Error(`No credential issuer value present`);
|
|
646
|
+
} else if (!this._state.credentialOffer && this._state.endpointMetadata && this.issuerSupportedFlowTypes().length === 0) {
|
|
647
|
+
throw Error(`No issuance initiation or credential offer present`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private assertServerMetadata(): void {
|
|
652
|
+
if (!this._state.endpointMetadata) {
|
|
653
|
+
throw Error('No server metadata');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private assertAccessToken(): void {
|
|
658
|
+
if (!this._state.accessTokenResponse) {
|
|
659
|
+
throw Error(`No access token present`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts): AuthorizationRequestOpts {
|
|
664
|
+
let authorizationRequestOpts = { ...this._state?.authorizationRequestOpts, ...opts } as AuthorizationRequestOpts;
|
|
665
|
+
if (!authorizationRequestOpts) {
|
|
666
|
+
// We only set a redirectUri if no options are provided.
|
|
667
|
+
// Note that this only works for mobile apps, that can handle a code query param on the default openid-credential-offer deeplink.
|
|
668
|
+
// Provide your own options if that is not desired!
|
|
669
|
+
authorizationRequestOpts = { redirectUri: `${DefaultURISchemes.CREDENTIAL_OFFER}://` };
|
|
670
|
+
}
|
|
671
|
+
const clientId = authorizationRequestOpts.clientId ?? this._state.clientId;
|
|
672
|
+
// sync clientId
|
|
673
|
+
this._state.clientId = clientId;
|
|
674
|
+
authorizationRequestOpts.clientId = clientId;
|
|
675
|
+
return authorizationRequestOpts;
|
|
676
|
+
}
|
|
677
|
+
}
|