@sphereon/oid4vci-client 0.6.1-next.8 → 0.7.1-next.10
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/MetadataClient.d.ts +7 -5
- package/dist/MetadataClient.d.ts.map +1 -1
- package/dist/MetadataClient.js +84 -42
- package/dist/MetadataClient.js.map +1 -1
- package/dist/OpenID4VCIClient.d.ts +4 -5
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +26 -74
- package/dist/OpenID4VCIClient.js.map +1 -1
- package/lib/MetadataClient.ts +106 -59
- package/lib/OpenID4VCIClient.ts +37 -78
- package/lib/__tests__/AccessTokenClient.spec.ts +10 -1
- package/lib/__tests__/IT.spec.ts +2 -0
- package/lib/__tests__/IssuanceInitiation.spec.ts +13 -0
- package/lib/__tests__/MattrE2E.spec.test.ts +106 -0
- package/lib/__tests__/MetadataClient.spec.ts +21 -12
- package/lib/__tests__/OpenID4VCIClient.spec.ts +9 -7
- package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +7 -5
- package/package.json +7 -7
package/lib/MetadataClient.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AuthorizationServerMetadata,
|
|
3
|
+
AuthorizationServerType,
|
|
2
4
|
CredentialIssuerMetadata,
|
|
3
5
|
CredentialOfferPayload,
|
|
4
6
|
CredentialOfferRequestWithBaseUrl,
|
|
5
|
-
|
|
7
|
+
EndpointMetadataResult,
|
|
6
8
|
getIssuerFromCredentialOfferPayload,
|
|
7
|
-
OAuth2ASMetadata,
|
|
8
|
-
Oauth2ASWithOID4VCIMetadata,
|
|
9
9
|
OpenIDResponse,
|
|
10
10
|
WellKnownEndpoints,
|
|
11
11
|
} from '@sphereon/oid4vci-common';
|
|
@@ -21,7 +21,7 @@ export class MetadataClient {
|
|
|
21
21
|
*
|
|
22
22
|
* @param credentialOffer
|
|
23
23
|
*/
|
|
24
|
-
public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<
|
|
24
|
+
public static async retrieveAllMetadataFromCredentialOffer(credentialOffer: CredentialOfferRequestWithBaseUrl): Promise<EndpointMetadataResult> {
|
|
25
25
|
return MetadataClient.retrieveAllMetadataFromCredentialOfferRequest(credentialOffer.credential_offer);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -29,9 +29,10 @@ export class MetadataClient {
|
|
|
29
29
|
* Retrieve the metada using the initiation request obtained from a previous step
|
|
30
30
|
* @param request
|
|
31
31
|
*/
|
|
32
|
-
public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise<
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
public static async retrieveAllMetadataFromCredentialOfferRequest(request: CredentialOfferPayload): Promise<EndpointMetadataResult> {
|
|
33
|
+
const issuer = getIssuerFromCredentialOfferPayload(request);
|
|
34
|
+
if (issuer) {
|
|
35
|
+
return MetadataClient.retrieveAllMetadata(issuer);
|
|
35
36
|
}
|
|
36
37
|
throw new Error("can't retrieve metadata from CredentialOfferRequest. No issuer field is present");
|
|
37
38
|
}
|
|
@@ -41,75 +42,115 @@ export class MetadataClient {
|
|
|
41
42
|
* @param issuer The issuer URL
|
|
42
43
|
* @param opts
|
|
43
44
|
*/
|
|
44
|
-
public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<
|
|
45
|
-
let token_endpoint;
|
|
46
|
-
let credential_endpoint;
|
|
47
|
-
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
WellKnownEndpoints.OAUTH_AS,
|
|
61
|
-
{
|
|
62
|
-
errorOnNotFound: true,
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
token_endpoint = response.successBody?.token_endpoint;
|
|
45
|
+
public static async retrieveAllMetadata(issuer: string, opts?: { errorOnNotFound: boolean }): Promise<EndpointMetadataResult> {
|
|
46
|
+
let token_endpoint: string | undefined;
|
|
47
|
+
let credential_endpoint: string | undefined;
|
|
48
|
+
let authorization_endpoint: string | undefined;
|
|
49
|
+
let authorizationServerType: AuthorizationServerType = 'OID4VCI';
|
|
50
|
+
let authorization_server: string = issuer;
|
|
51
|
+
const oid4vciResponse = await MetadataClient.retrieveOpenID4VCIServerMetadata(issuer, { errorOnNotFound: false }); // We will handle errors later, given we will also try other metadata locations
|
|
52
|
+
let credentialIssuerMetadata = oid4vciResponse?.successBody;
|
|
53
|
+
if (credentialIssuerMetadata) {
|
|
54
|
+
debug(`Issuer ${issuer} OID4VCI well-known server metadata\r\n${JSON.stringify(credentialIssuerMetadata)}`);
|
|
55
|
+
credential_endpoint = credentialIssuerMetadata.credential_endpoint;
|
|
56
|
+
if (credentialIssuerMetadata.token_endpoint) {
|
|
57
|
+
token_endpoint = credentialIssuerMetadata.token_endpoint;
|
|
58
|
+
}
|
|
59
|
+
if (credentialIssuerMetadata.authorization_server) {
|
|
60
|
+
authorization_server = credentialIssuerMetadata.authorization_server;
|
|
66
61
|
}
|
|
62
|
+
if (credentialIssuerMetadata.authorization_endpoint) {
|
|
63
|
+
authorization_endpoint = credentialIssuerMetadata.authorization_endpoint;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// No specific OID4VCI endpoint. Either can be an OAuth2 AS or an OIDC IDP. Let's start with OIDC first
|
|
67
|
+
let response: OpenIDResponse<AuthorizationServerMetadata> = await MetadataClient.retrieveWellknown(
|
|
68
|
+
authorization_server,
|
|
69
|
+
WellKnownEndpoints.OPENID_CONFIGURATION,
|
|
70
|
+
{
|
|
71
|
+
errorOnNotFound: false,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
let authMetadata = response.successBody;
|
|
75
|
+
if (authMetadata) {
|
|
76
|
+
debug(`Issuer ${issuer} has OpenID Connect Server metadata in well-known location`);
|
|
77
|
+
authorizationServerType = 'OIDC';
|
|
67
78
|
} else {
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// Now oAuth2
|
|
81
|
-
response = await MetadataClient.retrieveWellknown(issuer, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
|
|
82
|
-
asConfig = response.successBody;
|
|
79
|
+
// Now let's do OAuth2
|
|
80
|
+
response = await MetadataClient.retrieveWellknown(authorization_server, WellKnownEndpoints.OAUTH_AS, { errorOnNotFound: false });
|
|
81
|
+
authMetadata = response.successBody;
|
|
82
|
+
}
|
|
83
|
+
if (!authMetadata) {
|
|
84
|
+
// We will always throw an error, no matter whether the user provided the option not to, because this is bad.
|
|
85
|
+
if (issuer !== authorization_server) {
|
|
86
|
+
throw Error(`Issuer ${issuer} provided a separate authorization server ${authorization_server}, but that server did not provide metadata`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
if (!authorizationServerType) {
|
|
90
|
+
authorizationServerType = 'OAuth 2.0';
|
|
83
91
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
debug(`Issuer ${issuer} has ${authorizationServerType} Server metadata in well-known location`);
|
|
93
|
+
if (!authMetadata.authorization_endpoint) {
|
|
94
|
+
throw Error(`Authorization Sever ${authorization_server} did not provide an authorization_endpoint`);
|
|
95
|
+
} else if (authorization_endpoint && authMetadata.authorization_endpoint !== authorization_endpoint) {
|
|
96
|
+
throw Error(
|
|
97
|
+
`Credential issuer has a different authorization_endpoint (${authorization_endpoint}) from the Authorization Server (${authMetadata.authorization_endpoint})`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
authorization_endpoint = authMetadata.authorization_endpoint;
|
|
101
|
+
if (!authMetadata.token_endpoint) {
|
|
102
|
+
throw Error(`Authorization Sever ${authorization_server} did not provide a token_endpoint`);
|
|
103
|
+
} else if (token_endpoint && authMetadata.token_endpoint !== token_endpoint) {
|
|
104
|
+
throw Error(
|
|
105
|
+
`Credential issuer has a different token_endpoint (${token_endpoint}) from the Authorization Server (${authMetadata.token_endpoint})`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
token_endpoint = authMetadata.token_endpoint;
|
|
109
|
+
if (authMetadata.credential_endpoint) {
|
|
110
|
+
if (credential_endpoint && authMetadata.credential_endpoint !== credential_endpoint) {
|
|
111
|
+
debug(
|
|
112
|
+
`Credential issuer has a different credential_endpoint (${credential_endpoint}) from the Authorization Server (${authMetadata.token_endpoint}). Will use the issuer value`,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
credential_endpoint = authMetadata.credential_endpoint;
|
|
116
|
+
}
|
|
89
117
|
}
|
|
90
118
|
}
|
|
119
|
+
|
|
120
|
+
if (!authorization_endpoint) {
|
|
121
|
+
debug(`Issuer ${issuer} does not expose authorization_endpoint, so only pre-auth will be supported`);
|
|
122
|
+
}
|
|
91
123
|
if (!token_endpoint) {
|
|
92
124
|
debug(`Issuer ${issuer} does not have a token_endpoint listed in well-known locations!`);
|
|
93
125
|
if (opts?.errorOnNotFound) {
|
|
94
|
-
throw
|
|
126
|
+
throw Error(`Could not deduce the token_endpoint for ${issuer}`);
|
|
95
127
|
} else {
|
|
96
|
-
token_endpoint = `${issuer}${issuer.endsWith('/') ? '' : '/'}
|
|
128
|
+
token_endpoint = `${issuer}${issuer.endsWith('/') ? 'token' : '/token'}`;
|
|
97
129
|
}
|
|
98
130
|
}
|
|
99
131
|
if (!credential_endpoint) {
|
|
100
132
|
debug(`Issuer ${issuer} does not have a credential_endpoint listed in well-known locations!`);
|
|
101
133
|
if (opts?.errorOnNotFound) {
|
|
102
|
-
throw
|
|
134
|
+
throw Error(`Could not deduce the credential endpoint for ${issuer}`);
|
|
103
135
|
} else {
|
|
104
|
-
credential_endpoint = `${issuer}${issuer.endsWith('/') ? '' : '/'}
|
|
136
|
+
credential_endpoint = `${issuer}${issuer.endsWith('/') ? 'credential' : '/credential'}`;
|
|
105
137
|
}
|
|
106
138
|
}
|
|
139
|
+
|
|
140
|
+
if (!credentialIssuerMetadata && authMetadata) {
|
|
141
|
+
// Apparently everything worked out and the issuer is exposing everything in oAuth2/OIDC well-knowns. Spec is vague about this situation, but we can support it
|
|
142
|
+
credentialIssuerMetadata = authMetadata as CredentialIssuerMetadata;
|
|
143
|
+
}
|
|
107
144
|
debug(`Issuer ${issuer} token endpoint ${token_endpoint}, credential endpoint ${credential_endpoint}`);
|
|
108
145
|
return {
|
|
109
146
|
issuer,
|
|
110
147
|
token_endpoint,
|
|
111
148
|
credential_endpoint,
|
|
112
|
-
|
|
149
|
+
authorization_server,
|
|
150
|
+
authorization_endpoint,
|
|
151
|
+
authorizationServerType,
|
|
152
|
+
credentialIssuerMetadata: credentialIssuerMetadata,
|
|
153
|
+
authorizationServerMetadata: authMetadata,
|
|
113
154
|
};
|
|
114
155
|
}
|
|
115
156
|
|
|
@@ -118,9 +159,15 @@ export class MetadataClient {
|
|
|
118
159
|
*
|
|
119
160
|
* @param issuerHost The issuer hostname
|
|
120
161
|
*/
|
|
121
|
-
public static async retrieveOpenID4VCIServerMetadata(
|
|
122
|
-
|
|
123
|
-
|
|
162
|
+
public static async retrieveOpenID4VCIServerMetadata(
|
|
163
|
+
issuerHost: string,
|
|
164
|
+
opts?: {
|
|
165
|
+
errorOnNotFound?: boolean;
|
|
166
|
+
},
|
|
167
|
+
): Promise<OpenIDResponse<CredentialIssuerMetadata> | undefined> {
|
|
168
|
+
return MetadataClient.retrieveWellknown(issuerHost, WellKnownEndpoints.OPENID4VCI_ISSUER, {
|
|
169
|
+
errorOnNotFound: opts?.errorOnNotFound === undefined ? true : opts.errorOnNotFound,
|
|
170
|
+
});
|
|
124
171
|
}
|
|
125
172
|
|
|
126
173
|
/**
|
|
@@ -138,9 +185,9 @@ export class MetadataClient {
|
|
|
138
185
|
const result: OpenIDResponse<T> = await getJson(`${host.endsWith('/') ? host.slice(0, -1) : host}${endpointType}`, {
|
|
139
186
|
exceptionOnHttpErrorStatus: opts?.errorOnNotFound,
|
|
140
187
|
});
|
|
141
|
-
if (result.origResponse.status
|
|
188
|
+
if (result.origResponse.status >= 400) {
|
|
142
189
|
// We only get here when error on not found is false
|
|
143
|
-
debug(`host ${host} with endpoint type ${endpointType}
|
|
190
|
+
debug(`host ${host} with endpoint type ${endpointType} status: ${result.origResponse.status}, ${result.origResponse.statusText}`);
|
|
144
191
|
}
|
|
145
192
|
return result;
|
|
146
193
|
}
|
package/lib/OpenID4VCIClient.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CredentialResponse,
|
|
10
10
|
CredentialSupported,
|
|
11
11
|
EndpointMetadata,
|
|
12
|
+
EndpointMetadataResult,
|
|
12
13
|
OID4VCICredentialFormat,
|
|
13
14
|
OpenId4VCIVersion,
|
|
14
15
|
OpenIDResponse,
|
|
@@ -53,7 +54,7 @@ export class OpenID4VCIClient {
|
|
|
53
54
|
private _clientId?: string;
|
|
54
55
|
private _kid: string | undefined;
|
|
55
56
|
private _alg: Alg | string | undefined;
|
|
56
|
-
private _endpointMetadata:
|
|
57
|
+
private _endpointMetadata: EndpointMetadataResult | undefined;
|
|
57
58
|
private _accessTokenResponse: AccessTokenResponse | undefined;
|
|
58
59
|
|
|
59
60
|
private constructor(
|
|
@@ -119,9 +120,14 @@ export class OpenID4VCIClient {
|
|
|
119
120
|
if (!scope && !authorizationDetails) {
|
|
120
121
|
throw Error('Please provide a scope or authorization_details');
|
|
121
122
|
}
|
|
122
|
-
// todo:
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
124
|
+
// handling this because of the support for v1_0-08
|
|
125
|
+
if (
|
|
126
|
+
this._endpointMetadata &&
|
|
127
|
+
this._endpointMetadata.credentialIssuerMetadata &&
|
|
128
|
+
'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
|
|
129
|
+
) {
|
|
130
|
+
this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
|
|
125
131
|
}
|
|
126
132
|
if (!this._endpointMetadata?.authorization_endpoint) {
|
|
127
133
|
throw Error('Server metadata does not contain authorization endpoint');
|
|
@@ -169,13 +175,13 @@ export class OpenID4VCIClient {
|
|
|
169
175
|
// What happens if it doesn't ???
|
|
170
176
|
// let parEndpoint: string
|
|
171
177
|
if (
|
|
172
|
-
!this._endpointMetadata?.
|
|
173
|
-
!('pushed_authorization_request_endpoint' in this._endpointMetadata.
|
|
174
|
-
typeof this._endpointMetadata.
|
|
178
|
+
!this._endpointMetadata?.credentialIssuerMetadata ||
|
|
179
|
+
!('pushed_authorization_request_endpoint' in this._endpointMetadata.credentialIssuerMetadata) ||
|
|
180
|
+
typeof this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint !== 'string'
|
|
175
181
|
) {
|
|
176
182
|
throw Error('Server metadata does not contain pushed authorization request endpoint');
|
|
177
183
|
}
|
|
178
|
-
const parEndpoint: string = this._endpointMetadata.
|
|
184
|
+
const parEndpoint: string = this._endpointMetadata.credentialIssuerMetadata.pushed_authorization_request_endpoint;
|
|
179
185
|
|
|
180
186
|
// add 'openid' scope if not present
|
|
181
187
|
if (scope && !scope.includes('openid')) {
|
|
@@ -207,7 +213,10 @@ export class OpenID4VCIClient {
|
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
private handleLocations(authorizationDetails: AuthDetails) {
|
|
210
|
-
if (
|
|
216
|
+
if (
|
|
217
|
+
authorizationDetails &&
|
|
218
|
+
(this.endpointMetadata.credentialIssuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)
|
|
219
|
+
) {
|
|
211
220
|
if (authorizationDetails.locations) {
|
|
212
221
|
if (Array.isArray(authorizationDetails.locations)) {
|
|
213
222
|
(authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
|
|
@@ -293,8 +302,8 @@ export class OpenID4VCIClient {
|
|
|
293
302
|
metadata: this.endpointMetadata,
|
|
294
303
|
});
|
|
295
304
|
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
|
|
296
|
-
if (this.endpointMetadata?.
|
|
297
|
-
const metadata = this.endpointMetadata.
|
|
305
|
+
if (this.endpointMetadata?.credentialIssuerMetadata) {
|
|
306
|
+
const metadata = this.endpointMetadata.credentialIssuerMetadata;
|
|
298
307
|
const types = Array.isArray(credentialTypes) ? credentialTypes : [credentialTypes];
|
|
299
308
|
if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
300
309
|
for (const type of types) {
|
|
@@ -357,79 +366,29 @@ export class OpenID4VCIClient {
|
|
|
357
366
|
return response.successBody;
|
|
358
367
|
}
|
|
359
368
|
|
|
360
|
-
getCredentialsSupported(
|
|
369
|
+
getCredentialsSupported(
|
|
370
|
+
restrictToInitiationTypes: boolean,
|
|
371
|
+
format?: (OID4VCICredentialFormat | string) | (OID4VCICredentialFormat | string)[],
|
|
372
|
+
): CredentialSupported[] {
|
|
361
373
|
return getSupportedCredentials({
|
|
362
|
-
issuerMetadata: this.endpointMetadata.
|
|
374
|
+
issuerMetadata: this.endpointMetadata.credentialIssuerMetadata,
|
|
363
375
|
version: this.version(),
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
format: format,
|
|
377
|
+
types: restrictToInitiationTypes ? this.getCredentialTypes() : undefined,
|
|
366
378
|
});
|
|
367
|
-
/*//FIXME: delegate to getCredentialsSupported from IssuerMetadataUtils
|
|
368
|
-
let credentialsSupported = this.endpointMetadata?.issuerMetadata?.credentials_supported
|
|
369
|
-
|
|
370
|
-
if (this.version() === OpenId4VCIVersion.VER_1_0_08 || typeof credentialsSupported === 'object') {
|
|
371
|
-
const issuerMetadata = this.endpointMetadata.issuerMetadata as IssuerMetadataV1_0_08
|
|
372
|
-
const v8CredentialsSupported = issuerMetadata.credentials_supported
|
|
373
|
-
credentialsSupported = []
|
|
374
|
-
credentialsSupported = Object.entries(v8CredentialsSupported).map((key, value) => )
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (!credentialsSupported) {
|
|
380
|
-
return []
|
|
381
|
-
} else if (!restrictToInitiationTypes) {
|
|
382
|
-
return credentialsSupported
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
/!**
|
|
388
|
-
* the following (not array part is a legacy code from version 1_0-08 which jff implementors used)
|
|
389
|
-
*!/
|
|
390
|
-
if (!Array.isArray(credentialsSupported)) {
|
|
391
|
-
const credentialsSupportedV8: CredentialSupportedV1_0_08 = credentialsSupported as CredentialSupportedV1_0_08;
|
|
392
|
-
const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes();
|
|
393
|
-
const supported: IssuerCredentialSubject = {};
|
|
394
|
-
for (const [key, value] of Object.entries(credentialsSupportedV8)) {
|
|
395
|
-
if (initiationTypes.includes(key)) {
|
|
396
|
-
supported[key] = value;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// todo: fix this later. we're returning CredentialSupportedV1_0_08 as a list of CredentialSupported (for v09 onward)
|
|
400
|
-
return supported as unknown as CredentialSupported[];
|
|
401
|
-
}
|
|
402
|
-
const initiationTypes = supportedType ? [supportedType] : this.getCredentialTypes()
|
|
403
|
-
const credentialSupportedOverlap: CredentialSupported[] = []
|
|
404
|
-
for (const supported of credentialsSupported) {
|
|
405
|
-
const supportedTypeOverlap: string[] = []
|
|
406
|
-
for (const type of supported.types) {
|
|
407
|
-
initiationTypes.includes(type)
|
|
408
|
-
supportedTypeOverlap.push(type)
|
|
409
|
-
}
|
|
410
|
-
if (supportedTypeOverlap.length > 0) {
|
|
411
|
-
credentialSupportedOverlap.push({
|
|
412
|
-
...supported,
|
|
413
|
-
types: supportedTypeOverlap
|
|
414
|
-
})
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return credentialSupportedOverlap as CredentialSupported[]*/
|
|
418
379
|
}
|
|
419
380
|
|
|
420
|
-
|
|
421
|
-
return this.getCredentialsSupported(false, type);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// todo https://sphereon.atlassian.net/browse/VDX-184
|
|
425
|
-
getCredentialTypes(): string[] {
|
|
381
|
+
getCredentialTypes(): string[][] {
|
|
426
382
|
if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
383
|
+
const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
|
|
384
|
+
const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
|
|
385
|
+
const result: string[][] = [];
|
|
386
|
+
result[0] = types;
|
|
387
|
+
return result;
|
|
430
388
|
} else {
|
|
431
|
-
|
|
432
|
-
|
|
389
|
+
return this.credentialOffer.credential_offer.credentials.map((c, index) => {
|
|
390
|
+
return typeof c === 'string' ? [c] : c.types;
|
|
391
|
+
});
|
|
433
392
|
}
|
|
434
393
|
}
|
|
435
394
|
|
|
@@ -449,7 +408,7 @@ export class OpenID4VCIClient {
|
|
|
449
408
|
return this.credentialOffer.version;
|
|
450
409
|
}
|
|
451
410
|
|
|
452
|
-
public get endpointMetadata():
|
|
411
|
+
public get endpointMetadata(): EndpointMetadataResult {
|
|
453
412
|
this.assertServerMetadata();
|
|
454
413
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
455
414
|
return this._endpointMetadata!;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AccessTokenRequest,
|
|
3
|
+
AccessTokenRequestOpts,
|
|
4
|
+
AccessTokenResponse,
|
|
5
|
+
GrantTypes,
|
|
6
|
+
OpenIDResponse,
|
|
7
|
+
WellKnownEndpoints,
|
|
8
|
+
} from '@sphereon/oid4vci-common';
|
|
2
9
|
import nock from 'nock';
|
|
3
10
|
|
|
4
11
|
import { AccessTokenClient } from '../AccessTokenClient';
|
|
@@ -11,6 +18,8 @@ const MOCK_URL = 'https://sphereonjunit20221013.com/';
|
|
|
11
18
|
describe('AccessTokenClient should', () => {
|
|
12
19
|
beforeEach(() => {
|
|
13
20
|
nock.cleanAll();
|
|
21
|
+
nock(MOCK_URL).get(WellKnownEndpoints.OAUTH_AS).reply(404, {});
|
|
22
|
+
nock(MOCK_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {});
|
|
14
23
|
});
|
|
15
24
|
|
|
16
25
|
afterEach(() => {
|
package/lib/__tests__/IT.spec.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Jwt,
|
|
7
7
|
OpenId4VCIVersion,
|
|
8
8
|
ProofOfPossession,
|
|
9
|
+
WellKnownEndpoints,
|
|
9
10
|
} from '@sphereon/oid4vci-common';
|
|
10
11
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
11
12
|
// @ts-ignore
|
|
@@ -55,6 +56,7 @@ describe('OID4VCI-Client should', () => {
|
|
|
55
56
|
function succeedWithAFullFlowWithClientSetup() {
|
|
56
57
|
nock(IDENTIPROOF_ISSUER_URL).get('/.well-known/openid-credential-issuer').reply(200, JSON.stringify(IDENTIPROOF_OID4VCI_METADATA));
|
|
57
58
|
nock(IDENTIPROOF_AS_URL).get('/.well-known/oauth-authorization-server').reply(200, JSON.stringify(IDENTIPROOF_AS_METADATA));
|
|
59
|
+
nock(IDENTIPROOF_AS_URL).get(WellKnownEndpoints.OPENID_CONFIGURATION).reply(404, {});
|
|
58
60
|
nock(IDENTIPROOF_AS_URL)
|
|
59
61
|
.post(/oauth2\/token.*/)
|
|
60
62
|
.reply(200, JSON.stringify(mockedAccessTokenResponse));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { OpenId4VCIVersion } from '@sphereon/oid4vci-common';
|
|
2
|
+
|
|
1
3
|
import { CredentialOfferClient } from '../CredentialOfferClient';
|
|
2
4
|
|
|
3
5
|
import { INITIATION_TEST, INITIATION_TEST_HTTPS_URI, INITIATION_TEST_URI } from './MetadataMocks';
|
|
@@ -45,4 +47,15 @@ describe('Issuance Initiation', () => {
|
|
|
45
47
|
const issuanceInitiationURI = INITIATION_TEST_HTTPS_URI.replace('?', '');
|
|
46
48
|
await expect(async () => CredentialOfferClient.fromURI(issuanceInitiationURI)).rejects.toThrowError('Invalid Credential Offer Request');
|
|
47
49
|
});
|
|
50
|
+
|
|
51
|
+
it('Should return Credential Offer', async () => {
|
|
52
|
+
const client = await CredentialOfferClient.fromURI(
|
|
53
|
+
'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22OpenBadgeCredential%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X%22%7D%7D%7D',
|
|
54
|
+
);
|
|
55
|
+
expect(client.version).toEqual(OpenId4VCIVersion.VER_1_0_11);
|
|
56
|
+
expect(client.baseUrl).toEqual('openid-credential-offer://');
|
|
57
|
+
expect(client.scheme).toEqual('openid-credential-offer');
|
|
58
|
+
expect(client.credential_offer.credential_issuer).toEqual('https://launchpad.vii.electron.mattrlabs.io');
|
|
59
|
+
expect(client.preAuthorizedCode).toEqual('UPZohaodPlLBnGsqB02n2tIupCIg8nKRRUEUHWA665X');
|
|
60
|
+
});
|
|
48
61
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Alg, AuthzFlowType, Jwt } from '@sphereon/oid4vci-common';
|
|
2
|
+
import { CredentialMapper } from '@sphereon/ssi-types';
|
|
3
|
+
import { fetch } from 'cross-fetch';
|
|
4
|
+
import { importJWK, JWK, SignJWT } from 'jose';
|
|
5
|
+
|
|
6
|
+
import { OpenID4VCIClient } from '..';
|
|
7
|
+
|
|
8
|
+
export const UNIT_TEST_TIMEOUT = 30000;
|
|
9
|
+
|
|
10
|
+
const ISSUER_URL = 'https://launchpad.vii.electron.mattrlabs.io';
|
|
11
|
+
|
|
12
|
+
const jwk: JWK = {
|
|
13
|
+
crv: 'Ed25519',
|
|
14
|
+
d: 'kTRm0aONHYwNPA-w_DtjMHUIWjE3K70qgCIhWojZ0eU',
|
|
15
|
+
x: 'NeA0d8sp86xRh3DczU4m5wPNIbl0HCSwOBcMN3sNmdk',
|
|
16
|
+
kty: 'OKP',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// pub hex: 35e03477cb29f3ac518770dccd4e26e703cd21b9741c24b038170c377b0d99d9
|
|
20
|
+
// priv hex: 913466d1a38d1d8c0d3c0fb0fc3b633075085a31372bbd2a8022215a88d9d1e5
|
|
21
|
+
const did = `did:key:z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
|
|
22
|
+
const kid = `${did}#z6Mki5ZwZKN1dBQprfJTikUvkDxrHijiiQngkWviMF5gw2Hv`;
|
|
23
|
+
describe('OID4VCI-Client using Mattr issuer should', () => {
|
|
24
|
+
async function test(format: 'ldp_vc' | 'jwt_vc_json') {
|
|
25
|
+
const offer = await getCredentialOffer(format);
|
|
26
|
+
const client = await OpenID4VCIClient.fromURI({
|
|
27
|
+
uri: offer.offerUrl,
|
|
28
|
+
flowType: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW,
|
|
29
|
+
kid,
|
|
30
|
+
alg: Alg.EdDSA,
|
|
31
|
+
});
|
|
32
|
+
expect(client.flowType).toEqual(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW);
|
|
33
|
+
expect(client.credentialOffer).toBeDefined();
|
|
34
|
+
expect(client.endpointMetadata).toBeDefined();
|
|
35
|
+
expect(client.getCredentialEndpoint()).toEqual(`${ISSUER_URL}/oidc/v1/auth/credential`);
|
|
36
|
+
expect(client.getAccessTokenEndpoint()).toEqual('https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token');
|
|
37
|
+
|
|
38
|
+
const accessToken = await client.acquireAccessToken();
|
|
39
|
+
console.log(accessToken);
|
|
40
|
+
expect(accessToken).toMatchObject({
|
|
41
|
+
expires_in: 3600,
|
|
42
|
+
scope: 'OpenBadgeCredential',
|
|
43
|
+
token_type: 'Bearer',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const credentialResponse = await client.acquireCredentials({
|
|
47
|
+
credentialTypes: 'OpenBadgeCredential',
|
|
48
|
+
format,
|
|
49
|
+
proofCallbacks: {
|
|
50
|
+
signCallback: proofOfPossessionCallbackFunction,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
expect(credentialResponse.credential).toBeDefined();
|
|
54
|
+
const wrappedVC = CredentialMapper.toWrappedVerifiableCredential(credentialResponse.credential!);
|
|
55
|
+
expect(format.startsWith(wrappedVC.format)).toEqual(true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it(
|
|
59
|
+
'succeed in a full flow with the client using OpenID4VCI version 11 and ldp_vc',
|
|
60
|
+
async () => {
|
|
61
|
+
await test('ldp_vc');
|
|
62
|
+
},
|
|
63
|
+
UNIT_TEST_TIMEOUT,
|
|
64
|
+
);
|
|
65
|
+
it(
|
|
66
|
+
'succeed in a full flow with the client using OpenID4VCI version 11 and jwt_vc_json',
|
|
67
|
+
async () => {
|
|
68
|
+
await test('jwt_vc_json');
|
|
69
|
+
},
|
|
70
|
+
UNIT_TEST_TIMEOUT,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
interface CreateCredentialOfferResponse {
|
|
75
|
+
id: string;
|
|
76
|
+
offerUrl: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function getCredentialOffer(format: 'ldp_vc' | 'jwt_vc_json'): Promise<CreateCredentialOfferResponse> {
|
|
80
|
+
const credentialOffer = await fetch('https://launchpad.mattrlabs.com/api/credential-offer', {
|
|
81
|
+
method: 'post',
|
|
82
|
+
headers: {
|
|
83
|
+
Accept: 'application/json',
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
//make sure to serialize your JSON body
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
format,
|
|
90
|
+
type: 'OpenBadgeCredential',
|
|
91
|
+
userId: '622a9f65-21c0-4c0b-9a6a-f7574c2a1549',
|
|
92
|
+
userAuthenticationRequired: false,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return (await credentialOffer.json()) as CreateCredentialOfferResponse;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function proofOfPossessionCallbackFunction(args: Jwt, kid?: string): Promise<string> {
|
|
100
|
+
const importedJwk = await importJWK(jwk, 'EdDSA');
|
|
101
|
+
return await new SignJWT({ ...args.payload })
|
|
102
|
+
.setProtectedHeader({ ...args.header })
|
|
103
|
+
.setIssuedAt()
|
|
104
|
+
.setExpirationTime('2h')
|
|
105
|
+
.sign(importedJwk);
|
|
106
|
+
}
|