@sphereon/oid4vci-client 0.8.2-unstable.49 → 0.8.2-unstable.52
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/AuthorizationCodeClient.d.ts +9 -0
- package/dist/AuthorizationCodeClient.d.ts.map +1 -0
- package/dist/AuthorizationCodeClient.js +121 -0
- package/dist/AuthorizationCodeClient.js.map +1 -0
- package/dist/CredentialOfferClient.d.ts.map +1 -1
- package/dist/CredentialOfferClient.js +3 -1
- package/dist/CredentialOfferClient.js.map +1 -1
- package/dist/OpenID4VCIClient.d.ts +20 -7
- package/dist/OpenID4VCIClient.d.ts.map +1 -1
- package/dist/OpenID4VCIClient.js +89 -110
- package/dist/OpenID4VCIClient.js.map +1 -1
- package/dist/functions/AuthorizationUtil.d.ts +2 -2
- package/dist/functions/AuthorizationUtil.d.ts.map +1 -1
- package/dist/functions/AuthorizationUtil.js +3 -3
- package/dist/functions/AuthorizationUtil.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +0 -48
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -15
- package/dist/types/index.js.map +1 -1
- package/lib/AuthorizationCodeClient.ts +149 -0
- package/lib/CredentialOfferClient.ts +3 -0
- package/lib/OpenID4VCIClient.ts +117 -124
- package/lib/__tests__/EBSIE2E.spec.test.ts +3 -1
- package/lib/__tests__/OpenID4VCIClient.spec.ts +78 -48
- package/lib/__tests__/OpenID4VCIClientPAR.spec.ts +48 -36
- package/lib/functions/AuthorizationUtil.ts +2 -4
- package/lib/index.ts +1 -0
- package/lib/types/index.ts +0 -56
- package/package.json +3 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.generateMissingPKCEOpts = void 0;
|
|
4
4
|
const oid4vci_common_1 = require("@sphereon/oid4vci-common");
|
|
5
|
-
const
|
|
5
|
+
const generateMissingPKCEOpts = (pkce) => {
|
|
6
6
|
if (pkce.disabled) {
|
|
7
7
|
return pkce;
|
|
8
8
|
}
|
|
@@ -18,5 +18,5 @@ const createPKCEOpts = (pkce) => {
|
|
|
18
18
|
}
|
|
19
19
|
return pkce;
|
|
20
20
|
};
|
|
21
|
-
exports.
|
|
21
|
+
exports.generateMissingPKCEOpts = generateMissingPKCEOpts;
|
|
22
22
|
//# sourceMappingURL=AuthorizationUtil.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthorizationUtil.js","sourceRoot":"","sources":["../../lib/functions/AuthorizationUtil.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"AuthorizationUtil.js","sourceRoot":"","sources":["../../lib/functions/AuthorizationUtil.ts"],"names":[],"mappings":";;;AAAA,6DAA6I;AAEtI,MAAM,uBAAuB,GAAG,CAAC,IAAc,EAAE,EAAE;IACxD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC9B,IAAI,CAAC,mBAAmB,GAAG,oCAAmB,CAAC,IAAI,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACvB,IAAI,CAAC,YAAY,GAAG,IAAA,qCAAoB,GAAE,CAAC;IAC7C,CAAC;IACD,IAAA,wCAAuB,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QACxB,IAAI,CAAC,aAAa,GAAG,IAAA,oCAAmB,EAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAfW,QAAA,uBAAuB,2BAelC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,kCAAkC,CAAC;AACjD,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,4BAA4B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./AccessTokenClient"), exports);
|
|
18
|
+
__exportStar(require("./CredentialRequestClient"), exports);
|
|
18
19
|
__exportStar(require("./CredentialOfferClient"), exports);
|
|
19
20
|
__exportStar(require("./CredentialRequestClient"), exports);
|
|
20
21
|
__exportStar(require("./CredentialRequestClientBuilder"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,0DAAwC;AACxC,4DAA0C;AAC1C,mEAAiD;AACjD,8CAA4B;AAC5B,mDAAiC;AACjC,qDAAmC;AACnC,6DAA2C"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,4DAA0C;AAC1C,0DAAwC;AACxC,4DAA0C;AAC1C,mEAAiD;AACjD,8CAA4B;AAC5B,mDAAiC;AACjC,qDAAmC;AACnC,6DAA2C"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,49 +1 @@
|
|
|
1
|
-
import { CodeChallengeMethod } from '@sphereon/oid4vci-common';
|
|
2
|
-
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
3
|
-
export interface AuthDetails {
|
|
4
|
-
type: 'openid_credential' | string;
|
|
5
|
-
locations?: string | string[];
|
|
6
|
-
format: CredentialFormat | CredentialFormat[];
|
|
7
|
-
[s: string]: unknown;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Determinse whether PAR should be used when supported
|
|
11
|
-
*
|
|
12
|
-
* REQUIRE: Require PAR, if AS does not support it throw an error
|
|
13
|
-
* AUTO: Use PAR is the AS supports it, otherwise construct a reqular URI,
|
|
14
|
-
* NEVER: Do not use PAR even if the AS supports it (not recommended)
|
|
15
|
-
*/
|
|
16
|
-
export declare enum PARMode {
|
|
17
|
-
REQUIRE = 0,
|
|
18
|
-
AUTO = 1,
|
|
19
|
-
NEVER = 2
|
|
20
|
-
}
|
|
21
|
-
export interface AuthRequestOpts {
|
|
22
|
-
pkce?: PKCEOpts;
|
|
23
|
-
parMode?: PARMode;
|
|
24
|
-
authorizationDetails?: AuthDetails | AuthDetails[];
|
|
25
|
-
redirectUri: string;
|
|
26
|
-
scope?: string;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Optional options to provide PKCE params like code verifier and challenge yourself, or to disable PKCE altogether. If not provide PKCE will still be used! If individual params are not provide, they will be generated/calculated
|
|
30
|
-
*/
|
|
31
|
-
export interface PKCEOpts {
|
|
32
|
-
/**
|
|
33
|
-
* PKCE is enabled by default even if you do not provide these options. Set this to true to disable PKCE
|
|
34
|
-
*/
|
|
35
|
-
disabled?: boolean;
|
|
36
|
-
/**
|
|
37
|
-
* Provide a code_challenge, otherwise it will be calculated using the code_verifier and method
|
|
38
|
-
*/
|
|
39
|
-
codeChallenge?: string;
|
|
40
|
-
/**
|
|
41
|
-
* The code_challenge_method, should always by S256
|
|
42
|
-
*/
|
|
43
|
-
codeChallengeMethod?: CodeChallengeMethod;
|
|
44
|
-
/**
|
|
45
|
-
* Provide a code_verifier, otherwise it will be generated
|
|
46
|
-
*/
|
|
47
|
-
codeVerifier?: string;
|
|
48
|
-
}
|
|
49
1
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":""}
|
package/dist/types/index.js
CHANGED
|
@@ -1,17 +1,2 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PARMode = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Determinse whether PAR should be used when supported
|
|
6
|
-
*
|
|
7
|
-
* REQUIRE: Require PAR, if AS does not support it throw an error
|
|
8
|
-
* AUTO: Use PAR is the AS supports it, otherwise construct a reqular URI,
|
|
9
|
-
* NEVER: Do not use PAR even if the AS supports it (not recommended)
|
|
10
|
-
*/
|
|
11
|
-
var PARMode;
|
|
12
|
-
(function (PARMode) {
|
|
13
|
-
PARMode[PARMode["REQUIRE"] = 0] = "REQUIRE";
|
|
14
|
-
PARMode[PARMode["AUTO"] = 1] = "AUTO";
|
|
15
|
-
PARMode[PARMode["NEVER"] = 2] = "NEVER";
|
|
16
|
-
})(PARMode || (exports.PARMode = PARMode = {}));
|
|
17
2
|
//# sourceMappingURL=index.js.map
|
package/dist/types/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/types/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthorizationDetails,
|
|
3
|
+
AuthorizationRequestOpts,
|
|
4
|
+
CodeChallengeMethod,
|
|
5
|
+
convertJsonToURI,
|
|
6
|
+
CredentialOfferRequestWithBaseUrl,
|
|
7
|
+
CredentialSupported,
|
|
8
|
+
EndpointMetadataResult,
|
|
9
|
+
JsonURIMode,
|
|
10
|
+
PARMode,
|
|
11
|
+
PKCEOpts,
|
|
12
|
+
PushedAuthorizationResponse,
|
|
13
|
+
ResponseType,
|
|
14
|
+
} from '@sphereon/oid4vci-common';
|
|
15
|
+
import { formPost } from '@sphereon/oid4vci-common';
|
|
16
|
+
|
|
17
|
+
export const createAuthorizationRequestUrl = async ({
|
|
18
|
+
pkce,
|
|
19
|
+
endpointMetadata,
|
|
20
|
+
authorizationRequest,
|
|
21
|
+
credentialOffer,
|
|
22
|
+
credentialsSupported,
|
|
23
|
+
}: {
|
|
24
|
+
pkce: PKCEOpts;
|
|
25
|
+
endpointMetadata: EndpointMetadataResult;
|
|
26
|
+
authorizationRequest: AuthorizationRequestOpts;
|
|
27
|
+
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
28
|
+
credentialsSupported?: CredentialSupported[];
|
|
29
|
+
}): Promise<string> => {
|
|
30
|
+
const { redirectUri } = authorizationRequest;
|
|
31
|
+
let { scope, authorizationDetails } = authorizationRequest;
|
|
32
|
+
const parMode = endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
|
|
33
|
+
? PARMode.REQUIRE
|
|
34
|
+
: authorizationRequest.parMode ?? PARMode.AUTO;
|
|
35
|
+
// Scope and authorization_details can be used in the same authorization request
|
|
36
|
+
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
37
|
+
if (!scope && !authorizationDetails) {
|
|
38
|
+
if (!credentialOffer) {
|
|
39
|
+
throw Error('Please provide a scope or authorization_details if no credential offer is present');
|
|
40
|
+
}
|
|
41
|
+
const creds = credentialOffer.credential_offer.credentials;
|
|
42
|
+
|
|
43
|
+
// FIXME: complains about VCT for sd-jwt
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
authorizationDetails = creds
|
|
47
|
+
.flatMap((cred) => (typeof cred === 'string' ? credentialsSupported : (cred as CredentialSupported)))
|
|
48
|
+
.filter((cred) => !!cred)
|
|
49
|
+
.map((cred) => {
|
|
50
|
+
return {
|
|
51
|
+
...cred,
|
|
52
|
+
type: 'openid_credential',
|
|
53
|
+
locations: [endpointMetadata.issuer],
|
|
54
|
+
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
56
|
+
// @ts-ignore
|
|
57
|
+
format: cred!.format,
|
|
58
|
+
} satisfies AuthorizationDetails;
|
|
59
|
+
});
|
|
60
|
+
if (!authorizationDetails || authorizationDetails.length === 0) {
|
|
61
|
+
throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!endpointMetadata?.authorization_endpoint) {
|
|
65
|
+
throw Error('Server metadata does not contain authorization endpoint');
|
|
66
|
+
}
|
|
67
|
+
const parEndpoint = endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint;
|
|
68
|
+
|
|
69
|
+
// add 'openid' scope if not present
|
|
70
|
+
if (!scope?.includes('openid')) {
|
|
71
|
+
scope = ['openid', scope].filter((s) => !!s).join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
|
|
75
|
+
response_type: ResponseType.AUTH_CODE,
|
|
76
|
+
...(!pkce.disabled && {
|
|
77
|
+
code_challenge_method: pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
|
|
78
|
+
code_challenge: pkce.codeChallenge,
|
|
79
|
+
}),
|
|
80
|
+
authorization_details: JSON.stringify(handleAuthorizationDetails(endpointMetadata, authorizationDetails)),
|
|
81
|
+
redirect_uri: redirectUri,
|
|
82
|
+
scope: scope,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (authorizationRequest.clientId) {
|
|
86
|
+
queryObj['client_id'] = authorizationRequest.clientId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (credentialOffer?.issuerState) {
|
|
90
|
+
queryObj['issuer_state'] = credentialOffer.issuerState;
|
|
91
|
+
}
|
|
92
|
+
if (!parEndpoint && parMode === PARMode.REQUIRE) {
|
|
93
|
+
throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`);
|
|
94
|
+
} else if (parEndpoint && parMode !== PARMode.NEVER) {
|
|
95
|
+
const parResponse = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));
|
|
96
|
+
if (parResponse.errorBody || !parResponse.successBody) {
|
|
97
|
+
throw Error(`PAR error`);
|
|
98
|
+
}
|
|
99
|
+
queryObj = { request_uri: parResponse.successBody.request_uri };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const url = convertJsonToURI(queryObj, {
|
|
103
|
+
baseUrl: endpointMetadata.authorization_endpoint,
|
|
104
|
+
uriTypeProperties: ['request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
|
|
105
|
+
mode: JsonURIMode.X_FORM_WWW_URLENCODED,
|
|
106
|
+
// We do not add the version here, as this always needs to be form encoded
|
|
107
|
+
});
|
|
108
|
+
console.log(`Authorization Request URL: ${url}`);
|
|
109
|
+
return url;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleAuthorizationDetails = (
|
|
113
|
+
endpointMetadata: EndpointMetadataResult,
|
|
114
|
+
authorizationDetails?: AuthorizationDetails | AuthorizationDetails[],
|
|
115
|
+
): AuthorizationDetails | AuthorizationDetails[] | undefined => {
|
|
116
|
+
if (authorizationDetails) {
|
|
117
|
+
if (typeof authorizationDetails === 'string') {
|
|
118
|
+
// backwards compat for older versions of the lib
|
|
119
|
+
return authorizationDetails;
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(authorizationDetails)) {
|
|
122
|
+
return authorizationDetails
|
|
123
|
+
.filter((value) => typeof value !== 'string')
|
|
124
|
+
.map((value) => handleLocations(endpointMetadata, typeof value === 'string' ? value : { ...value }));
|
|
125
|
+
} else {
|
|
126
|
+
return handleLocations(endpointMetadata, { ...authorizationDetails });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return authorizationDetails;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleLocations = (endpointMetadata: EndpointMetadataResult, authorizationDetails: AuthorizationDetails) => {
|
|
133
|
+
if (typeof authorizationDetails === 'string') {
|
|
134
|
+
// backwards compat for older versions of the lib
|
|
135
|
+
return authorizationDetails;
|
|
136
|
+
}
|
|
137
|
+
if (authorizationDetails && (endpointMetadata.credentialIssuerMetadata?.authorization_server || endpointMetadata.authorization_endpoint)) {
|
|
138
|
+
if (authorizationDetails.locations) {
|
|
139
|
+
if (Array.isArray(authorizationDetails.locations)) {
|
|
140
|
+
(authorizationDetails.locations as string[]).push(endpointMetadata.issuer);
|
|
141
|
+
} else {
|
|
142
|
+
authorizationDetails.locations = [authorizationDetails.locations as string, endpointMetadata.issuer];
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
authorizationDetails.locations = [endpointMetadata.issuer];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return authorizationDetails;
|
|
149
|
+
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CredentialOfferRequestWithBaseUrl,
|
|
6
6
|
CredentialOfferV1_0_11,
|
|
7
7
|
determineSpecVersionFromURI,
|
|
8
|
+
getClientIdFromCredentialOfferPayload,
|
|
8
9
|
OpenId4VCIVersion,
|
|
9
10
|
toUniformCredentialOfferRequest,
|
|
10
11
|
} from '@sphereon/oid4vci-common';
|
|
@@ -43,6 +44,7 @@ export class CredentialOfferClient {
|
|
|
43
44
|
throw Error('Either a credential_offer or credential_offer_uri should be present in ' + uri);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
const clientId = getClientIdFromCredentialOfferPayload(credentialOffer?.credential_offer);
|
|
46
48
|
const request = await toUniformCredentialOfferRequest(credentialOffer, {
|
|
47
49
|
...opts,
|
|
48
50
|
version,
|
|
@@ -53,6 +55,7 @@ export class CredentialOfferClient {
|
|
|
53
55
|
return {
|
|
54
56
|
scheme,
|
|
55
57
|
baseUrl,
|
|
58
|
+
clientId,
|
|
56
59
|
...request,
|
|
57
60
|
...(grants?.authorization_code?.issuer_state && { issuerState: grants.authorization_code.issuer_state }),
|
|
58
61
|
...(grants?.['urn:ietf:params:oauth:grant-type:pre-authorized_code']?.['pre-authorized_code'] && {
|
package/lib/OpenID4VCIClient.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AccessTokenResponse,
|
|
3
3
|
Alg,
|
|
4
|
+
AuthorizationRequestOpts,
|
|
4
5
|
AuthzFlowType,
|
|
5
6
|
CodeChallengeMethod,
|
|
6
7
|
CredentialOfferPayloadV1_0_08,
|
|
@@ -11,26 +12,23 @@ import {
|
|
|
11
12
|
getIssuerFromCredentialOfferPayload,
|
|
12
13
|
getSupportedCredentials,
|
|
13
14
|
getTypesFromCredentialSupported,
|
|
14
|
-
JsonURIMode,
|
|
15
15
|
JWK,
|
|
16
16
|
KID_JWK_X5C_ERROR,
|
|
17
17
|
OID4VCICredentialFormat,
|
|
18
18
|
OpenId4VCIVersion,
|
|
19
|
+
PKCEOpts,
|
|
19
20
|
ProofOfPossessionCallbacks,
|
|
20
|
-
PushedAuthorizationResponse,
|
|
21
|
-
ResponseType,
|
|
22
21
|
} from '@sphereon/oid4vci-common';
|
|
23
22
|
import { CredentialFormat } from '@sphereon/ssi-types';
|
|
24
23
|
import Debug from 'debug';
|
|
25
24
|
|
|
26
25
|
import { AccessTokenClient } from './AccessTokenClient';
|
|
26
|
+
import { createAuthorizationRequestUrl } from './AuthorizationCodeClient';
|
|
27
27
|
import { CredentialOfferClient } from './CredentialOfferClient';
|
|
28
28
|
import { CredentialRequestClientBuilder } from './CredentialRequestClientBuilder';
|
|
29
29
|
import { MetadataClient } from './MetadataClient';
|
|
30
30
|
import { ProofOfPossessionBuilder } from './ProofOfPossessionBuilder';
|
|
31
|
-
import {
|
|
32
|
-
import { createPKCEOpts } from './functions/AuthorizationUtil';
|
|
33
|
-
import { AuthDetails, AuthRequestOpts, PARMode, PKCEOpts } from './types';
|
|
31
|
+
import { generateMissingPKCEOpts } from './functions/AuthorizationUtil';
|
|
34
32
|
|
|
35
33
|
const debug = Debug('sphereon:oid4vci');
|
|
36
34
|
|
|
@@ -44,6 +42,9 @@ export class OpenID4VCIClient {
|
|
|
44
42
|
private _endpointMetadata: EndpointMetadataResult | undefined;
|
|
45
43
|
private _accessTokenResponse: AccessTokenResponse | undefined;
|
|
46
44
|
private _pkce: PKCEOpts = { disabled: false, codeChallengeMethod: CodeChallengeMethod.S256 };
|
|
45
|
+
private _authorizationRequestOpts?: AuthorizationRequestOpts;
|
|
46
|
+
|
|
47
|
+
private _authorizationURL?: string;
|
|
47
48
|
|
|
48
49
|
private constructor({
|
|
49
50
|
credentialOffer,
|
|
@@ -51,12 +52,16 @@ export class OpenID4VCIClient {
|
|
|
51
52
|
kid,
|
|
52
53
|
alg,
|
|
53
54
|
credentialIssuer,
|
|
55
|
+
pkce,
|
|
56
|
+
authorizationRequest,
|
|
54
57
|
}: {
|
|
55
58
|
credentialOffer?: CredentialOfferRequestWithBaseUrl;
|
|
56
59
|
kid?: string;
|
|
57
60
|
alg?: Alg | string;
|
|
58
61
|
clientId?: string;
|
|
59
62
|
credentialIssuer?: string;
|
|
63
|
+
pkce?: PKCEOpts;
|
|
64
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
60
65
|
}) {
|
|
61
66
|
this._credentialOffer = credentialOffer;
|
|
62
67
|
const issuer = credentialIssuer ?? (credentialOffer ? getIssuerFromCredentialOfferPayload(credentialOffer.credential_offer) : undefined);
|
|
@@ -67,6 +72,9 @@ export class OpenID4VCIClient {
|
|
|
67
72
|
this._kid = kid;
|
|
68
73
|
this._alg = alg;
|
|
69
74
|
this._clientId = clientId;
|
|
75
|
+
this._pkce = { ...this._pkce, ...pkce };
|
|
76
|
+
this._authorizationRequestOpts = authorizationRequest;
|
|
77
|
+
this.syncAuthorizationRequestOpts(authorizationRequest);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
public static async fromCredentialIssuer({
|
|
@@ -75,17 +83,33 @@ export class OpenID4VCIClient {
|
|
|
75
83
|
retrieveServerMetadata,
|
|
76
84
|
clientId,
|
|
77
85
|
credentialIssuer,
|
|
86
|
+
pkce,
|
|
87
|
+
authorizationRequest,
|
|
88
|
+
createAuthorizationRequestURL,
|
|
78
89
|
}: {
|
|
79
90
|
credentialIssuer: string;
|
|
80
91
|
kid?: string;
|
|
81
92
|
alg?: Alg | string;
|
|
82
93
|
retrieveServerMetadata?: boolean;
|
|
83
94
|
clientId?: string;
|
|
95
|
+
createAuthorizationRequestURL?: boolean;
|
|
96
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
97
|
+
pkce?: PKCEOpts;
|
|
84
98
|
}) {
|
|
85
|
-
const client = new OpenID4VCIClient({
|
|
99
|
+
const client = new OpenID4VCIClient({
|
|
100
|
+
kid,
|
|
101
|
+
alg,
|
|
102
|
+
clientId: clientId ?? authorizationRequest?.clientId,
|
|
103
|
+
credentialIssuer,
|
|
104
|
+
pkce,
|
|
105
|
+
authorizationRequest,
|
|
106
|
+
});
|
|
86
107
|
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
87
108
|
await client.retrieveServerMetadata();
|
|
88
109
|
}
|
|
110
|
+
if (createAuthorizationRequestURL === undefined || createAuthorizationRequestURL) {
|
|
111
|
+
await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
|
|
112
|
+
}
|
|
89
113
|
return client;
|
|
90
114
|
}
|
|
91
115
|
|
|
@@ -95,28 +119,74 @@ export class OpenID4VCIClient {
|
|
|
95
119
|
alg,
|
|
96
120
|
retrieveServerMetadata,
|
|
97
121
|
clientId,
|
|
122
|
+
pkce,
|
|
123
|
+
createAuthorizationRequestURL,
|
|
124
|
+
authorizationRequest,
|
|
98
125
|
resolveOfferUri,
|
|
99
126
|
}: {
|
|
100
127
|
uri: string;
|
|
101
128
|
kid?: string;
|
|
102
129
|
alg?: Alg | string;
|
|
103
130
|
retrieveServerMetadata?: boolean;
|
|
131
|
+
createAuthorizationRequestURL?: boolean;
|
|
104
132
|
resolveOfferUri?: boolean;
|
|
133
|
+
pkce?: PKCEOpts;
|
|
105
134
|
clientId?: string;
|
|
135
|
+
authorizationRequest?: AuthorizationRequestOpts; // Can be provided here, or when manually calling createAuthorizationUrl
|
|
106
136
|
}): Promise<OpenID4VCIClient> {
|
|
137
|
+
const credentialOfferClient = await CredentialOfferClient.fromURI(uri, { resolve: resolveOfferUri });
|
|
107
138
|
const client = new OpenID4VCIClient({
|
|
108
|
-
credentialOffer:
|
|
139
|
+
credentialOffer: credentialOfferClient,
|
|
109
140
|
kid,
|
|
110
141
|
alg,
|
|
111
|
-
clientId,
|
|
142
|
+
clientId: clientId ?? authorizationRequest?.clientId ?? credentialOfferClient.clientId,
|
|
143
|
+
pkce,
|
|
144
|
+
authorizationRequest,
|
|
112
145
|
});
|
|
113
146
|
|
|
114
147
|
if (retrieveServerMetadata === undefined || retrieveServerMetadata) {
|
|
115
148
|
await client.retrieveServerMetadata();
|
|
116
149
|
}
|
|
150
|
+
if (
|
|
151
|
+
credentialOfferClient.supportedFlows.includes(AuthzFlowType.AUTHORIZATION_CODE_FLOW) &&
|
|
152
|
+
(createAuthorizationRequestURL === undefined || createAuthorizationRequestURL)
|
|
153
|
+
) {
|
|
154
|
+
console.log(`AUTH REQ`);
|
|
155
|
+
await client.createAuthorizationRequestUrl({ authorizationRequest, pkce });
|
|
156
|
+
console.log(`AUTH REQ URL: ${client._authorizationURL}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
117
159
|
return client;
|
|
118
160
|
}
|
|
119
161
|
|
|
162
|
+
public async createAuthorizationRequestUrl(opts?: { authorizationRequest?: AuthorizationRequestOpts; pkce?: PKCEOpts }): Promise<string> {
|
|
163
|
+
if (!this._authorizationURL) {
|
|
164
|
+
this.calculatePKCEOpts(opts?.pkce);
|
|
165
|
+
this.syncAuthorizationRequestOpts(opts?.authorizationRequest);
|
|
166
|
+
if (!this._authorizationRequestOpts) {
|
|
167
|
+
throw Error(`No Authorization Request options present or provided in this call`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
171
|
+
// handling this because of the support for v1_0-08
|
|
172
|
+
if (
|
|
173
|
+
this._endpointMetadata &&
|
|
174
|
+
this._endpointMetadata.credentialIssuerMetadata &&
|
|
175
|
+
'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
|
|
176
|
+
) {
|
|
177
|
+
this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
|
|
178
|
+
}
|
|
179
|
+
this._authorizationURL = await createAuthorizationRequestUrl({
|
|
180
|
+
pkce: this._pkce,
|
|
181
|
+
endpointMetadata: this.endpointMetadata,
|
|
182
|
+
authorizationRequest: this._authorizationRequestOpts,
|
|
183
|
+
credentialOffer: this.credentialOffer,
|
|
184
|
+
credentialsSupported: this.getCredentialsSupported(true),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return this._authorizationURL;
|
|
188
|
+
}
|
|
189
|
+
|
|
120
190
|
public async retrieveServerMetadata(): Promise<EndpointMetadataResult> {
|
|
121
191
|
this.assertIssuerData();
|
|
122
192
|
if (!this._endpointMetadata) {
|
|
@@ -131,117 +201,8 @@ export class OpenID4VCIClient {
|
|
|
131
201
|
return this.endpointMetadata;
|
|
132
202
|
}
|
|
133
203
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let { scope, authorizationDetails } = opts;
|
|
137
|
-
const parMode = this._endpointMetadata?.credentialIssuerMetadata?.require_pushed_authorization_requests
|
|
138
|
-
? PARMode.REQUIRE
|
|
139
|
-
: opts.parMode ?? PARMode.AUTO;
|
|
140
|
-
this._pkce = createPKCEOpts({ ...this._pkce, ...opts.pkce });
|
|
141
|
-
// Scope and authorization_details can be used in the same authorization request
|
|
142
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param
|
|
143
|
-
if (!scope && !authorizationDetails) {
|
|
144
|
-
if (!this.credentialOffer) {
|
|
145
|
-
throw Error('Please provide a scope or authorization_details');
|
|
146
|
-
}
|
|
147
|
-
const creds = this.credentialOffer.credential_offer.credentials;
|
|
148
|
-
|
|
149
|
-
authorizationDetails = creds
|
|
150
|
-
.flatMap((cred) => (typeof cred === 'string' ? this.getCredentialsSupported(true) : (cred as CredentialSupported)))
|
|
151
|
-
.map((cred) => {
|
|
152
|
-
return {
|
|
153
|
-
...cred,
|
|
154
|
-
type: 'openid_credential',
|
|
155
|
-
locations: [this._credentialIssuer],
|
|
156
|
-
format: cred.format,
|
|
157
|
-
} satisfies AuthDetails;
|
|
158
|
-
});
|
|
159
|
-
if (authorizationDetails.length === 0) {
|
|
160
|
-
throw Error(`Could not create authorization details from credential offer. Please pass in explicit details`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
// todo: Probably can go with current logic in MetadataClient who will always set the authorization_endpoint when found
|
|
164
|
-
// handling this because of the support for v1_0-08
|
|
165
|
-
if (
|
|
166
|
-
this._endpointMetadata &&
|
|
167
|
-
this._endpointMetadata.credentialIssuerMetadata &&
|
|
168
|
-
'authorization_endpoint' in this._endpointMetadata.credentialIssuerMetadata
|
|
169
|
-
) {
|
|
170
|
-
this._endpointMetadata.authorization_endpoint = this._endpointMetadata.credentialIssuerMetadata.authorization_endpoint as string;
|
|
171
|
-
}
|
|
172
|
-
if (!this._endpointMetadata?.authorization_endpoint) {
|
|
173
|
-
throw Error('Server metadata does not contain authorization endpoint');
|
|
174
|
-
}
|
|
175
|
-
const parEndpoint = this._endpointMetadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint;
|
|
176
|
-
|
|
177
|
-
// add 'openid' scope if not present
|
|
178
|
-
if (!scope?.includes('openid')) {
|
|
179
|
-
scope = ['openid', scope].filter((s) => !!s).join(' ');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let queryObj: { [key: string]: string } | PushedAuthorizationResponse = {
|
|
183
|
-
response_type: ResponseType.AUTH_CODE,
|
|
184
|
-
...(!this._pkce.disabled && {
|
|
185
|
-
code_challenge_method: this._pkce.codeChallengeMethod ?? CodeChallengeMethod.S256,
|
|
186
|
-
code_challenge: this._pkce.codeChallenge,
|
|
187
|
-
}),
|
|
188
|
-
authorization_details: JSON.stringify(this.handleAuthorizationDetails(authorizationDetails)),
|
|
189
|
-
redirect_uri: redirectUri,
|
|
190
|
-
scope: scope,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
if (this.clientId) {
|
|
194
|
-
queryObj['client_id'] = this.clientId;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (this.credentialOffer?.issuerState) {
|
|
198
|
-
queryObj['issuer_state'] = this.credentialOffer.issuerState;
|
|
199
|
-
}
|
|
200
|
-
if (!parEndpoint && parMode === PARMode.REQUIRE) {
|
|
201
|
-
throw Error(`PAR mode is set to required by Authorization Server does not support PAR!`);
|
|
202
|
-
} else if (parEndpoint && parMode !== PARMode.NEVER) {
|
|
203
|
-
const parResponse = await formPost<PushedAuthorizationResponse>(parEndpoint, new URLSearchParams(queryObj));
|
|
204
|
-
if (parResponse.errorBody || !parResponse.successBody) {
|
|
205
|
-
throw Error(`PAR error`);
|
|
206
|
-
}
|
|
207
|
-
queryObj = { request_uri: parResponse.successBody.request_uri };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return convertJsonToURI(queryObj, {
|
|
211
|
-
baseUrl: this._endpointMetadata.authorization_endpoint,
|
|
212
|
-
uriTypeProperties: ['request_uri', 'redirect_uri', 'scope', 'authorization_details', 'issuer_state'],
|
|
213
|
-
mode: JsonURIMode.X_FORM_WWW_URLENCODED,
|
|
214
|
-
// We do not add the version here, as this always needs to be form encoded
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
public handleAuthorizationDetails(authorizationDetails?: AuthDetails | AuthDetails[]): AuthDetails | AuthDetails[] | undefined {
|
|
219
|
-
if (authorizationDetails) {
|
|
220
|
-
if (Array.isArray(authorizationDetails)) {
|
|
221
|
-
return authorizationDetails.map((value) => this.handleLocations({ ...value }));
|
|
222
|
-
} else {
|
|
223
|
-
return this.handleLocations({ ...authorizationDetails });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return authorizationDetails;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private handleLocations(authorizationDetails: AuthDetails) {
|
|
230
|
-
if (
|
|
231
|
-
authorizationDetails &&
|
|
232
|
-
(this.endpointMetadata.credentialIssuerMetadata?.authorization_server || this.endpointMetadata.authorization_endpoint)
|
|
233
|
-
) {
|
|
234
|
-
if (authorizationDetails.locations) {
|
|
235
|
-
if (Array.isArray(authorizationDetails.locations)) {
|
|
236
|
-
(authorizationDetails.locations as string[]).push(this.endpointMetadata.issuer);
|
|
237
|
-
} else {
|
|
238
|
-
authorizationDetails.locations = [authorizationDetails.locations as string, this.endpointMetadata.issuer];
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
authorizationDetails.locations = this.endpointMetadata.issuer;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return authorizationDetails;
|
|
204
|
+
private calculatePKCEOpts(pkce?: PKCEOpts) {
|
|
205
|
+
this._pkce = generateMissingPKCEOpts({ ...this._pkce, ...pkce });
|
|
245
206
|
}
|
|
246
207
|
|
|
247
208
|
public async acquireAccessToken(opts?: {
|
|
@@ -452,7 +413,22 @@ export class OpenID4VCIClient {
|
|
|
452
413
|
}
|
|
453
414
|
|
|
454
415
|
issuerSupportedFlowTypes(): AuthzFlowType[] {
|
|
455
|
-
return
|
|
416
|
+
return (
|
|
417
|
+
this.credentialOffer?.supportedFlows ??
|
|
418
|
+
(this._endpointMetadata?.credentialIssuerMetadata?.authorization_endpoint ? [AuthzFlowType.AUTHORIZATION_CODE_FLOW] : [])
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
isFlowTypeSupported(flowType: AuthzFlowType): boolean {
|
|
423
|
+
return this.issuerSupportedFlowTypes().includes(flowType);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
get authorizationURL(): string | undefined {
|
|
427
|
+
return this._authorizationURL;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
public hasAuthorizationURL(): boolean {
|
|
431
|
+
return !!this.authorizationURL;
|
|
456
432
|
}
|
|
457
433
|
|
|
458
434
|
get credentialOffer(): CredentialOfferRequestWithBaseUrl | undefined {
|
|
@@ -522,10 +498,10 @@ export class OpenID4VCIClient {
|
|
|
522
498
|
}
|
|
523
499
|
|
|
524
500
|
private assertIssuerData(): void {
|
|
525
|
-
if (!this.
|
|
526
|
-
throw Error(`No issuance initiation or credential offer present`);
|
|
527
|
-
} else if (!this._credentialIssuer) {
|
|
501
|
+
if (!this._credentialIssuer) {
|
|
528
502
|
throw Error(`No credential issuer value present`);
|
|
503
|
+
} else if (!this._credentialOffer && this._endpointMetadata && this.issuerSupportedFlowTypes().length === 0) {
|
|
504
|
+
throw Error(`No issuance initiation or credential offer present`);
|
|
529
505
|
}
|
|
530
506
|
}
|
|
531
507
|
|
|
@@ -540,4 +516,21 @@ export class OpenID4VCIClient {
|
|
|
540
516
|
throw Error(`No access token present`);
|
|
541
517
|
}
|
|
542
518
|
}
|
|
519
|
+
|
|
520
|
+
private syncAuthorizationRequestOpts(opts?: AuthorizationRequestOpts) {
|
|
521
|
+
if (!this._authorizationRequestOpts && !opts) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const authorizationRequestOpts = { ...this._authorizationRequestOpts, ...opts } as AuthorizationRequestOpts;
|
|
525
|
+
if (authorizationRequestOpts.clientId) {
|
|
526
|
+
this._clientId = authorizationRequestOpts.clientId;
|
|
527
|
+
}
|
|
528
|
+
if (this._clientId && authorizationRequestOpts) {
|
|
529
|
+
authorizationRequestOpts.clientId = this._clientId;
|
|
530
|
+
}
|
|
531
|
+
if (!authorizationRequestOpts.redirectUri) {
|
|
532
|
+
authorizationRequestOpts.redirectUri = 'openid4vc%3A';
|
|
533
|
+
}
|
|
534
|
+
this._authorizationRequestOpts = authorizationRequestOpts;
|
|
535
|
+
}
|
|
543
536
|
}
|
|
@@ -69,7 +69,9 @@ describe('OID4VCI-Client using Sphereon issuer should', () => {
|
|
|
69
69
|
|
|
70
70
|
if (credentialType !== 'CTWalletCrossPreAuthorisedInTime') {
|
|
71
71
|
const url = await client.createAuthorizationRequestUrl({
|
|
72
|
-
|
|
72
|
+
authorizationRequest: {
|
|
73
|
+
redirectUri: 'openid4vc%3A',
|
|
74
|
+
},
|
|
73
75
|
});
|
|
74
76
|
const result = await fetch(url);
|
|
75
77
|
console.log(result.text());
|