@sphereon/oid4vci-client 0.8.1 → 0.8.2-next.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/AccessTokenClient.d.ts.map +1 -1
- package/dist/AccessTokenClient.js +8 -8
- package/dist/AccessTokenClient.js.map +1 -1
- package/dist/AuthorizationDetailsBuilder.d.ts.map +1 -1
- package/dist/CredentialRequestClient.d.ts.map +1 -1
- package/dist/CredentialRequestClient.js +36 -36
- package/dist/CredentialRequestClient.js.map +1 -1
- package/dist/CredentialRequestClientBuilder.d.ts +6 -0
- package/dist/CredentialRequestClientBuilder.d.ts.map +1 -1
- package/dist/CredentialRequestClientBuilder.js +10 -1
- package/dist/CredentialRequestClientBuilder.js.map +1 -1
- package/dist/OpenID4VCIClient.d.ts +15 -5
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +111 -41
- package/dist/OpenID4VCIClient.js.map +1 -1
- package/dist/ProofOfPossessionBuilder.d.ts +3 -1
- package/dist/ProofOfPossessionBuilder.d.ts.map +1 -1
- package/dist/ProofOfPossessionBuilder.js +5 -0
- package/dist/ProofOfPossessionBuilder.js.map +1 -1
- package/dist/functions/ProofUtil.d.ts +2 -1
- package/dist/functions/ProofUtil.d.ts.map +1 -1
- package/dist/functions/ProofUtil.js +6 -4
- package/dist/functions/ProofUtil.js.map +1 -1
- package/lib/AccessTokenClient.ts +10 -8
- package/lib/AuthorizationDetailsBuilder.ts +2 -2
- package/lib/CredentialRequestClient.ts +42 -38
- package/lib/CredentialRequestClientBuilder.ts +21 -1
- package/lib/OpenID4VCIClient.ts +140 -42
- package/lib/ProofOfPossessionBuilder.ts +8 -0
- package/lib/__tests__/CredentialRequestClient.spec.ts +21 -9
- package/lib/__tests__/EBSIE2E.spec.test.ts +138 -0
- package/lib/__tests__/MattrE2E.spec.test.ts +1 -1
- package/lib/__tests__/MetadataClient.spec.ts +4 -1
- package/lib/__tests__/OpenID4VCIClient.spec.ts +5 -0
- package/lib/__tests__/SdJwt.spec.ts +161 -0
- package/lib/__tests__/SphereonE2E.spec.test.ts +169 -0
- package/lib/__tests__/data/VciDataFixtures.ts +14 -13
- package/lib/functions/ProofUtil.ts +18 -4
- package/package.json +10 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
CredentialRequestV1_0_08,
|
|
3
2
|
CredentialResponse,
|
|
3
|
+
getCredentialRequestForVersion,
|
|
4
|
+
getUniformFormat,
|
|
4
5
|
OID4VCICredentialFormat,
|
|
5
6
|
OpenId4VCIVersion,
|
|
6
7
|
OpenIDResponse,
|
|
@@ -53,33 +54,17 @@ export class CredentialRequestClient {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
public async acquireCredentialsUsingRequest(uniformRequest: UniformCredentialRequest): Promise<OpenIDResponse<CredentialResponse>> {
|
|
56
|
-
|
|
57
|
-
if (!this.isV11OrHigher()) {
|
|
58
|
-
let format: string = uniformRequest.format;
|
|
59
|
-
if (format === 'jwt_vc_json') {
|
|
60
|
-
format = 'jwt_vc';
|
|
61
|
-
} else if (format === 'jwt_vc_json-ld') {
|
|
62
|
-
format = 'ldp_vc';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
request = {
|
|
66
|
-
format,
|
|
67
|
-
proof: uniformRequest.proof,
|
|
68
|
-
type:
|
|
69
|
-
'types' in uniformRequest
|
|
70
|
-
? uniformRequest.types.filter((t) => t !== 'VerifiableCredential')[0]
|
|
71
|
-
: uniformRequest.credential_definition.types[0],
|
|
72
|
-
} as CredentialRequestV1_0_08;
|
|
73
|
-
}
|
|
57
|
+
const request = getCredentialRequestForVersion(uniformRequest, this.version());
|
|
74
58
|
const credentialEndpoint: string = this.credentialRequestOpts.credentialEndpoint;
|
|
75
59
|
if (!isValidURL(credentialEndpoint)) {
|
|
76
60
|
debug(`Invalid credential endpoint: ${credentialEndpoint}`);
|
|
77
61
|
throw new Error(URL_NOT_VALID);
|
|
78
62
|
}
|
|
79
63
|
debug(`Acquiring credential(s) from: ${credentialEndpoint}`);
|
|
64
|
+
debug(`request\n: ${JSON.stringify(request, null, 2)}`);
|
|
80
65
|
const requestToken: string = this.credentialRequestOpts.token;
|
|
81
66
|
const response: OpenIDResponse<CredentialResponse> = await post(credentialEndpoint, JSON.stringify(request), { bearerToken: requestToken });
|
|
82
|
-
debug(`Credential endpoint ${credentialEndpoint} response:\r\n${response}`);
|
|
67
|
+
debug(`Credential endpoint ${credentialEndpoint} response:\r\n${JSON.stringify(response, null, 2)}`);
|
|
83
68
|
return response;
|
|
84
69
|
}
|
|
85
70
|
|
|
@@ -92,20 +77,10 @@ export class CredentialRequestClient {
|
|
|
92
77
|
const { proofInput } = opts;
|
|
93
78
|
const formatSelection = opts.format ?? this.credentialRequestOpts.format;
|
|
94
79
|
|
|
95
|
-
|
|
96
|
-
if (opts.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
97
|
-
if (formatSelection === 'jwt_vc' || formatSelection === 'jwt') {
|
|
98
|
-
format = 'jwt_vc_json';
|
|
99
|
-
} else if (formatSelection === 'ldp_vc' || formatSelection === 'ldp') {
|
|
100
|
-
format = 'jwt_vc_json-ld';
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!format) {
|
|
80
|
+
if (!formatSelection) {
|
|
105
81
|
throw Error(`Format of credential to be issued is missing`);
|
|
106
|
-
} else if (format !== 'jwt_vc_json-ld' && format !== 'jwt_vc_json' && format !== 'ldp_vc') {
|
|
107
|
-
throw Error(`Invalid format of credential to be issued: ${format}`);
|
|
108
82
|
}
|
|
83
|
+
const format = getUniformFormat(formatSelection);
|
|
109
84
|
const typesSelection =
|
|
110
85
|
opts?.credentialTypes && (typeof opts.credentialTypes === 'string' || opts.credentialTypes.length > 0)
|
|
111
86
|
? opts.credentialTypes
|
|
@@ -113,7 +88,9 @@ export class CredentialRequestClient {
|
|
|
113
88
|
const types = Array.isArray(typesSelection) ? typesSelection : [typesSelection];
|
|
114
89
|
if (types.length === 0) {
|
|
115
90
|
throw Error(`Credential type(s) need to be provided`);
|
|
116
|
-
}
|
|
91
|
+
}
|
|
92
|
+
// FIXME: this is mixing up the type (as id) from v8/v9 and the types (from the vc.type) from v11
|
|
93
|
+
else if (!this.isV11OrHigher() && types.length !== 1) {
|
|
117
94
|
throw Error('Only a single credential type is supported for V8/V9');
|
|
118
95
|
}
|
|
119
96
|
|
|
@@ -121,16 +98,43 @@ export class CredentialRequestClient {
|
|
|
121
98
|
'proof_type' in proofInput
|
|
122
99
|
? await ProofOfPossessionBuilder.fromProof(proofInput as ProofOfPossession, opts.version).build()
|
|
123
100
|
: await proofInput.build();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
101
|
+
|
|
102
|
+
// TODO: we should move format specific logic
|
|
103
|
+
if (format === 'jwt_vc_json' || format === 'jwt_vc') {
|
|
104
|
+
return {
|
|
105
|
+
types,
|
|
106
|
+
format,
|
|
107
|
+
proof,
|
|
108
|
+
};
|
|
109
|
+
} else if (format === 'jwt_vc_json-ld' || format === 'ldp_vc') {
|
|
110
|
+
return {
|
|
111
|
+
format,
|
|
112
|
+
proof,
|
|
113
|
+
credential_definition: {
|
|
114
|
+
types,
|
|
115
|
+
// FIXME: this was not included in the original code, but it is required
|
|
116
|
+
'@context': [],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
} else if (format === 'vc+sd-jwt') {
|
|
120
|
+
if (types.length > 1) {
|
|
121
|
+
throw Error(`Only a single credential type is supported for ${format}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
format,
|
|
126
|
+
proof,
|
|
127
|
+
vct: types[0],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
private version(): OpenId4VCIVersion {
|
|
132
135
|
return this.credentialRequestOpts?.version ?? OpenId4VCIVersion.VER_1_0_11;
|
|
133
136
|
}
|
|
137
|
+
|
|
134
138
|
private isV11OrHigher(): boolean {
|
|
135
139
|
return this.version() >= OpenId4VCIVersion.VER_1_0_11;
|
|
136
140
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
determineSpecVersionFromOffer,
|
|
7
7
|
EndpointMetadata,
|
|
8
8
|
getIssuerFromCredentialOfferPayload,
|
|
9
|
+
getTypesFromOffer,
|
|
9
10
|
OID4VCICredentialFormat,
|
|
10
11
|
OpenId4VCIVersion,
|
|
11
12
|
UniformCredentialOfferRequest,
|
|
@@ -22,6 +23,25 @@ export class CredentialRequestClientBuilder {
|
|
|
22
23
|
token?: string;
|
|
23
24
|
version?: OpenId4VCIVersion;
|
|
24
25
|
|
|
26
|
+
public static fromCredentialIssuer({
|
|
27
|
+
credentialIssuer,
|
|
28
|
+
metadata,
|
|
29
|
+
version,
|
|
30
|
+
credentialTypes,
|
|
31
|
+
}: {
|
|
32
|
+
credentialIssuer: string;
|
|
33
|
+
metadata?: EndpointMetadata;
|
|
34
|
+
version?: OpenId4VCIVersion;
|
|
35
|
+
credentialTypes: string | string[];
|
|
36
|
+
}): CredentialRequestClientBuilder {
|
|
37
|
+
const issuer = credentialIssuer;
|
|
38
|
+
const builder = new CredentialRequestClientBuilder();
|
|
39
|
+
builder.withVersion(version ?? OpenId4VCIVersion.VER_1_0_11);
|
|
40
|
+
builder.withCredentialEndpoint(metadata?.credential_endpoint ?? (issuer.endsWith('/') ? `${issuer}credential` : `${issuer}/credential`));
|
|
41
|
+
builder.withCredentialType(credentialTypes);
|
|
42
|
+
return builder;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
public static async fromURI({ uri, metadata }: { uri: string; metadata?: EndpointMetadata }): Promise<CredentialRequestClientBuilder> {
|
|
26
46
|
const offer = await CredentialOfferClient.fromURI(uri);
|
|
27
47
|
return CredentialRequestClientBuilder.fromCredentialOfferRequest({ request: offer, ...offer, metadata, version: offer.version });
|
|
@@ -46,7 +66,7 @@ export class CredentialRequestClientBuilder {
|
|
|
46
66
|
builder.withCredentialType((request.original_credential_offer as CredentialOfferPayloadV1_0_08).credential_type);
|
|
47
67
|
} else {
|
|
48
68
|
// todo: look whether this is correct
|
|
49
|
-
builder.withCredentialType(request.credential_offer
|
|
69
|
+
builder.withCredentialType(getTypesFromOffer(request.credential_offer));
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
return builder;
|
package/lib/OpenID4VCIClient.ts
CHANGED
|
@@ -8,15 +8,18 @@ import {
|
|
|
8
8
|
CredentialResponse,
|
|
9
9
|
CredentialSupported,
|
|
10
10
|
EndpointMetadataResult,
|
|
11
|
+
getIssuerFromCredentialOfferPayload,
|
|
12
|
+
getSupportedCredentials,
|
|
13
|
+
getTypesFromCredentialSupported,
|
|
11
14
|
JsonURIMode,
|
|
15
|
+
JWK,
|
|
16
|
+
KID_JWK_X5C_ERROR,
|
|
12
17
|
OID4VCICredentialFormat,
|
|
13
18
|
OpenId4VCIVersion,
|
|
14
19
|
ProofOfPossessionCallbacks,
|
|
15
20
|
PushedAuthorizationResponse,
|
|
16
21
|
ResponseType,
|
|
17
22
|
} from '@sphereon/oid4vci-common';
|
|
18
|
-
import { getSupportedCredentials } from '@sphereon/oid4vci-common/dist/functions/IssuerMetadataUtils';
|
|
19
|
-
import { CredentialSupportedTypeV1_0_08 } from '@sphereon/oid4vci-common/dist/types/v1_0_08.types';
|
|
20
23
|
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
21
24
|
import Debug from 'debug';
|
|
22
25
|
|
|
@@ -39,27 +42,66 @@ interface AuthDetails {
|
|
|
39
42
|
|
|
40
43
|
interface AuthRequestOpts {
|
|
41
44
|
codeChallenge: string;
|
|
42
|
-
codeChallengeMethod
|
|
45
|
+
codeChallengeMethod?: CodeChallengeMethod;
|
|
43
46
|
authorizationDetails?: AuthDetails | AuthDetails[];
|
|
44
47
|
redirectUri: string;
|
|
45
48
|
scope?: string;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export class OpenID4VCIClient {
|
|
49
|
-
private readonly _credentialOffer
|
|
52
|
+
private readonly _credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
53
|
+
private _credentialIssuer: string;
|
|
50
54
|
private _clientId?: string;
|
|
51
55
|
private _kid: string | undefined;
|
|
56
|
+
private _jwk: JWK | undefined;
|
|
52
57
|
private _alg: Alg | string | undefined;
|
|
53
58
|
private _endpointMetadata: EndpointMetadataResult | undefined;
|
|
54
59
|
private _accessTokenResponse: AccessTokenResponse | undefined;
|
|
55
60
|
|
|
56
|
-
private constructor(
|
|
61
|
+
private constructor({
|
|
62
|
+
credentialOffer,
|
|
63
|
+
clientId,
|
|
64
|
+
kid,
|
|
65
|
+
alg,
|
|
66
|
+
credentialIssuer,
|
|
67
|
+
}: {
|
|
68
|
+
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
69
|
+
kid?: string;
|
|
70
|
+
alg?: Alg | string;
|
|
71
|
+
clientId?: string;
|
|
72
|
+
credentialIssuer?: string;
|
|
73
|
+
}) {
|
|
57
74
|
this._credentialOffer = credentialOffer;
|
|
75
|
+
const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
|
|
76
|
+
if (!issuer) {
|
|
77
|
+
throw Error('No credential issuer supplied or deduced from offer');
|
|
78
|
+
}
|
|
79
|
+
this._credentialIssuer = issuer;
|
|
58
80
|
this._kid = kid;
|
|
59
81
|
this._alg = alg;
|
|
60
82
|
this._clientId = clientId;
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
public static async fromCredentialIssuer({
|
|
86
|
+
kid,
|
|
87
|
+
alg,
|
|
88
|
+
retrieveServerMetadata,
|
|
89
|
+
clientId,
|
|
90
|
+
credentialIssuer,
|
|
91
|
+
}: {
|
|
92
|
+
credentialIssuer: string;
|
|
93
|
+
kid?: string;
|
|
94
|
+
alg?: Alg | string;
|
|
95
|
+
retrieveServerMetadata?: boolean;
|
|
96
|
+
clientId?: string;
|
|
97
|
+
}) {
|
|
98
|
+
const client = new OpenID4VCIClient({ kid, alg, clientId, credentialIssuer });
|
|
99
|
+
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
100
|
+
await client.retrieveServerMetadata();
|
|
101
|
+
}
|
|
102
|
+
return client;
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
public static async fromURI({
|
|
64
106
|
uri,
|
|
65
107
|
kid,
|
|
@@ -75,7 +117,12 @@ export class OpenID4VCIClient {
|
|
|
75
117
|
resolveOfferUri?: boolean;
|
|
76
118
|
clientId?: string;
|
|
77
119
|
}): Promise<OpenID4VCIClient> {
|
|
78
|
-
const client = new OpenID4VCIClient(
|
|
120
|
+
const client = new OpenID4VCIClient({
|
|
121
|
+
credentialOffer: await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri }),
|
|
122
|
+
kid,
|
|
123
|
+
alg,
|
|
124
|
+
clientId,
|
|
125
|
+
});
|
|
79
126
|
|
|
80
127
|
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
81
128
|
await client.retrieveServerMetadata();
|
|
@@ -86,16 +133,41 @@ export class OpenID4VCIClient {
|
|
|
86
133
|
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
|
|
87
134
|
this.assertIssuerData();
|
|
88
135
|
if (!this._endpointMetadata) {
|
|
89
|
-
|
|
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
|
+
}
|
|
90
143
|
}
|
|
91
144
|
return this.endpointMetadata;
|
|
92
145
|
}
|
|
93
146
|
|
|
147
|
+
// todo: Unify this method with the par method
|
|
148
|
+
|
|
94
149
|
public createAuthorizationRequestUrl({ codeChallengeMethod, codeChallenge, authorizationDetails, redirectUri, scope }: AuthRequestOpts): string {
|
|
95
150
|
// Scope and authorization_details can be used in the same authorization request
|
|
96
151
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
97
152
|
if (!scope && !authorizationDetails) {
|
|
98
|
-
|
|
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
|
+
}
|
|
99
171
|
}
|
|
100
172
|
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
101
173
|
// handling this because of the support for v1_0-08
|
|
@@ -117,7 +189,7 @@ export class OpenID4VCIClient {
|
|
|
117
189
|
|
|
118
190
|
const queryObj: { [key: string]: string } = {
|
|
119
191
|
response_type: ResponseType.AUTH_CODE,
|
|
120
|
-
code_challenge_method: codeChallengeMethod,
|
|
192
|
+
code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
|
|
121
193
|
code_challenge: codeChallenge,
|
|
122
194
|
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
123
195
|
redirect_uri: redirectUri,
|
|
@@ -128,7 +200,7 @@ export class OpenID4VCIClient {
|
|
|
128
200
|
queryObj['client_id'] = this.clientId;
|
|
129
201
|
}
|
|
130
202
|
|
|
131
|
-
if (this.credentialOffer
|
|
203
|
+
if (this.credentialOffer?.issuerState) {
|
|
132
204
|
queryObj['issuer_state'] = this.credentialOffer.issuerState;
|
|
133
205
|
}
|
|
134
206
|
|
|
@@ -140,6 +212,7 @@ export class OpenID4VCIClient {
|
|
|
140
212
|
});
|
|
141
213
|
}
|
|
142
214
|
|
|
215
|
+
// todo: Unify this method with the create auth request url method
|
|
143
216
|
public async acquirePushedAuthorizationRequestURI({
|
|
144
217
|
codeChallengeMethod,
|
|
145
218
|
codeChallenge,
|
|
@@ -173,7 +246,7 @@ export class OpenID4VCIClient {
|
|
|
173
246
|
|
|
174
247
|
const queryObj: { [key: string]: string } = {
|
|
175
248
|
response_type: ResponseType.AUTH_CODE,
|
|
176
|
-
code_challenge_method: codeChallengeMethod,
|
|
249
|
+
code_challenge_method: codeChallengeMethod ?? CodeChallengeMethod.SHA256,
|
|
177
250
|
code_challenge: codeChallenge,
|
|
178
251
|
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
179
252
|
redirect_uri: redirectUri,
|
|
@@ -184,7 +257,7 @@ export class OpenID4VCIClient {
|
|
|
184
257
|
queryObj['client_id'] = this.clientId;
|
|
185
258
|
}
|
|
186
259
|
|
|
187
|
-
if (this.credentialOffer
|
|
260
|
+
if (this.credentialOffer?.issuerState) {
|
|
188
261
|
queryObj['issuer_state'] = this.credentialOffer.issuerState;
|
|
189
262
|
}
|
|
190
263
|
|
|
@@ -249,6 +322,7 @@ export class OpenID4VCIClient {
|
|
|
249
322
|
const response = await accessTokenClient.acquireAccessToken({
|
|
250
323
|
credentialOffer: this.credentialOffer,
|
|
251
324
|
metadata: this.endpointMetadata,
|
|
325
|
+
credentialIssuer: this.getIssuer(),
|
|
252
326
|
pin,
|
|
253
327
|
codeVerifier,
|
|
254
328
|
code,
|
|
@@ -281,6 +355,7 @@ export class OpenID4VCIClient {
|
|
|
281
355
|
proofCallbacks,
|
|
282
356
|
format,
|
|
283
357
|
kid,
|
|
358
|
+
jwk,
|
|
284
359
|
alg,
|
|
285
360
|
jti,
|
|
286
361
|
}: {
|
|
@@ -288,46 +363,54 @@ export class OpenID4VCIClient {
|
|
|
288
363
|
proofCallbacks: ProofOfPossessionCallbacks<any>;
|
|
289
364
|
format?: CredentialFormat | OID4VCICredentialFormat;
|
|
290
365
|
kid?: string;
|
|
366
|
+
jwk?: JWK;
|
|
291
367
|
alg?: Alg | string;
|
|
292
368
|
jti?: string;
|
|
293
369
|
}): Promise<CredentialResponse> {
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
if (kid) {
|
|
298
|
-
this._kid = kid;
|
|
370
|
+
if ([jwk, kid].filter((v) => v !== undefined).length > 1) {
|
|
371
|
+
throw new Error(KID_JWK_X5C_ERROR + `. jwk: ${jwk !== undefined}, kid: ${kid !== undefined}`);
|
|
299
372
|
}
|
|
300
373
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
374
|
+
if (alg) this._alg = alg;
|
|
375
|
+
if (jwk) this._jwk = jwk;
|
|
376
|
+
if (kid) this._kid = kid;
|
|
377
|
+
|
|
378
|
+
const requestBuilder = this.credentialOffer
|
|
379
|
+
? CredentialRequestClientBuilder.fromCredentialOffer({
|
|
380
|
+
credentialOffer: this.credentialOffer,
|
|
381
|
+
metadata: this.endpointMetadata,
|
|
382
|
+
})
|
|
383
|
+
: CredentialRequestClientBuilder.fromCredentialIssuer({
|
|
384
|
+
credentialIssuer: this.getIssuer(),
|
|
385
|
+
credentialTypes,
|
|
386
|
+
metadata: this.endpointMetadata,
|
|
387
|
+
version: this.version(),
|
|
388
|
+
});
|
|
305
389
|
|
|
306
390
|
requestBuilder.withTokenFromResponse(this.accessTokenResponse);
|
|
307
391
|
if (this.endpointMetadata?.credentialIssuerMetadata) {
|
|
308
392
|
const metadata = this.endpointMetadata.credentialIssuerMetadata;
|
|
309
|
-
const types = Array.isArray(credentialTypes) ? credentialTypes.sort() : [credentialTypes];
|
|
393
|
+
const types = Array.isArray(credentialTypes) ? [...credentialTypes].sort() : [credentialTypes];
|
|
310
394
|
|
|
311
395
|
if (metadata.credentials_supported && Array.isArray(metadata.credentials_supported)) {
|
|
312
396
|
let typeSupported = false;
|
|
313
397
|
|
|
314
398
|
metadata.credentials_supported.forEach((supportedCredential) => {
|
|
315
|
-
|
|
316
|
-
throw Error('types is required in the credentials supported');
|
|
317
|
-
}
|
|
399
|
+
const subTypes = getTypesFromCredentialSupported(supportedCredential);
|
|
318
400
|
if (
|
|
319
|
-
|
|
320
|
-
(types.length === 1 && (types[0] === supportedCredential.id ||
|
|
401
|
+
subTypes.sort().every((t, i) => types[i] === t) ||
|
|
402
|
+
(types.length === 1 && (types[0] === supportedCredential.id || subTypes.includes(types[0])))
|
|
321
403
|
) {
|
|
322
404
|
typeSupported = true;
|
|
323
405
|
}
|
|
324
406
|
});
|
|
325
407
|
|
|
326
408
|
if (!typeSupported) {
|
|
327
|
-
|
|
409
|
+
console.log(`Not all credential types ${JSON.stringify(credentialTypes)} are present in metadata for ${this.getIssuer()}`);
|
|
410
|
+
// throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
328
411
|
}
|
|
329
412
|
} else if (metadata.credentials_supported && !Array.isArray(metadata.credentials_supported)) {
|
|
330
|
-
const credentialsSupported = metadata.credentials_supported
|
|
413
|
+
const credentialsSupported = metadata.credentials_supported;
|
|
331
414
|
if (types.some((type) => !metadata.credentials_supported || !credentialsSupported[type])) {
|
|
332
415
|
throw Error(`Not all credential types ${JSON.stringify(credentialTypes)} are supported by issuer ${this.getIssuer()}`);
|
|
333
416
|
}
|
|
@@ -341,8 +424,14 @@ export class OpenID4VCIClient {
|
|
|
341
424
|
version: this.version(),
|
|
342
425
|
})
|
|
343
426
|
.withIssuer(this.getIssuer())
|
|
344
|
-
.withAlg(this.alg)
|
|
345
|
-
|
|
427
|
+
.withAlg(this.alg);
|
|
428
|
+
|
|
429
|
+
if (this._jwk) {
|
|
430
|
+
proofBuilder.withJWK(this._jwk);
|
|
431
|
+
}
|
|
432
|
+
if (this._kid) {
|
|
433
|
+
proofBuilder.withKid(this._kid);
|
|
434
|
+
}
|
|
346
435
|
|
|
347
436
|
if (this.clientId) {
|
|
348
437
|
proofBuilder.withClientId(this.clientId);
|
|
@@ -356,7 +445,7 @@ export class OpenID4VCIClient {
|
|
|
356
445
|
format,
|
|
357
446
|
});
|
|
358
447
|
if (response.errorBody) {
|
|
359
|
-
debug(`Credential request error:\r\n${response.errorBody}`);
|
|
448
|
+
debug(`Credential request error:\r\n${JSON.stringify(response.errorBody)}`);
|
|
360
449
|
throw Error(
|
|
361
450
|
`Retrieving a credential from ${this._endpointMetadata?.credential_endpoint} for issuer ${this.getIssuer()} failed with status: ${
|
|
362
451
|
response.origResponse.status
|
|
@@ -389,7 +478,9 @@ export class OpenID4VCIClient {
|
|
|
389
478
|
}
|
|
390
479
|
|
|
391
480
|
getCredentialOfferTypes(): string[][] {
|
|
392
|
-
if (this.credentialOffer
|
|
481
|
+
if (!this.credentialOffer) {
|
|
482
|
+
return [];
|
|
483
|
+
} else if (this.credentialOffer.version < OpenId4VCIVersion.VER_1_0_11) {
|
|
393
484
|
const orig = this.credentialOffer.original_credential_offer as CredentialOfferPayloadV1_0_08;
|
|
394
485
|
const types: string[] = typeof orig.credential_type === 'string' ? [orig.credential_type] : orig.credential_type;
|
|
395
486
|
const result: string[][] = [];
|
|
@@ -397,21 +488,29 @@ export class OpenID4VCIClient {
|
|
|
397
488
|
return result;
|
|
398
489
|
} else {
|
|
399
490
|
return this.credentialOffer.credential_offer.credentials.map((c) => {
|
|
400
|
-
|
|
491
|
+
if (typeof c === 'string') {
|
|
492
|
+
return [c];
|
|
493
|
+
} else if ('types' in c) {
|
|
494
|
+
return c.types;
|
|
495
|
+
} else if ('vct' in c) {
|
|
496
|
+
return [c.vct];
|
|
497
|
+
} else {
|
|
498
|
+
return c.credential_definition.types;
|
|
499
|
+
}
|
|
401
500
|
});
|
|
402
501
|
}
|
|
403
502
|
}
|
|
404
503
|
|
|
405
504
|
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
406
|
-
return this.credentialOffer.
|
|
505
|
+
return this.credentialOffer?.supportedFlows ?? [AuthzFlowType.AUTHORIZATION_CODE_FLOW];
|
|
407
506
|
}
|
|
408
507
|
|
|
409
|
-
get credentialOffer(): CredentialOfferRequestWithBaseUrl {
|
|
508
|
+
get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
|
|
410
509
|
return this._credentialOffer;
|
|
411
510
|
}
|
|
412
511
|
|
|
413
512
|
public version(): OpenId4VCIVersion {
|
|
414
|
-
return this.credentialOffer.
|
|
513
|
+
return this.credentialOffer?.version ?? OpenId4VCIVersion.VER_1_0_11;
|
|
415
514
|
}
|
|
416
515
|
|
|
417
516
|
public get endpointMetadata(): EndpointMetadataResult {
|
|
@@ -437,9 +536,6 @@ export class OpenID4VCIClient {
|
|
|
437
536
|
}
|
|
438
537
|
|
|
439
538
|
get clientId(): string | undefined {
|
|
440
|
-
/*if (!this._clientId) {
|
|
441
|
-
throw Error('No client id present');
|
|
442
|
-
}*/
|
|
443
539
|
return this._clientId;
|
|
444
540
|
}
|
|
445
541
|
|
|
@@ -451,7 +547,7 @@ export class OpenID4VCIClient {
|
|
|
451
547
|
|
|
452
548
|
public getIssuer(): string {
|
|
453
549
|
this.assertIssuerData();
|
|
454
|
-
return this.
|
|
550
|
+
return this._credentialIssuer!;
|
|
455
551
|
}
|
|
456
552
|
|
|
457
553
|
public getAccessTokenEndpoint(): string {
|
|
@@ -467,8 +563,10 @@ export class OpenID4VCIClient {
|
|
|
467
563
|
}
|
|
468
564
|
|
|
469
565
|
private assertIssuerData(): void {
|
|
470
|
-
if (!this._credentialOffer) {
|
|
566
|
+
if (!this._credentialOffer && this.issuerSupportedFlowTypes().includes(AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW)) {
|
|
471
567
|
throw Error(`No issuance initiation or credential offer present`);
|
|
568
|
+
} else if (!this._credentialIssuer) {
|
|
569
|
+
throw Error(`No credential issuer value present`);
|
|
472
570
|
}
|
|
473
571
|
}
|
|
474
572
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
AccessTokenResponse,
|
|
3
3
|
Alg,
|
|
4
4
|
EndpointMetadata,
|
|
5
|
+
JWK,
|
|
5
6
|
Jwt,
|
|
6
7
|
NO_JWT_PROVIDED,
|
|
7
8
|
OpenId4VCIVersion,
|
|
@@ -19,6 +20,7 @@ export class ProofOfPossessionBuilder<DIDDoc> {
|
|
|
19
20
|
private readonly version: OpenId4VCIVersion;
|
|
20
21
|
|
|
21
22
|
private kid?: string;
|
|
23
|
+
private jwk?: JWK;
|
|
22
24
|
private clientId?: string;
|
|
23
25
|
private issuer?: string;
|
|
24
26
|
private jwt?: Jwt;
|
|
@@ -91,6 +93,11 @@ export class ProofOfPossessionBuilder<DIDDoc> {
|
|
|
91
93
|
return this;
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
withJWK(jwk: JWK): this {
|
|
97
|
+
this.jwk = jwk;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
withIssuer(issuer: string): this {
|
|
95
102
|
this.issuer = issuer;
|
|
96
103
|
return this;
|
|
@@ -182,6 +189,7 @@ export class ProofOfPossessionBuilder<DIDDoc> {
|
|
|
182
189
|
{
|
|
183
190
|
typ: this.typ ?? (this.version < OpenId4VCIVersion.VER_1_0_11 ? 'jwt' : 'openid4vci-proof+jwt'),
|
|
184
191
|
kid: this.kid,
|
|
192
|
+
jwk: this.jwk,
|
|
185
193
|
jti: this.jti,
|
|
186
194
|
alg: this.alg,
|
|
187
195
|
issuer: this.issuer,
|
|
@@ -3,6 +3,7 @@ import { KeyObject } from 'crypto';
|
|
|
3
3
|
import {
|
|
4
4
|
Alg,
|
|
5
5
|
EndpointMetadata,
|
|
6
|
+
getCredentialRequestForVersion,
|
|
6
7
|
getIssuerFromCredentialOfferPayload,
|
|
7
8
|
Jwt,
|
|
8
9
|
OpenId4VCIVersion,
|
|
@@ -127,7 +128,7 @@ describe('Credential Request Client ', () => {
|
|
|
127
128
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
128
129
|
});
|
|
129
130
|
expect(credentialRequest.proof?.jwt?.includes(partialJWT)).toBeTruthy();
|
|
130
|
-
expect(credentialRequest.format).toEqual('
|
|
131
|
+
expect(credentialRequest.format).toEqual('jwt_vc');
|
|
131
132
|
const result = await credReqClient.acquireCredentialsUsingRequest(credentialRequest);
|
|
132
133
|
expect(result?.successBody?.credential).toEqual(mockedVC);
|
|
133
134
|
});
|
|
@@ -149,17 +150,22 @@ describe('Credential Request Client ', () => {
|
|
|
149
150
|
.withKid(kid)
|
|
150
151
|
.withClientId('sphereon:wallet')
|
|
151
152
|
.build();
|
|
152
|
-
|
|
153
|
-
// @ts-ignore
|
|
154
|
-
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json-ld', types: ['random'], proof })).rejects.toThrow(
|
|
153
|
+
await expect(credReqClient.acquireCredentialsUsingRequest({ format: 'jwt_vc_json', types: ['random'], proof })).rejects.toThrow(
|
|
155
154
|
Error(URL_NOT_VALID),
|
|
156
155
|
);
|
|
157
156
|
});
|
|
158
157
|
});
|
|
159
158
|
|
|
160
159
|
describe('Credential Request Client with Walt.id ', () => {
|
|
161
|
-
|
|
160
|
+
beforeAll(() => {
|
|
162
161
|
nock.cleanAll();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
nock.cleanAll();
|
|
166
|
+
});
|
|
167
|
+
it('should have correct metadata endpoints', async function () {
|
|
168
|
+
// nock.cleanAll();
|
|
163
169
|
const WALT_IRR_URI =
|
|
164
170
|
'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Foidc%2F&credential_type=OpenBadgeCredential&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhOTUyZjUxNi1jYWVmLTQ4YjMtODIxYy00OTRkYzgyNjljZjAiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.YE5DlalcLC2ChGEg47CQDaN1gTxbaQqSclIVqsSAUHE&user_pin_required=false';
|
|
165
171
|
const credentialOffer = await CredentialOfferClient.fromURI(WALT_IRR_URI);
|
|
@@ -194,10 +200,11 @@ describe('Credential Request Client with different issuers ', () => {
|
|
|
194
200
|
jwt: getMockData('spruce')?.credential.request.proof.jwt as string,
|
|
195
201
|
},
|
|
196
202
|
credentialTypes: ['OpenBadgeCredential'],
|
|
197
|
-
format: '
|
|
203
|
+
format: 'jwt_vc',
|
|
198
204
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
199
205
|
});
|
|
200
|
-
|
|
206
|
+
const draft8CredentialRequest = getCredentialRequestForVersion(credentialRequest, OpenId4VCIVersion.VER_1_0_08);
|
|
207
|
+
expect(draft8CredentialRequest).toEqual(getMockData('spruce')?.credential.request);
|
|
201
208
|
});
|
|
202
209
|
|
|
203
210
|
it('should create correct CredentialRequest for Walt', async () => {
|
|
@@ -264,7 +271,8 @@ describe('Credential Request Client with different issuers ', () => {
|
|
|
264
271
|
format: 'ldp_vc',
|
|
265
272
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
266
273
|
});
|
|
267
|
-
|
|
274
|
+
const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08);
|
|
275
|
+
expect(credentialRequest).toEqual(getMockData('mattr')?.credential.request);
|
|
268
276
|
});
|
|
269
277
|
|
|
270
278
|
it('should create correct CredentialRequest for diwala', async () => {
|
|
@@ -286,6 +294,10 @@ describe('Credential Request Client with different issuers ', () => {
|
|
|
286
294
|
format: 'ldp_vc',
|
|
287
295
|
version: OpenId4VCIVersion.VER_1_0_08,
|
|
288
296
|
});
|
|
289
|
-
|
|
297
|
+
|
|
298
|
+
// createCredentialRequest returns uniform format in draft 11
|
|
299
|
+
const credentialRequest = getCredentialRequestForVersion(credentialOffer, OpenId4VCIVersion.VER_1_0_08);
|
|
300
|
+
|
|
301
|
+
expect(credentialRequest).toEqual(getMockData('diwala')?.credential.request);
|
|
290
302
|
});
|
|
291
303
|
});
|