@sphereon/oid4vci-client 0.2.0 → 0.4.1-next.285
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/LICENSE +201 -201
- package/README.md +494 -371
- package/dist/AccessTokenClient.d.ts +30 -0
- package/dist/AccessTokenClient.d.ts.map +1 -0
- package/dist/AccessTokenClient.js +222 -0
- package/dist/AccessTokenClient.js.map +1 -0
- package/dist/AuthorizationDetailsBuilder.d.ts +11 -0
- package/dist/AuthorizationDetailsBuilder.d.ts.map +1 -0
- package/dist/AuthorizationDetailsBuilder.js +44 -0
- package/dist/AuthorizationDetailsBuilder.js.map +1 -0
- package/dist/CredentialOfferClient.d.ts +10 -0
- package/dist/CredentialOfferClient.d.ts.map +1 -0
- package/dist/CredentialOfferClient.js +101 -0
- package/dist/CredentialOfferClient.js.map +1 -0
- package/dist/CredentialRequestClient.d.ts +33 -0
- package/dist/CredentialRequestClient.d.ts.map +1 -0
- package/dist/CredentialRequestClient.js +118 -0
- package/dist/CredentialRequestClient.js.map +1 -0
- package/dist/CredentialRequestClientBuilder.d.ts +34 -0
- package/dist/CredentialRequestClientBuilder.d.ts.map +1 -0
- package/dist/CredentialRequestClientBuilder.js +87 -0
- package/dist/CredentialRequestClientBuilder.js.map +1 -0
- package/dist/{main/lib/MetadataClient.d.ts → MetadataClient.d.ts} +39 -38
- package/dist/MetadataClient.d.ts.map +1 -0
- package/dist/MetadataClient.js +148 -0
- package/dist/MetadataClient.js.map +1 -0
- package/dist/OpenID4VCIClient.d.ts +75 -0
- package/dist/OpenID4VCIClient.d.ts.map +1 -0
- package/dist/OpenID4VCIClient.js +403 -0
- package/dist/OpenID4VCIClient.js.map +1 -0
- package/dist/ProofOfPossessionBuilder.d.ts +38 -0
- package/dist/ProofOfPossessionBuilder.d.ts.map +1 -0
- package/dist/ProofOfPossessionBuilder.js +129 -0
- package/dist/ProofOfPossessionBuilder.js.map +1 -0
- package/dist/functions/ProofUtil.d.ts +29 -0
- package/dist/functions/ProofUtil.d.ts.map +1 -0
- package/dist/functions/ProofUtil.js +104 -0
- package/dist/functions/ProofUtil.js.map +1 -0
- package/dist/functions/index.d.ts +4 -0
- package/dist/functions/index.d.ts.map +1 -0
- package/dist/{main → functions}/index.js +20 -18
- package/dist/functions/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/{main/lib/index.js → index.js} +25 -24
- package/dist/index.js.map +1 -0
- package/lib/AccessTokenClient.ts +249 -0
- package/lib/AuthorizationDetailsBuilder.ts +46 -0
- package/lib/CredentialOfferClient.ts +108 -0
- package/lib/CredentialRequestClient.ts +137 -0
- package/lib/CredentialRequestClientBuilder.ts +110 -0
- package/lib/MetadataClient.ts +147 -0
- package/lib/OpenID4VCIClient.ts +523 -0
- package/lib/ProofOfPossessionBuilder.ts +181 -0
- package/lib/__tests__/AccessTokenClient.spec.ts +225 -0
- package/lib/__tests__/AuthorizationDetailsBuilder.spec.ts +65 -0
- package/lib/__tests__/AuthzFlowType.spec.ts +39 -0
- package/lib/__tests__/CredentialRequestClient.spec.ts +291 -0
- package/lib/__tests__/CredentialRequestClientBuilder.spec.ts +121 -0
- package/lib/__tests__/HttpUtils.spec.ts +37 -0
- package/lib/__tests__/IT.spec.ts +173 -0
- package/lib/__tests__/IssuanceInitiation.spec.ts +48 -0
- package/lib/__tests__/JsonURIConversions.spec.ts +146 -0
- package/lib/__tests__/MetadataClient.spec.ts +203 -0
- package/lib/__tests__/MetadataMocks.ts +444 -0
- package/lib/__tests__/OpenID4VCIClient.spec.ts +166 -0
- package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +112 -0
- package/lib/__tests__/ProofOfPossessionBuilder.spec.ts +110 -0
- package/lib/__tests__/data/VciDataFixtures.ts +744 -0
- package/lib/functions/ProofUtil.ts +120 -0
- package/lib/functions/index.ts +3 -0
- package/{dist/main/lib/index.d.ts → lib/index.ts} +8 -7
- package/package.json +68 -71
- package/CHANGELOG.md +0 -21
- package/dist/main/index.d.ts +0 -1
- package/dist/main/lib/AccessTokenClient.d.ts +0 -20
- package/dist/main/lib/AccessTokenClient.js +0 -141
- package/dist/main/lib/CredentialRequestClient.d.ts +0 -31
- package/dist/main/lib/CredentialRequestClient.js +0 -66
- package/dist/main/lib/CredentialRequestClientBuilder.d.ts +0 -21
- package/dist/main/lib/CredentialRequestClientBuilder.js +0 -56
- package/dist/main/lib/IssuanceInitiation.d.ts +0 -5
- package/dist/main/lib/IssuanceInitiation.js +0 -29
- package/dist/main/lib/MetadataClient.js +0 -127
- package/dist/main/lib/functions/Encoding.d.ts +0 -17
- package/dist/main/lib/functions/Encoding.js +0 -138
- package/dist/main/lib/functions/HttpUtils.d.ts +0 -17
- package/dist/main/lib/functions/HttpUtils.js +0 -133
- package/dist/main/lib/functions/ProofUtil.d.ts +0 -9
- package/dist/main/lib/functions/ProofUtil.js +0 -76
- package/dist/main/lib/functions/index.d.ts +0 -3
- package/dist/main/lib/functions/index.js +0 -20
- package/dist/main/lib/types/Authorization.types.d.ts +0 -66
- package/dist/main/lib/types/Authorization.types.js +0 -35
- package/dist/main/lib/types/CredentialIssuance.types.d.ts +0 -88
- package/dist/main/lib/types/CredentialIssuance.types.js +0 -8
- package/dist/main/lib/types/Generic.types.d.ts +0 -19
- package/dist/main/lib/types/Generic.types.js +0 -11
- package/dist/main/lib/types/OAuth2ASMetadata.d.ts +0 -37
- package/dist/main/lib/types/OAuth2ASMetadata.js +0 -3
- package/dist/main/lib/types/OID4VCIServerMetadata.d.ts +0 -65
- package/dist/main/lib/types/OID4VCIServerMetadata.js +0 -3
- package/dist/main/lib/types/Oidc4vciErrors.d.ts +0 -3
- package/dist/main/lib/types/Oidc4vciErrors.js +0 -7
- package/dist/main/lib/types/index.d.ts +0 -6
- package/dist/main/lib/types/index.js +0 -23
- package/dist/main/tsconfig.build.tsbuildinfo +0 -1
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessTokenResponse,
|
|
3
|
+
Alg,
|
|
4
|
+
AuthorizationRequestV1_0_09,
|
|
5
|
+
AuthzFlowType,
|
|
6
|
+
CodeChallengeMethod,
|
|
7
|
+
CredentialOfferPayloadV1_0_08,
|
|
8
|
+
CredentialOfferRequestWithBaseUrl,
|
|
9
|
+
CredentialResponse,
|
|
10
|
+
CredentialSupported,
|
|
11
|
+
EndpointMetadata,
|
|
12
|
+
OID4VCICredentialFormat,
|
|
13
|
+
OpenId4VCIVersion,
|
|
14
|
+
OpenIDResponse,
|
|
15
|
+
ProofOfPossessionCallbacks,
|
|
16
|
+
PushedAuthorizationResponse,
|
|
17
|
+
ResponseType,
|
|
18
|
+
} from '@sphereon/oid4vci-common';
|
|
19
|
+
import { getSupportedCredentials } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
|
|
20
|
+
import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types';
|
|
21
|
+
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
22
|
+
import Debug from 'debug';
|
|
23
|
+
|
|
24
|
+
import { AccessTokenClient } from './AccessTokenClient';
|
|
25
|
+
import { CredentialOfferClient } from './CredentialOfferClient';
|
|
26
|
+
import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
|
|
27
|
+
import { MetadataClient } from './MetadataClient';
|
|
28
|
+
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
|
|
29
|
+
import { convertJsonToURI, formPost } from './functions';
|
|
30
|
+
|
|
31
|
+
const debug = Debug('sphereon:oid4vci');
|
|
32
|
+
|
|
33
|
+
interface AuthDetails {
|
|
34
|
+
type: 'openid_credential' | string;
|
|
35
|
+
locations?: string | string[];
|
|
36
|
+
format: CredentialFormat | CredentialFormat[];
|
|
37
|
+
|
|
38
|
+
[s: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface AuthRequestOpts {
|
|
42
|
+
clientId: string;
|
|
43
|
+
codeChallenge: string;
|
|
44
|
+
codeChallengeMethod: CodeChallengeMethod;
|
|
45
|
+
authorizationDetails?: AuthDetails | AuthDetails[];
|
|
46
|
+
redirectUri: string;
|
|
47
|
+
scope?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class OpenID4VCIClient {
|
|
51
|
+
private readonly _flowType: AuthzFlowType;
|
|
52
|
+
private readonly _credentialOffer: CredentialOfferRequestWithBaseUrl;
|
|
53
|
+
private _clientId?: string;
|
|
54
|
+
private _kid: string | undefined;
|
|
55
|
+
private _alg: Alg | string | undefined;
|
|
56
|
+
private _endpointMetadata: EndpointMetadata | undefined;
|
|
57
|
+
private _accessTokenResponse: AccessTokenResponse | undefined;
|
|
58
|
+
|
|
59
|
+
private constructor(
|
|
60
|
+
credentialOffer: CredentialOfferRequestWithBaseUrl,
|
|
61
|
+
flowType: AuthzFlowType,
|
|
62
|
+
kid?: string,
|
|
63
|
+
alg?: Alg | string,
|
|
64
|
+
clientId?: string
|
|
65
|
+
) {
|
|
66
|
+
if (!credentialOffer.supportedFlows.includes(flowType)) {
|
|
67
|
+
throw Error(`Flows ${flowType} is not supported by issuer ${credentialOffer.credential_offer_uri}`);
|
|
68
|
+
}
|
|
69
|
+
this._flowType = flowType;
|
|
70
|
+
this._credentialOffer = credentialOffer;
|
|
71
|
+
this._kid = kid;
|
|
72
|
+
this._alg = alg;
|
|
73
|
+
this._clientId = clientId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public static async fromURI({
|
|
77
|
+
uri,
|
|
78
|
+
flowType,
|
|
79
|
+
kid,
|
|
80
|
+
alg,
|
|
81
|
+
retrieveServerMetadata,
|
|
82
|
+
clientId,
|
|
83
|
+
resolveOfferUri,
|
|
84
|
+
}: {
|
|
85
|
+
uri: string;
|
|
86
|
+
flowType: AuthzFlowType;
|
|
87
|
+
kid?: string;
|
|
88
|
+
alg?: Alg | string;
|
|
89
|
+
retrieveServerMetadata?: boolean;
|
|
90
|
+
resolveOfferUri?: boolean;
|
|
91
|
+
clientId?: string;
|
|
92
|
+
}): Promise<OpenID4VCIClient> {
|
|
93
|
+
const client = new OpenID4VCIClient(await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }), flowType, kid, alg, clientId);
|
|
94
|
+
|
|
95
|
+
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
96
|
+
await client.retrieveServerMetadata();
|
|
97
|
+
}
|
|
98
|
+
return client;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public async retrieveServerMetadata(): Promise<EndpointMetadata> {
|
|
102
|
+
this.assertIssuerData();
|
|
103
|
+
if (!this._endpointMetadata) {
|
|
104
|
+
this._endpointMetadata = await MetadataClient.retrieveAllMetadataFromCredentialOffer(this.credentialOffer);
|
|
105
|
+
}
|
|
106
|
+
return this.endpointMetadata;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public createAuthorizationRequestUrl({
|
|
110
|
+
clientId,
|
|
111
|
+
codeChallengeMethod,
|
|
112
|
+
codeChallenge,
|
|
113
|
+
authorizationDetails,
|
|
114
|
+
redirectUri,
|
|
115
|
+
scope,
|
|
116
|
+
}: AuthRequestOpts): string {
|
|
117
|
+
// Scope and authorization_details can be used in the same authorization request
|
|
118
|
+
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
119
|
+
if (!scope && !authorizationDetails) {
|
|
120
|
+
throw Error('Please provide a scope or authorization_details');
|
|
121
|
+
}
|
|
122
|
+
// todo: handling this because of the support for v1_0-08
|
|
123
|
+
if (this._endpointMetadata && this._endpointMetadata.issuerMetadata && 'authorization_endpoint' in this._endpointMetadata.issuerMetadata) {
|
|
124
|
+
this._endpointMetadata.authorization_endpoint = this._endpointMetadata.issuerMetadata.authorization_endpoint as string;
|
|
125
|
+
}
|
|
126
|
+
if (!this._endpointMetadata?.authorization_endpoint) {
|
|
127
|
+
throw Error('Server metadata does not contain authorization endpoint');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// add 'openid' scope if not present
|
|
131
|
+
if (scope && !scope.includes('openid')) {
|
|
132
|
+
scope = `openid ${scope}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//fixme: handle this for v11
|
|
136
|
+
const queryObj = {
|
|
137
|
+
response_type: ResponseType.AUTH_CODE,
|
|
138
|
+
client_id: clientId,
|
|
139
|
+
code_challenge_method: codeChallengeMethod,
|
|
140
|
+
code_challenge: codeChallenge,
|
|
141
|
+
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
142
|
+
redirect_uri: redirectUri,
|
|
143
|
+
scope: scope,
|
|
144
|
+
} as AuthorizationRequestV1_0_09;
|
|
145
|
+
|
|
146
|
+
return convertJsonToURI(queryObj, {
|
|
147
|
+
baseUrl: this._endpointMetadata.authorization_endpoint,
|
|
148
|
+
uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details'],
|
|
149
|
+
version: this.version(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public async acquirePushedAuthorizationRequestURI({
|
|
154
|
+
clientId,
|
|
155
|
+
codeChallengeMethod,
|
|
156
|
+
codeChallenge,
|
|
157
|
+
authorizationDetails,
|
|
158
|
+
redirectUri,
|
|
159
|
+
scope,
|
|
160
|
+
}: AuthRequestOpts): Promise<OpenIDResponse<PushedAuthorizationResponse>> {
|
|
161
|
+
// Scope and authorization_details can be used in the same authorization request
|
|
162
|
+
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
163
|
+
if (!scope && !authorizationDetails) {
|
|
164
|
+
throw Error('Please provide a scope or authorization_details');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document
|
|
168
|
+
// Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow.
|
|
169
|
+
// What happens if it doesn't ???
|
|
170
|
+
// let parEndpoint: string
|
|
171
|
+
if (
|
|
172
|
+
!this._endpointMetadata?.issuerMetadata ||
|
|
173
|
+
!('pushed_authorization_request_endpoint' in this._endpointMetadata.issuerMetadata) ||
|
|
174
|
+
typeof this._endpointMetadata.issuerMetadata.pushed_authorization_request_endpoint !== 'string'
|
|
175
|
+
) {
|
|
176
|
+
throw Error('Server metadata does not contain pushed authorization request endpoint');
|
|
177
|
+
}
|
|
178
|
+
const parEndpoint: string = this._endpointMetadata.issuerMetadata.pushed_authorization_request_endpoint;
|
|
179
|
+
|
|
180
|
+
// add 'openid' scope if not present
|
|
181
|
+
if (scope && !scope.includes('openid')) {
|
|
182
|
+
scope = `openid ${scope}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//fixme: handle this for v11
|
|
186
|
+
const queryObj: AuthorizationRequestV1_0_09 = {
|
|
187
|
+
response_type: ResponseType.AUTH_CODE,
|
|
188
|
+
client_id: clientId,
|
|
189
|
+
code_challenge_method: codeChallengeMethod,
|
|
190
|
+
code_challenge: codeChallenge,
|
|
191
|
+
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
192
|
+
redirect_uri: redirectUri,
|
|
193
|
+
scope: scope,
|
|
194
|
+
};
|
|
195
|
+
return await formPost(parEndpoint, JSON.stringify(queryObj));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined {
|
|
199
|
+
if (authorizationDetails) {
|
|
200
|
+
if (Array.isArray(authorizationDetails)) {
|
|
201
|
+
return authorizationDetails.map((value) => this.handleLocations({ ...value }));
|
|
202
|
+
} else {
|
|
203
|
+
return this.handleLocations({ ...authorizationDetails });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return authorizationDetails;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private handleLocations(authorizationDetails: AuthDetails) {
|
|
210
|
+
if (authorizationDetails && (this.endpointMetadata.issuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)) {
|
|
211
|
+
if (authorizationDetails.locations) {
|
|
212
|
+
if (Array.isArray(authorizationDetails.locations)) {
|
|
213
|
+
(authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
|
|
214
|
+
} else {
|
|
215
|
+
authorizationDetails.locations = [authorizationDetails.locations as string, this.endpointMetadata.issuer];
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
authorizationDetails.locations = this.endpointMetadata.issuer;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return authorizationDetails;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public async acquireAccessToken(opts?: {
|
|
225
|
+
pin?: string;
|
|
226
|
+
clientId?: string;
|
|
227
|
+
codeVerifier?: string;
|
|
228
|
+
code?: string;
|
|
229
|
+
redirectUri?: string;
|
|
230
|
+
}): Promise<AccessTokenResponse> {
|
|
231
|
+
const { pin, clientId, codeVerifier, code, redirectUri } = opts ?? {};
|
|
232
|
+
this.assertIssuerData();
|
|
233
|
+
if (clientId) {
|
|
234
|
+
this._clientId = clientId;
|
|
235
|
+
}
|
|
236
|
+
if (!this._accessTokenResponse) {
|
|
237
|
+
const accessTokenClient = new AccessTokenClient();
|
|
238
|
+
|
|
239
|
+
const response = await accessTokenClient.acquireAccessToken({
|
|
240
|
+
credentialOffer: this.credentialOffer,
|
|
241
|
+
metadata: this.endpointMetadata,
|
|
242
|
+
pin,
|
|
243
|
+
codeVerifier,
|
|
244
|
+
code,
|
|
245
|
+
redirectUri,
|
|
246
|
+
asOpts: { clientId },
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (response.errorBody) {
|
|
250
|
+
debug(`Access token error:\r\n${response.errorBody}`);
|
|
251
|
+
throw Error(
|
|
252
|
+
`Retrieving an access token from ${this._endpointMetadata?.token_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
253
|
+
response.origResponse.status
|
|
254
|
+
}`
|
|
255
|
+
);
|
|
256
|
+
} else if (!response.successBody) {
|
|
257
|
+
debug(`Access token error. No success body`);
|
|
258
|
+
throw Error(
|
|
259
|
+
`Retrieving an access token from ${
|
|
260
|
+
this._endpointMetadata?.token_endpoint
|
|
261
|
+
} for issuer ${this.getIssuer()} failed as there was no success response body`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
this._accessTokenResponse = response.successBody;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this.accessTokenResponse;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public async acquireCredentials({
|
|
271
|
+
credentialTypes,
|
|
272
|
+
proofCallbacks,
|
|
273
|
+
format,
|
|
274
|
+
kid,
|
|
275
|
+
alg,
|
|
276
|
+
jti,
|
|
277
|
+
}: {
|
|
278
|
+
credentialTypes: string | string[];
|
|
279
|
+
proofCallbacks: ProofOfPossessionCallbacks;
|
|
280
|
+
format?: CredentialFormat | OID4VCICredentialFormat;
|
|
281
|
+
kid?: string;
|
|
282
|
+
alg?: Alg | string;
|
|
283
|
+
jti?: string;
|
|
284
|
+
}): Promise<CredentialResponse> {
|
|
285
|
+
if (alg) {
|
|
286
|
+
this._alg = alg;
|
|
287
|
+
}
|
|
288
|
+
if (kid) {
|
|
289
|
+
this._kid = kid;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const requestBuilder = CredentialRequestClientBuilder.fromCredentialOffer({
|
|
293
|
+
credentialOffer: this.credentialOffer,
|
|
294
|
+
metadata: this.endpointMetadata,
|
|
295
|
+
});
|
|
296
|
+
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
|
|
297
|
+
if (this.endpointMetadata?.issuerMetadata) {
|
|
298
|
+
const metadata = this.endpointMetadata.issuerMetadata;
|
|
299
|
+
const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
|
|
300
|
+
if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
301
|
+
for (const type of types) {
|
|
302
|
+
let typeSupported = false;
|
|
303
|
+
for (const credentialSupported of metadata.credentials_supported) {
|
|
304
|
+
if (!credentialSupported.types || credentialSupported.types.length === 0) {
|
|
305
|
+
throw Error('types is required in the credentials supported');
|
|
306
|
+
}
|
|
307
|
+
if (credentialSupported.types.indexOf(type) != -1) {
|
|
308
|
+
typeSupported = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!typeSupported) {
|
|
312
|
+
throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) {
|
|
316
|
+
const credentialsSupported = metadata.credentials_supported as CredentialSupportedTypeV1_0_08;
|
|
317
|
+
if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) {
|
|
318
|
+
throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// todo: Format check? We might end up with some disjoint type / format combinations supported by the server
|
|
322
|
+
}
|
|
323
|
+
const credentialRequestClient = requestBuilder.build();
|
|
324
|
+
const proofBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({
|
|
325
|
+
accessTokenResponse: this.accessTokenResponse,
|
|
326
|
+
callbacks: proofCallbacks,
|
|
327
|
+
version: this.version(),
|
|
328
|
+
})
|
|
329
|
+
.withIssuer(this.getIssuer())
|
|
330
|
+
.withAlg(this.alg)
|
|
331
|
+
.withKid(this.kid);
|
|
332
|
+
|
|
333
|
+
if (this.clientId) {
|
|
334
|
+
proofBuilder.withClientId(this.clientId);
|
|
335
|
+
}
|
|
336
|
+
if (jti) {
|
|
337
|
+
proofBuilder.withJti(jti);
|
|
338
|
+
}
|
|
339
|
+
const response = await credentialRequestClient.acquireCredentialsUsingProof({
|
|
340
|
+
proofInput: proofBuilder,
|
|
341
|
+
credentialTypes: credentialTypes,
|
|
342
|
+
format,
|
|
343
|
+
});
|
|
344
|
+
if (response.errorBody) {
|
|
345
|
+
debug(`Credential request error:\r\n${response.errorBody}`);
|
|
346
|
+
throw Error(
|
|
347
|
+
`Retrieving a credential from ${this._endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
348
|
+
response.origResponse.status
|
|
349
|
+
}`
|
|
350
|
+
);
|
|
351
|
+
} else if (!response.successBody) {
|
|
352
|
+
debug(`Credential request error. No success body`);
|
|
353
|
+
throw Error(
|
|
354
|
+
`Retrieving a credential from ${
|
|
355
|
+
this._endpointMetadata?.credential_endpoint
|
|
356
|
+
} for issuer ${this.getIssuer()} failed as there was no success response body`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return response.successBody;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getCredentialsSupported(restrictToInitiationTypes: boolean, supportedType?: string): CredentialSupported[] {
|
|
363
|
+
return getSupportedCredentials({
|
|
364
|
+
issuerMetadata: this.endpointMetadata.issuerMetadata,
|
|
365
|
+
version: this.version(),
|
|
366
|
+
supportedType,
|
|
367
|
+
credentialTypes: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
|
|
368
|
+
});
|
|
369
|
+
/*//FIXME: delegate to getCredentialsSupported from IssuerMetadataUtils
|
|
370
|
+
let credentialsSupported = this.endpointMetadata?.issuerMetadata?.credentials_supported
|
|
371
|
+
|
|
372
|
+
if (this.version() === OpenId4VCIVersion.VER_1_0_08 || typeof credentialsSupported === 'object') {
|
|
373
|
+
const issuerMetadata = this.endpointMetadata.issuerMetadata as IssuerMetadataV1_0_08
|
|
374
|
+
const v8CredentialsSupported = issuerMetadata.credentials_supported
|
|
375
|
+
credentialsSupported = []
|
|
376
|
+
credentialsSupported = Object.entries(v8CredentialsSupported).map((key, value) => )
|
|
377
|
+
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
if (!credentialsSupported) {
|
|
382
|
+
return []
|
|
383
|
+
} else if (!restrictToInitiationTypes) {
|
|
384
|
+
return credentialsSupported
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
/!**
|
|
390
|
+
* the following (not array part is a legacy code from version 1_0-08 which jff implementors used)
|
|
391
|
+
*!/
|
|
392
|
+
if (!Array.isArray(credentialsSupported)) {
|
|
393
|
+
const credentialsSupportedV8: CredentialSupportedV1_0_08 = credentialsSupported as CredentialSupportedV1_0_08;
|
|
394
|
+
const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes();
|
|
395
|
+
const supported: IssuerCredentialSubject = {};
|
|
396
|
+
for (const [key, value] of Object.entries(credentialsSupportedV8)) {
|
|
397
|
+
if (initiationTypes.includes(key)) {
|
|
398
|
+
supported[key] = value;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// todo: fix this later. we're returning CredentialSupportedV1_0_08 as a list of CredentialSupported (for v09 onward)
|
|
402
|
+
return supported as unknown as CredentialSupported[];
|
|
403
|
+
}
|
|
404
|
+
const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes()
|
|
405
|
+
const credentialSupportedOverlap: CredentialSupported[] = []
|
|
406
|
+
for (const supported of credentialsSupported) {
|
|
407
|
+
const supportedTypeOverlap: string[] = []
|
|
408
|
+
for (const type of supported.types) {
|
|
409
|
+
initiationTypes.includes(type)
|
|
410
|
+
supportedTypeOverlap.push(type)
|
|
411
|
+
}
|
|
412
|
+
if (supportedTypeOverlap.length > 0) {
|
|
413
|
+
credentialSupportedOverlap.push({
|
|
414
|
+
...supported,
|
|
415
|
+
types: supportedTypeOverlap
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return credentialSupportedOverlap as CredentialSupported[]*/
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getCredentialMetadata(type: string): CredentialSupported[] {
|
|
423
|
+
return this.getCredentialsSupported(false, type);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// todo https://sphereon.atlassian.net/browse/VDX-184
|
|
427
|
+
getCredentialTypes(): string[] {
|
|
428
|
+
if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
429
|
+
return typeof (this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type === 'string'
|
|
430
|
+
? [(this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string]
|
|
431
|
+
: ((this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type as string[]);
|
|
432
|
+
} else {
|
|
433
|
+
// FIXME: this for sure isn't correct. It would also include VerifiableCredential. The whole call to this getCredentialsTypes should be changed to begin with
|
|
434
|
+
return this.credentialOffer.credential_offer.credentials.flatMap((c) => (typeof c === 'string' ? c : c.types));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
get flowType(): AuthzFlowType {
|
|
439
|
+
return this._flowType;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
443
|
+
return this.credentialOffer.supportedFlows;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
get credentialOffer(): CredentialOfferRequestWithBaseUrl {
|
|
447
|
+
return this._credentialOffer;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public version(): OpenId4VCIVersion {
|
|
451
|
+
return this.credentialOffer.version;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public get endpointMetadata(): EndpointMetadata {
|
|
455
|
+
this.assertServerMetadata();
|
|
456
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
457
|
+
return this._endpointMetadata!;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
get kid(): string {
|
|
461
|
+
this.assertIssuerData();
|
|
462
|
+
if (!this._kid) {
|
|
463
|
+
throw new Error('No value for kid is supplied');
|
|
464
|
+
}
|
|
465
|
+
return this._kid;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
get alg(): string {
|
|
469
|
+
this.assertIssuerData();
|
|
470
|
+
if (!this._alg) {
|
|
471
|
+
throw new Error('No value for alg is supplied');
|
|
472
|
+
}
|
|
473
|
+
return this._alg;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
get clientId(): string | undefined {
|
|
477
|
+
/*if (!this._clientId) {
|
|
478
|
+
throw Error('No client id present');
|
|
479
|
+
}*/
|
|
480
|
+
return this._clientId;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
get accessTokenResponse(): AccessTokenResponse {
|
|
484
|
+
this.assertAccessToken();
|
|
485
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
486
|
+
return this._accessTokenResponse!;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
public getIssuer(): string {
|
|
490
|
+
this.assertIssuerData();
|
|
491
|
+
return this._endpointMetadata ? this.endpointMetadata.issuer : this.getIssuer();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
public getAccessTokenEndpoint(): string {
|
|
495
|
+
this.assertIssuerData();
|
|
496
|
+
return this.endpointMetadata
|
|
497
|
+
? this.endpointMetadata.token_endpoint
|
|
498
|
+
: AccessTokenClient.determineTokenURL({ issuerOpts: { issuer: this.getIssuer() } });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
public getCredentialEndpoint(): string {
|
|
502
|
+
this.assertIssuerData();
|
|
503
|
+
return this.endpointMetadata ? this.endpointMetadata.credential_endpoint : `${this.getIssuer()}/credential`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private assertIssuerData(): void {
|
|
507
|
+
if (!this._credentialOffer) {
|
|
508
|
+
throw Error(`No issuance initiation or credential offer present`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private assertServerMetadata(): void {
|
|
513
|
+
if (!this._endpointMetadata) {
|
|
514
|
+
throw Error('No server metadata');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private assertAccessToken(): void {
|
|
519
|
+
if (!this._accessTokenResponse) {
|
|
520
|
+
throw Error(`No access token present`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccessTokenResponse,
|
|
3
|
+
Alg,
|
|
4
|
+
EndpointMetadata,
|
|
5
|
+
Jwt,
|
|
6
|
+
NO_JWT_PROVIDED,
|
|
7
|
+
OpenId4VCIVersion,
|
|
8
|
+
PROOF_CANT_BE_CONSTRUCTED,
|
|
9
|
+
ProofOfPossession,
|
|
10
|
+
ProofOfPossessionCallbacks,
|
|
11
|
+
Typ,
|
|
12
|
+
} from '@sphereon/oid4vci-common';
|
|
13
|
+
|
|
14
|
+
import { createProofOfPossession } from './functions';
|
|
15
|
+
|
|
16
|
+
export class ProofOfPossessionBuilder {
|
|
17
|
+
private readonly proof?: ProofOfPossession;
|
|
18
|
+
private readonly callbacks?: ProofOfPossessionCallbacks;
|
|
19
|
+
|
|
20
|
+
private version: OpenId4VCIVersion;
|
|
21
|
+
|
|
22
|
+
private kid?: string;
|
|
23
|
+
private clientId?: string;
|
|
24
|
+
private issuer?: string;
|
|
25
|
+
private jwt?: Jwt;
|
|
26
|
+
private alg?: string;
|
|
27
|
+
private jti?: string;
|
|
28
|
+
private cNonce?: string;
|
|
29
|
+
private typ?: Typ;
|
|
30
|
+
|
|
31
|
+
private constructor({
|
|
32
|
+
proof,
|
|
33
|
+
callbacks,
|
|
34
|
+
jwt,
|
|
35
|
+
accessTokenResponse,
|
|
36
|
+
version,
|
|
37
|
+
}: {
|
|
38
|
+
proof?: ProofOfPossession;
|
|
39
|
+
callbacks?: ProofOfPossessionCallbacks;
|
|
40
|
+
accessTokenResponse?: AccessTokenResponse;
|
|
41
|
+
jwt?: Jwt;
|
|
42
|
+
version: OpenId4VCIVersion;
|
|
43
|
+
}) {
|
|
44
|
+
this.proof = proof;
|
|
45
|
+
this.callbacks = callbacks;
|
|
46
|
+
if (jwt) {
|
|
47
|
+
this.withJwt(jwt);
|
|
48
|
+
}
|
|
49
|
+
if (accessTokenResponse) {
|
|
50
|
+
this.withAccessTokenResponse(accessTokenResponse);
|
|
51
|
+
}
|
|
52
|
+
this.version = version;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static fromJwt({
|
|
56
|
+
jwt,
|
|
57
|
+
callbacks,
|
|
58
|
+
version,
|
|
59
|
+
}: {
|
|
60
|
+
jwt: Jwt;
|
|
61
|
+
callbacks: ProofOfPossessionCallbacks;
|
|
62
|
+
version: OpenId4VCIVersion;
|
|
63
|
+
}): ProofOfPossessionBuilder {
|
|
64
|
+
return new ProofOfPossessionBuilder({ callbacks, jwt, version });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static fromAccessTokenResponse({
|
|
68
|
+
accessTokenResponse,
|
|
69
|
+
callbacks,
|
|
70
|
+
version,
|
|
71
|
+
}: {
|
|
72
|
+
accessTokenResponse: AccessTokenResponse;
|
|
73
|
+
callbacks: ProofOfPossessionCallbacks;
|
|
74
|
+
version: OpenId4VCIVersion;
|
|
75
|
+
}): ProofOfPossessionBuilder {
|
|
76
|
+
return new ProofOfPossessionBuilder({ callbacks, accessTokenResponse, version });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static fromProof(proof: ProofOfPossession, version: OpenId4VCIVersion): ProofOfPossessionBuilder {
|
|
80
|
+
return new ProofOfPossessionBuilder({ proof, version });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
withClientId(clientId: string): ProofOfPossessionBuilder {
|
|
84
|
+
this.clientId = clientId;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
withKid(kid: string): ProofOfPossessionBuilder {
|
|
89
|
+
this.kid = kid;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
withIssuer(issuer: string): ProofOfPossessionBuilder {
|
|
94
|
+
this.issuer = issuer;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
withAlg(alg: Alg | string): ProofOfPossessionBuilder {
|
|
99
|
+
this.alg = alg;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
withJti(jti: string): ProofOfPossessionBuilder {
|
|
104
|
+
this.jti = jti;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
withTyp(typ: Typ): ProofOfPossessionBuilder {
|
|
109
|
+
this.typ = typ;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
withAccessTokenNonce(cNonce: string): ProofOfPossessionBuilder {
|
|
114
|
+
this.cNonce = cNonce;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
withAccessTokenResponse(accessToken: AccessTokenResponse): ProofOfPossessionBuilder {
|
|
119
|
+
if (accessToken.c_nonce) {
|
|
120
|
+
this.withAccessTokenNonce(accessToken.c_nonce);
|
|
121
|
+
}
|
|
122
|
+
return this;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
withEndpointMetadata(endpointMetadata: EndpointMetadata): ProofOfPossessionBuilder {
|
|
126
|
+
this.withIssuer(endpointMetadata.issuer);
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
withJwt(jwt: Jwt): ProofOfPossessionBuilder {
|
|
131
|
+
if (!jwt) {
|
|
132
|
+
throw new Error(NO_JWT_PROVIDED);
|
|
133
|
+
}
|
|
134
|
+
this.jwt = jwt;
|
|
135
|
+
if (!jwt.header) {
|
|
136
|
+
throw Error(`No JWT header present`);
|
|
137
|
+
} else if (!jwt.payload) {
|
|
138
|
+
throw Error(`No JWT payload present`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (jwt.header.kid) {
|
|
142
|
+
this.withKid(jwt.header.kid);
|
|
143
|
+
}
|
|
144
|
+
if (jwt.header.typ) {
|
|
145
|
+
this.withTyp(jwt.header.typ as Typ);
|
|
146
|
+
}
|
|
147
|
+
if (this.version >= OpenId4VCIVersion.VER_1_0_11) {
|
|
148
|
+
this.withTyp('openid4vci-proof+jwt');
|
|
149
|
+
}
|
|
150
|
+
this.withAlg(jwt.header.alg);
|
|
151
|
+
|
|
152
|
+
if (jwt.payload) {
|
|
153
|
+
if (jwt.payload.iss) this.withClientId(jwt.payload.iss);
|
|
154
|
+
if (jwt.payload.aud) this.withIssuer(jwt.payload.aud);
|
|
155
|
+
if (jwt.payload.jti) this.withJti(jwt.payload.jti);
|
|
156
|
+
if (jwt.payload.nonce) this.withAccessTokenNonce(jwt.payload.nonce);
|
|
157
|
+
}
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public async build(): Promise<ProofOfPossession> {
|
|
162
|
+
if (this.proof) {
|
|
163
|
+
return Promise.resolve(this.proof);
|
|
164
|
+
} else if (this.callbacks) {
|
|
165
|
+
return await createProofOfPossession(
|
|
166
|
+
this.callbacks,
|
|
167
|
+
{
|
|
168
|
+
typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'),
|
|
169
|
+
kid: this.kid,
|
|
170
|
+
jti: this.jti,
|
|
171
|
+
alg: this.alg,
|
|
172
|
+
issuer: this.issuer,
|
|
173
|
+
clientId: this.clientId,
|
|
174
|
+
nonce: this.cNonce,
|
|
175
|
+
},
|
|
176
|
+
this.jwt
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
throw new Error(PROOF_CANT_BE_CONSTRUCTED);
|
|
180
|
+
}
|
|
181
|
+
}
|