@sphereon/oid4vci-client 0.8.2-next.48 → 0.8.2-next.87
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/dist/AccessTokenClient.d.ts +0 -1
- package/dist/AccessTokenClient.d.ts.map +1 -1
- package/dist/AccessTokenClient.js +2 -9
- package/dist/AccessTokenClient.js.map +1 -1
- package/dist/AuthorizationCodeClient.d.ts +9 -0
- package/dist/AuthorizationCodeClient.d.ts.map +1 -0
- package/dist/AuthorizationCodeClient.js +124 -0
- package/dist/AuthorizationCodeClient.js.map +1 -0
- package/dist/CredentialOfferClient.d.ts.map +1 -1
- package/dist/CredentialOfferClient.js +3 -1
- package/dist/CredentialOfferClient.js.map +1 -1
- package/dist/CredentialRequestClient.d.ts +2 -0
- package/dist/CredentialRequestClient.d.ts.map +1 -1
- package/dist/CredentialRequestClient.js +9 -7
- package/dist/CredentialRequestClient.js.map +1 -1
- package/dist/OpenID4VCIClient.d.ts +50 -29
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +191 -190
- package/dist/OpenID4VCIClient.js.map +1 -1
- package/dist/functions/AuthorizationUtil.d.ts +3 -0
- package/dist/functions/AuthorizationUtil.d.ts.map +1 -0
- package/dist/functions/AuthorizationUtil.js +22 -0
- package/dist/functions/AuthorizationUtil.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/lib/AccessTokenClient.ts +5 -11
- package/lib/AuthorizationCodeClient.ts +151 -0
- package/lib/CredentialOfferClient.ts +4 -1
- package/lib/CredentialRequestClient.ts +13 -4
- package/lib/OpenID4VCIClient.ts +250 -228
- package/lib/__tests__/AccessTokenClient.spec.ts +2 -0
- package/lib/__tests__/CredentialRequestClient.spec.ts +10 -2
- package/lib/__tests__/EBSIE2E.spec.test.ts +8 -6
- package/lib/__tests__/OpenID4VCIClient.spec.ts +115 -79
- package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +59 -49
- package/lib/__tests__/SdJwt.spec.ts +2 -0
- package/lib/__tests__/SphereonE2E.spec.test.ts +2 -2
- package/lib/functions/AuthorizationUtil.ts +18 -0
- package/lib/index.ts +1 -0
- package/lib/types/index.ts +0 -0
- package/package.json +3 -3
package/lib/OpenID4VCIClient.ts
CHANGED
|
@@ -1,62 +1,57 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AccessTokenResponse,
|
|
3
3
|
Alg,
|
|
4
|
+
AuthorizationRequestOpts,
|
|
5
|
+
AuthorizationResponse,
|
|
4
6
|
AuthzFlowType,
|
|
5
7
|
CodeChallengeMethod,
|
|
6
8
|
CredentialOfferPayloadV1_0_08,
|
|
7
9
|
CredentialOfferRequestWithBaseUrl,
|
|
8
10
|
CredentialResponse,
|
|
9
11
|
CredentialSupported,
|
|
12
|
+
DefaultURISchemes,
|
|
10
13
|
EndpointMetadataResult,
|
|
14
|
+
getClientIdFromCredentialOfferPayload,
|
|
11
15
|
getIssuerFromCredentialOfferPayload,
|
|
12
16
|
getSupportedCredentials,
|
|
13
17
|
getTypesFromCredentialSupported,
|
|
14
|
-
JsonURIMode,
|
|
15
18
|
JWK,
|
|
16
19
|
KID_JWK_X5C_ERROR,
|
|
17
20
|
OID4VCICredentialFormat,
|
|
18
21
|
OpenId4VCIVersion,
|
|
22
|
+
PKCEOpts,
|
|
19
23
|
ProofOfPossessionCallbacks,
|
|
20
|
-
|
|
21
|
-
ResponseType,
|
|
24
|
+
toAuthorizationResponsePayload,
|
|
22
25
|
} from '@sphereon/oid4vci-common';
|
|
23
26
|
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
24
27
|
import Debug from 'debug';
|
|
25
28
|
|
|
26
29
|
import { AccessTokenClient } from './AccessTokenClient';
|
|
30
|
+
import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
|
|
27
31
|
import { CredentialOfferClient } from './CredentialOfferClient';
|
|
28
32
|
import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
|
|
29
33
|
import { MetadataClient } from './MetadataClient';
|
|
30
34
|
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
|
|
31
|
-
import {
|
|
35
|
+
import { generateMissingPKCEOpts } from './functions/AuthorizationUtil';
|
|
32
36
|
|
|
33
37
|
const debug = Debug('sphereon:oid4vci');
|
|
34
38
|
|
|
35
|
-
interface
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
redirectUri: string;
|
|
48
|
-
scope?: string;
|
|
39
|
+
export interface OpenID4VCIClientState {
|
|
40
|
+
credentialIssuer: string;
|
|
41
|
+
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
42
|
+
clientId?: string;
|
|
43
|
+
kid?: string;
|
|
44
|
+
jwk?: JWK;
|
|
45
|
+
alg?: Alg | string;
|
|
46
|
+
endpointMetadata?: EndpointMetadataResult;
|
|
47
|
+
accessTokenResponse?: AccessTokenResponse;
|
|
48
|
+
authorizationRequestOpts?: AuthorizationRequestOpts;
|
|
49
|
+
pkce: PKCEOpts;
|
|
50
|
+
authorizationURL?: string;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export class OpenID4VCIClient {
|
|
52
|
-
private readonly
|
|
53
|
-
private _credentialIssuer: string;
|
|
54
|
-
private _clientId?: string;
|
|
55
|
-
private _kid: string | undefined;
|
|
56
|
-
private _jwk: JWK | undefined;
|
|
57
|
-
private _alg: Alg | string | undefined;
|
|
58
|
-
private _endpointMetadata: EndpointMetadataResult | undefined;
|
|
59
|
-
private _accessTokenResponse: AccessTokenResponse | undefined;
|
|
54
|
+
private readonly _state: OpenID4VCIClientState;
|
|
60
55
|
|
|
61
56
|
private constructor({
|
|
62
57
|
credentialOffer,
|
|
@@ -64,22 +59,50 @@ export class OpenID4VCIClient {
|
|
|
64
59
|
kid,
|
|
65
60
|
alg,
|
|
66
61
|
credentialIssuer,
|
|
62
|
+
pkce,
|
|
63
|
+
authorizationRequest,
|
|
64
|
+
jwk,
|
|
65
|
+
endpointMetadata,
|
|
66
|
+
accessTokenResponse,
|
|
67
|
+
authorizationRequestOpts,
|
|
68
|
+
authorizationURL,
|
|
67
69
|
}: {
|
|
68
70
|
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
69
71
|
kid?: string;
|
|
70
72
|
alg?: Alg | string;
|
|
71
73
|
clientId?: string;
|
|
72
74
|
credentialIssuer?: string;
|
|
75
|
+
pkce?: PKCEOpts;
|
|
76
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
77
|
+
jwk?: JWK;
|
|
78
|
+
endpointMetadata?: EndpointMetadataResult;
|
|
79
|
+
accessTokenResponse?: AccessTokenResponse;
|
|
80
|
+
authorizationRequestOpts?: AuthorizationRequestOpts;
|
|
81
|
+
authorizationURL?: string;
|
|
73
82
|
}) {
|
|
74
|
-
this._credentialOffer = credentialOffer;
|
|
75
83
|
const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
|
|
76
84
|
if (!issuer) {
|
|
77
85
|
throw Error('No credential issuer supplied or deduced from offer');
|
|
78
86
|
}
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
this._state = {
|
|
88
|
+
credentialOffer,
|
|
89
|
+
credentialIssuer: issuer,
|
|
90
|
+
kid,
|
|
91
|
+
alg,
|
|
92
|
+
// 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
|
|
93
|
+
clientId: clientId ?? (credentialOffer && getClientIdFromCredentialOfferPayload(credentialOffer.credential_offer)) ?? kid?.split('#')[0],
|
|
94
|
+
pkce: { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256, ...pkce },
|
|
95
|
+
authorizationRequestOpts,
|
|
96
|
+
jwk,
|
|
97
|
+
endpointMetadata,
|
|
98
|
+
accessTokenResponse,
|
|
99
|
+
authorizationURL,
|
|
100
|
+
};
|
|
101
|
+
// Running syncAuthorizationRequestOpts later as it is using the state
|
|
102
|
+
if (!this._state.authorizationRequestOpts) {
|
|
103
|
+
this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(authorizationRequest);
|
|
104
|
+
}
|
|
105
|
+
debug(`Authorization req options: ${JSON.stringify(this._state.authorizationRequestOpts, null, 2)}`);
|
|
83
106
|
}
|
|
84
107
|
|
|
85
108
|
public static async fromCredentialIssuer({
|
|
@@ -88,263 +111,197 @@ export class OpenID4VCIClient {
|
|
|
88
111
|
retrieveServerMetadata,
|
|
89
112
|
clientId,
|
|
90
113
|
credentialIssuer,
|
|
114
|
+
pkce,
|
|
115
|
+
authorizationRequest,
|
|
116
|
+
createAuthorizationRequestURL,
|
|
91
117
|
}: {
|
|
92
118
|
credentialIssuer: string;
|
|
93
119
|
kid?: string;
|
|
94
120
|
alg?: Alg | string;
|
|
95
121
|
retrieveServerMetadata?: boolean;
|
|
96
122
|
clientId?: string;
|
|
123
|
+
createAuthorizationRequestURL?: boolean;
|
|
124
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
125
|
+
pkce?: PKCEOpts;
|
|
97
126
|
}) {
|
|
98
|
-
const client = new OpenID4VCIClient({
|
|
127
|
+
const client = new OpenID4VCIClient({
|
|
128
|
+
kid,
|
|
129
|
+
alg,
|
|
130
|
+
clientId: clientId ?? authorizationRequest?.clientId,
|
|
131
|
+
credentialIssuer,
|
|
132
|
+
pkce,
|
|
133
|
+
authorizationRequest,
|
|
134
|
+
});
|
|
99
135
|
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
100
136
|
await client.retrieveServerMetadata();
|
|
101
137
|
}
|
|
138
|
+
if (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) {
|
|
139
|
+
await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
|
|
140
|
+
}
|
|
102
141
|
return client;
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
public static async fromState({ state }: { state: OpenID4VCIClientState | string }): Promise<OpenID4VCIClient> {
|
|
145
|
+
const clientState = typeof state === 'string' ? JSON.parse(state) : state;
|
|
146
|
+
|
|
147
|
+
return new OpenID4VCIClient(clientState);
|
|
148
|
+
}
|
|
149
|
+
|
|
105
150
|
public static async fromURI({
|
|
106
151
|
uri,
|
|
107
152
|
kid,
|
|
108
153
|
alg,
|
|
109
154
|
retrieveServerMetadata,
|
|
110
155
|
clientId,
|
|
156
|
+
pkce,
|
|
157
|
+
createAuthorizationRequestURL,
|
|
158
|
+
authorizationRequest,
|
|
111
159
|
resolveOfferUri,
|
|
112
160
|
}: {
|
|
113
161
|
uri: string;
|
|
114
162
|
kid?: string;
|
|
115
163
|
alg?: Alg | string;
|
|
116
164
|
retrieveServerMetadata?: boolean;
|
|
165
|
+
createAuthorizationRequestURL?: boolean;
|
|
117
166
|
resolveOfferUri?: boolean;
|
|
167
|
+
pkce?: PKCEOpts;
|
|
118
168
|
clientId?: string;
|
|
169
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
119
170
|
}): Promise<OpenID4VCIClient> {
|
|
171
|
+
const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri });
|
|
120
172
|
const client = new OpenID4VCIClient({
|
|
121
|
-
credentialOffer:
|
|
173
|
+
credentialOffer: credentialOfferClient,
|
|
122
174
|
kid,
|
|
123
175
|
alg,
|
|
124
|
-
clientId,
|
|
176
|
+
clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId,
|
|
177
|
+
pkce,
|
|
178
|
+
authorizationRequest,
|
|
125
179
|
});
|
|
126
180
|
|
|
127
181
|
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
128
182
|
await client.retrieveServerMetadata();
|
|
129
183
|
}
|
|
130
|
-
return client;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
|
|
134
|
-
this.assertIssuerData();
|
|
135
|
-
if (!this._endpointMetadata) {
|
|
136
|
-
if (this.credentialOffer) {
|
|
137
|
-
this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
|
|
138
|
-
} else if (this._credentialIssuer) {
|
|
139
|
-
this._endpointMetadata = await MetadataClient.retrieveAllMetadata(this._credentialIssuer);
|
|
140
|
-
} else {
|
|
141
|
-
throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return this.endpointMetadata;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// todo: Unify this method with the par method
|
|
148
|
-
|
|
149
|
-
public createAuthorizationRequestUrl({ codeChallengeMethod, codeChallenge, authorizationDetails, redirectUri, scope }: AuthRequestOpts): string {
|
|
150
|
-
// Scope and authorization_details can be used in the same authorization request
|
|
151
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
152
|
-
if (!scope && !authorizationDetails) {
|
|
153
|
-
if (!this.credentialOffer) {
|
|
154
|
-
throw Error('Please provide a scope or authorization_details');
|
|
155
|
-
}
|
|
156
|
-
const creds = this.credentialOffer.credential_offer.credentials;
|
|
157
|
-
|
|
158
|
-
authorizationDetails = creds
|
|
159
|
-
.flatMap((cred) => (typeof cred === 'string' ? this.getCredentialsSupported(true) : (cred as CredentialSupported)))
|
|
160
|
-
.map((cred) => {
|
|
161
|
-
return {
|
|
162
|
-
...cred,
|
|
163
|
-
type: 'openid_credential',
|
|
164
|
-
locations: [this._credentialIssuer],
|
|
165
|
-
format: cred.format,
|
|
166
|
-
} satisfies AuthDetails;
|
|
167
|
-
});
|
|
168
|
-
if (authorizationDetails.length === 0) {
|
|
169
|
-
throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
173
|
-
// handling this because of the support for v1_0-08
|
|
174
184
|
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
|
|
185
|
+
credentialOfferClient.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
|
|
186
|
+
(createAuthorizationRequestURL === undefined || createAuthorizationRequestURL)
|
|
178
187
|
) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (!this._endpointMetadata?.authorization_endpoint) {
|
|
182
|
-
throw Error('Server metadata does not contain authorization endpoint');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// add 'openid' scope if not present
|
|
186
|
-
if (!scope?.includes('openid')) {
|
|
187
|
-
scope = ['openid', scope].filter((s) => !!s).join(' ');
|
|
188
|
+
await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
|
|
189
|
+
debug(`Authorization Request URL: ${client._state.authorizationURL}`);
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
response_type: ResponseType.AUTH_CODE,
|
|
192
|
-
code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
|
|
193
|
-
code_challenge: codeChallenge,
|
|
194
|
-
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
195
|
-
redirect_uri: redirectUri,
|
|
196
|
-
scope: scope,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
if (this.clientId) {
|
|
200
|
-
queryObj['client_id'] = this.clientId;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (this.credentialOffer?.issuerState) {
|
|
204
|
-
queryObj['issuer_state'] = this.credentialOffer.issuerState;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return convertJsonToURI(queryObj, {
|
|
208
|
-
baseUrl: this._endpointMetadata.authorization_endpoint,
|
|
209
|
-
uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
|
|
210
|
-
mode: JsonURIMode.X_FORM_WWW_URLENCODED,
|
|
211
|
-
// We do not add the version here, as this always needs to be form encoded
|
|
212
|
-
});
|
|
192
|
+
return client;
|
|
213
193
|
}
|
|
214
194
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document
|
|
230
|
-
// Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow.
|
|
231
|
-
// What happens if it doesn't ???
|
|
232
|
-
// let parEndpoint: string
|
|
233
|
-
if (
|
|
234
|
-
!this._endpointMetadata?.credentialIssuerMetadata ||
|
|
235
|
-
!('pushed_authorization_request_endpoint' in this._endpointMetadata.credentialIssuerMetadata) ||
|
|
236
|
-
typeof this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint !== 'string'
|
|
237
|
-
) {
|
|
238
|
-
throw Error('Server metadata does not contain pushed authorization request endpoint');
|
|
239
|
-
}
|
|
240
|
-
const parEndpoint: string = this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint;
|
|
241
|
-
|
|
242
|
-
// add 'openid' scope if not present
|
|
243
|
-
if (!scope?.includes('openid')) {
|
|
244
|
-
scope = ['openid', scope].filter((s) => !!s).join(' ');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const queryObj: { [key: string]: string } = {
|
|
248
|
-
response_type: ResponseType.AUTH_CODE,
|
|
249
|
-
code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
|
|
250
|
-
code_challenge: codeChallenge,
|
|
251
|
-
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
252
|
-
redirect_uri: redirectUri,
|
|
253
|
-
scope: scope,
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
if (this.clientId) {
|
|
257
|
-
queryObj['client_id'] = this.clientId;
|
|
258
|
-
}
|
|
195
|
+
/**
|
|
196
|
+
* 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)
|
|
197
|
+
*
|
|
198
|
+
* 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
|
|
199
|
+
* @param opts
|
|
200
|
+
*/
|
|
201
|
+
public async createAuthorizationRequestUrl(opts?: { authorizationRequest?: AuthorizationRequestOpts; pkce?: PKCEOpts }): Promise<string> {
|
|
202
|
+
if (!this._state.authorizationURL) {
|
|
203
|
+
this.calculatePKCEOpts(opts?.pkce);
|
|
204
|
+
this._state.authorizationRequestOpts = this.syncAuthorizationRequestOpts(opts?.authorizationRequest);
|
|
205
|
+
if (!this._state.authorizationRequestOpts) {
|
|
206
|
+
throw Error(`No Authorization Request options present or provided in this call`);
|
|
207
|
+
}
|
|
259
208
|
|
|
260
|
-
|
|
261
|
-
|
|
209
|
+
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
210
|
+
// handling this because of the support for v1_0-08
|
|
211
|
+
if (
|
|
212
|
+
this._state.endpointMetadata?.credentialIssuerMetadata &&
|
|
213
|
+
'authorization_endpoint' in this._state.endpointMetadata.credentialIssuerMetadata
|
|
214
|
+
) {
|
|
215
|
+
this._state.endpointMetadata.authorization_endpoint = this._state.endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
|
|
216
|
+
}
|
|
217
|
+
this._state.authorizationURL = await createAuthorizationRequestUrl({
|
|
218
|
+
pkce: this._state.pkce,
|
|
219
|
+
endpointMetadata: this.endpointMetadata,
|
|
220
|
+
authorizationRequest: this._state.authorizationRequestOpts,
|
|
221
|
+
credentialOffer: this.credentialOffer,
|
|
222
|
+
credentialsSupported: this.getCredentialsSupported(true),
|
|
223
|
+
});
|
|
262
224
|
}
|
|
263
|
-
|
|
264
|
-
const response = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));
|
|
265
|
-
|
|
266
|
-
return convertJsonToURI(
|
|
267
|
-
{ request_uri: response.successBody?.request_uri },
|
|
268
|
-
{
|
|
269
|
-
baseUrl: this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint,
|
|
270
|
-
uriTypeProperties: ['request_uri'],
|
|
271
|
-
mode: JsonURIMode.X_FORM_WWW_URLENCODED,
|
|
272
|
-
},
|
|
273
|
-
);
|
|
225
|
+
return this._state.authorizationURL;
|
|
274
226
|
}
|
|
275
227
|
|
|
276
|
-
public
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
228
|
+
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
|
|
229
|
+
this.assertIssuerData();
|
|
230
|
+
if (!this._state.endpointMetadata) {
|
|
231
|
+
if (this.credentialOffer) {
|
|
232
|
+
this._state.endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
|
|
233
|
+
} else if (this._state.credentialIssuer) {
|
|
234
|
+
this._state.endpointMetadata = await MetadataClient.retrieveAllMetadata(this._state.credentialIssuer);
|
|
280
235
|
} else {
|
|
281
|
-
|
|
236
|
+
throw Error(`Cannot retrieve issuer metadata without either a credential offer, or issuer value`);
|
|
282
237
|
}
|
|
283
238
|
}
|
|
284
|
-
|
|
239
|
+
|
|
240
|
+
return this.endpointMetadata;
|
|
285
241
|
}
|
|
286
242
|
|
|
287
|
-
private
|
|
288
|
-
|
|
289
|
-
authorizationDetails &&
|
|
290
|
-
(this.endpointMetadata.credentialIssuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)
|
|
291
|
-
) {
|
|
292
|
-
if (authorizationDetails.locations) {
|
|
293
|
-
if (Array.isArray(authorizationDetails.locations)) {
|
|
294
|
-
(authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
|
|
295
|
-
} else {
|
|
296
|
-
authorizationDetails.locations = [authorizationDetails.locations as string, this.endpointMetadata.issuer];
|
|
297
|
-
}
|
|
298
|
-
} else {
|
|
299
|
-
authorizationDetails.locations = this.endpointMetadata.issuer;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return authorizationDetails;
|
|
243
|
+
private calculatePKCEOpts(pkce?: PKCEOpts) {
|
|
244
|
+
this._state.pkce = generateMissingPKCEOpts({ ...this._state.pkce, ...pkce });
|
|
303
245
|
}
|
|
304
246
|
|
|
305
247
|
public async acquireAccessToken(opts?: {
|
|
306
248
|
pin?: string;
|
|
307
249
|
clientId?: string;
|
|
308
250
|
codeVerifier?: string;
|
|
309
|
-
|
|
251
|
+
authorizationResponse?: string | AuthorizationResponse; // Pass in an auth response, either as URI/redirect, or object
|
|
252
|
+
code?: string; // Directly pass in a code from an auth response
|
|
310
253
|
redirectUri?: string;
|
|
311
254
|
}): Promise<AccessTokenResponse> {
|
|
312
|
-
const { pin, clientId
|
|
255
|
+
const { pin, clientId } = opts ?? {};
|
|
256
|
+
let { redirectUri } = opts ?? {};
|
|
257
|
+
const code = opts?.code ?? (opts?.authorizationResponse ? toAuthorizationResponsePayload(opts.authorizationResponse).code : undefined);
|
|
313
258
|
|
|
259
|
+
if (opts?.codeVerifier) {
|
|
260
|
+
this._state.pkce.codeVerifier = opts.codeVerifier;
|
|
261
|
+
}
|
|
314
262
|
this.assertIssuerData();
|
|
315
263
|
|
|
316
264
|
if (clientId) {
|
|
317
|
-
this.
|
|
265
|
+
this._state.clientId = clientId;
|
|
318
266
|
}
|
|
319
|
-
if (!this.
|
|
267
|
+
if (!this._state.accessTokenResponse) {
|
|
320
268
|
const accessTokenClient = new AccessTokenClient();
|
|
321
269
|
|
|
270
|
+
if (redirectUri && redirectUri !== this._state.authorizationRequestOpts?.redirectUri) {
|
|
271
|
+
console.log(
|
|
272
|
+
`Redirect URI mismatch between access-token (${redirectUri}) and authorization request (${this._state.authorizationRequestOpts?.redirectUri}). According to the specification that is not allowed.`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (this._state.authorizationRequestOpts?.redirectUri && !redirectUri) {
|
|
276
|
+
redirectUri = this._state.authorizationRequestOpts.redirectUri;
|
|
277
|
+
}
|
|
278
|
+
|
|
322
279
|
const response = await accessTokenClient.acquireAccessToken({
|
|
323
280
|
credentialOffer: this.credentialOffer,
|
|
324
281
|
metadata: this.endpointMetadata,
|
|
325
282
|
credentialIssuer: this.getIssuer(),
|
|
326
283
|
pin,
|
|
327
|
-
codeVerifier,
|
|
284
|
+
...(!this._state.pkce.disabled && { codeVerifier: this._state.pkce.codeVerifier }),
|
|
328
285
|
code,
|
|
329
286
|
redirectUri,
|
|
330
|
-
asOpts: { clientId },
|
|
287
|
+
asOpts: { clientId: this.clientId },
|
|
331
288
|
});
|
|
332
289
|
|
|
333
290
|
if (response.errorBody) {
|
|
334
291
|
debug(`Access token error:\r\n${response.errorBody}`);
|
|
335
292
|
throw Error(
|
|
336
|
-
`Retrieving an access token from ${this.
|
|
293
|
+
`Retrieving an access token from ${this._state.endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
337
294
|
response.origResponse.status
|
|
338
295
|
}`,
|
|
339
296
|
);
|
|
340
297
|
} else if (!response.successBody) {
|
|
341
298
|
debug(`Access token error. No success body`);
|
|
342
299
|
throw Error(
|
|
343
|
-
`Retrieving an access token from ${this.
|
|
300
|
+
`Retrieving an access token from ${this._state.endpointMetadata
|
|
344
301
|
?.token_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`,
|
|
345
302
|
);
|
|
346
303
|
}
|
|
347
|
-
this.
|
|
304
|
+
this._state.accessTokenResponse = response.successBody;
|
|
348
305
|
}
|
|
349
306
|
|
|
350
307
|
return this.accessTokenResponse;
|
|
@@ -352,6 +309,7 @@ export class OpenID4VCIClient {
|
|
|
352
309
|
|
|
353
310
|
public async acquireCredentials({
|
|
354
311
|
credentialTypes,
|
|
312
|
+
context,
|
|
355
313
|
proofCallbacks,
|
|
356
314
|
format,
|
|
357
315
|
kid,
|
|
@@ -362,6 +320,7 @@ export class OpenID4VCIClient {
|
|
|
362
320
|
deferredCredentialIntervalInMS,
|
|
363
321
|
}: {
|
|
364
322
|
credentialTypes: string | string[];
|
|
323
|
+
context?: string[];
|
|
365
324
|
proofCallbacks: ProofOfPossessionCallbacks<any>;
|
|
366
325
|
format?: CredentialFormat | OID4VCICredentialFormat;
|
|
367
326
|
kid?: string;
|
|
@@ -375,9 +334,9 @@ export class OpenID4VCIClient {
|
|
|
375
334
|
throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
|
|
376
335
|
}
|
|
377
336
|
|
|
378
|
-
if (alg) this.
|
|
379
|
-
if (jwk) this.
|
|
380
|
-
if (kid) this.
|
|
337
|
+
if (alg) this._state.alg = alg;
|
|
338
|
+
if (jwk) this._state.jwk = jwk;
|
|
339
|
+
if (kid) this._state.kid = kid;
|
|
381
340
|
|
|
382
341
|
const requestBuilder = this.credentialOffer
|
|
383
342
|
? CredentialRequestClientBuilder.fromCredentialOffer({
|
|
@@ -395,7 +354,7 @@ export class OpenID4VCIClient {
|
|
|
395
354
|
requestBuilder.withDeferredCredentialAwait(deferredCredentialAwait ?? false, deferredCredentialIntervalInMS);
|
|
396
355
|
if (this.endpointMetadata?.credentialIssuerMetadata) {
|
|
397
356
|
const metadata = this.endpointMetadata.credentialIssuerMetadata;
|
|
398
|
-
const types = Array.isArray(credentialTypes) ?
|
|
357
|
+
const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
|
|
399
358
|
|
|
400
359
|
if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
401
360
|
let typeSupported = false;
|
|
@@ -403,7 +362,7 @@ export class OpenID4VCIClient {
|
|
|
403
362
|
metadata.credentials_supported.forEach((supportedCredential) => {
|
|
404
363
|
const subTypes = getTypesFromCredentialSupported(supportedCredential);
|
|
405
364
|
if (
|
|
406
|
-
subTypes.
|
|
365
|
+
subTypes.every((t, i) => types[i] === t) ||
|
|
407
366
|
(types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0])))
|
|
408
367
|
) {
|
|
409
368
|
typeSupported = true;
|
|
@@ -431,11 +390,11 @@ export class OpenID4VCIClient {
|
|
|
431
390
|
.withIssuer(this.getIssuer())
|
|
432
391
|
.withAlg(this.alg);
|
|
433
392
|
|
|
434
|
-
if (this.
|
|
435
|
-
proofBuilder.withJWK(this.
|
|
393
|
+
if (this._state.jwk) {
|
|
394
|
+
proofBuilder.withJWK(this._state.jwk);
|
|
436
395
|
}
|
|
437
|
-
if (this.
|
|
438
|
-
proofBuilder.withKid(this.
|
|
396
|
+
if (this._state.kid) {
|
|
397
|
+
proofBuilder.withKid(this._state.kid);
|
|
439
398
|
}
|
|
440
399
|
|
|
441
400
|
if (this.clientId) {
|
|
@@ -446,26 +405,31 @@ export class OpenID4VCIClient {
|
|
|
446
405
|
}
|
|
447
406
|
const response = await credentialRequestClient.acquireCredentialsUsingProof({
|
|
448
407
|
proofInput: proofBuilder,
|
|
449
|
-
credentialTypes
|
|
408
|
+
credentialTypes,
|
|
409
|
+
context,
|
|
450
410
|
format,
|
|
451
411
|
});
|
|
452
412
|
if (response.errorBody) {
|
|
453
413
|
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
|
|
454
414
|
throw Error(
|
|
455
|
-
`Retrieving a credential from ${this.
|
|
415
|
+
`Retrieving a credential from ${this._state.endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
456
416
|
response.origResponse.status
|
|
457
417
|
}`,
|
|
458
418
|
);
|
|
459
419
|
} else if (!response.successBody) {
|
|
460
420
|
debug(`Credential request error. No success body`);
|
|
461
421
|
throw Error(
|
|
462
|
-
`Retrieving a credential from ${this.
|
|
422
|
+
`Retrieving a credential from ${this._state.endpointMetadata
|
|
463
423
|
?.credential_endpoint} for issuer ${this.getIssuer()} failed as there was no success response body`,
|
|
464
424
|
);
|
|
465
425
|
}
|
|
466
426
|
return response.successBody;
|
|
467
427
|
}
|
|
468
428
|
|
|
429
|
+
public async exportState(): Promise<string> {
|
|
430
|
+
return JSON.stringify(this._state);
|
|
431
|
+
}
|
|
432
|
+
|
|
469
433
|
// FIXME: We really should convert <v11 to v12 objects first. Right now the logic doesn't map nicely and is brittle.
|
|
470
434
|
// We should resolve IDs to objects first in case of strings.
|
|
471
435
|
// When < v11 convert into a v12 object. When v12 object retain it.
|
|
@@ -507,11 +471,26 @@ export class OpenID4VCIClient {
|
|
|
507
471
|
}
|
|
508
472
|
|
|
509
473
|
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
510
|
-
return
|
|
474
|
+
return (
|
|
475
|
+
this.credentialOffer?.supportedFlows ??
|
|
476
|
+
(this._state.endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : [])
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
isFlowTypeSupported(flowType: AuthzFlowType): boolean {
|
|
481
|
+
return this.issuerSupportedFlowTypes().includes(flowType);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
get authorizationURL(): string | undefined {
|
|
485
|
+
return this._state.authorizationURL;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public hasAuthorizationURL(): boolean {
|
|
489
|
+
return !!this.authorizationURL;
|
|
511
490
|
}
|
|
512
491
|
|
|
513
492
|
get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
|
|
514
|
-
return this.
|
|
493
|
+
return this._state.credentialOffer;
|
|
515
494
|
}
|
|
516
495
|
|
|
517
496
|
public version(): OpenId4VCIVersion {
|
|
@@ -521,38 +500,46 @@ export class OpenID4VCIClient {
|
|
|
521
500
|
public get endpointMetadata(): EndpointMetadataResult {
|
|
522
501
|
this.assertServerMetadata();
|
|
523
502
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
524
|
-
return this.
|
|
503
|
+
return this._state.endpointMetadata!;
|
|
525
504
|
}
|
|
526
505
|
|
|
527
506
|
get kid(): string {
|
|
528
507
|
this.assertIssuerData();
|
|
529
|
-
if (!this.
|
|
508
|
+
if (!this._state.kid) {
|
|
530
509
|
throw new Error('No value for kid is supplied');
|
|
531
510
|
}
|
|
532
|
-
return this.
|
|
511
|
+
return this._state.kid;
|
|
533
512
|
}
|
|
534
513
|
|
|
535
514
|
get alg(): string {
|
|
536
515
|
this.assertIssuerData();
|
|
537
|
-
if (!this.
|
|
516
|
+
if (!this._state.alg) {
|
|
538
517
|
throw new Error('No value for alg is supplied');
|
|
539
518
|
}
|
|
540
|
-
return this.
|
|
519
|
+
return this._state.alg;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
set clientId(value: string | undefined) {
|
|
523
|
+
this._state.clientId = value;
|
|
541
524
|
}
|
|
542
525
|
|
|
543
526
|
get clientId(): string | undefined {
|
|
544
|
-
return this.
|
|
527
|
+
return this._state.clientId;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
public hasAccessTokenResponse(): boolean {
|
|
531
|
+
return !!this._state.accessTokenResponse;
|
|
545
532
|
}
|
|
546
533
|
|
|
547
534
|
get accessTokenResponse(): AccessTokenResponse {
|
|
548
535
|
this.assertAccessToken();
|
|
549
536
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
550
|
-
return this.
|
|
537
|
+
return this._state.accessTokenResponse!;
|
|
551
538
|
}
|
|
552
539
|
|
|
553
540
|
public getIssuer(): string {
|
|
554
541
|
this.assertIssuerData();
|
|
555
|
-
return this.
|
|
542
|
+
return this._state.credentialIssuer;
|
|
556
543
|
}
|
|
557
544
|
|
|
558
545
|
public getAccessTokenEndpoint(): string {
|
|
@@ -570,27 +557,62 @@ export class OpenID4VCIClient {
|
|
|
570
557
|
public hasDeferredCredentialEndpoint(): boolean {
|
|
571
558
|
return !!this.getAccessTokenEndpoint();
|
|
572
559
|
}
|
|
560
|
+
|
|
573
561
|
public getDeferredCredentialEndpoint(): string {
|
|
574
562
|
this.assertIssuerData();
|
|
575
563
|
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
|
|
576
564
|
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Too bad we need a method like this, but EBSI is not exposing metadata
|
|
568
|
+
*/
|
|
569
|
+
public isEBSI() {
|
|
570
|
+
if (
|
|
571
|
+
this.credentialOffer?.credential_offer.credentials.find(
|
|
572
|
+
(cred) =>
|
|
573
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
574
|
+
// @ts-ignore
|
|
575
|
+
typeof cred !== 'string' && 'trust_framework' in cred && 'name' in cred.trust_framework && cred.trust_framework.name.includes('ebsi'),
|
|
576
|
+
)
|
|
577
|
+
) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
this.assertIssuerData();
|
|
581
|
+
return this.endpointMetadata.credentialIssuerMetadata?.authorization_endpoint?.includes('ebsi.eu');
|
|
582
|
+
}
|
|
583
|
+
|
|
577
584
|
private assertIssuerData(): void {
|
|
578
|
-
if (!this.
|
|
579
|
-
throw Error(`No issuance initiation or credential offer present`);
|
|
580
|
-
} else if (!this._credentialIssuer) {
|
|
585
|
+
if (!this._state.credentialIssuer) {
|
|
581
586
|
throw Error(`No credential issuer value present`);
|
|
587
|
+
} else if (!this._state.credentialOffer && this._state.endpointMetadata && this.issuerSupportedFlowTypes().length === 0) {
|
|
588
|
+
throw Error(`No issuance initiation or credential offer present`);
|
|
582
589
|
}
|
|
583
590
|
}
|
|
584
591
|
|
|
585
592
|
private assertServerMetadata(): void {
|
|
586
|
-
if (!this.
|
|
593
|
+
if (!this._state.endpointMetadata) {
|
|
587
594
|
throw Error('No server metadata');
|
|
588
595
|
}
|
|
589
596
|
}
|
|
590
597
|
|
|
591
598
|
private assertAccessToken(): void {
|
|
592
|
-
if (!this.
|
|
599
|
+
if (!this._state.accessTokenResponse) {
|
|
593
600
|
throw Error(`No access token present`);
|
|
594
601
|
}
|
|
595
602
|
}
|
|
603
|
+
|
|
604
|
+
private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts): AuthorizationRequestOpts {
|
|
605
|
+
let authorizationRequestOpts = { ...this._state?.authorizationRequestOpts, ...opts } as AuthorizationRequestOpts;
|
|
606
|
+
if (!authorizationRequestOpts) {
|
|
607
|
+
// We only set a redirectUri if no options are provided.
|
|
608
|
+
// Note that this only works for mobile apps, that can handle a code query param on the default openid-credential-offer deeplink.
|
|
609
|
+
// Provide your own options if that is not desired!
|
|
610
|
+
authorizationRequestOpts = { redirectUri: `${DefaultURISchemes.CREDENTIAL_OFFER}://` };
|
|
611
|
+
}
|
|
612
|
+
const clientId = authorizationRequestOpts.clientId ?? this._state.clientId;
|
|
613
|
+
// sync clientId
|
|
614
|
+
this._state.clientId = clientId;
|
|
615
|
+
authorizationRequestOpts.clientId = clientId;
|
|
616
|
+
return authorizationRequestOpts;
|
|
617
|
+
}
|
|
596
618
|
}
|