@axa-fr/oidc-client 7.4.1 → 7.5.1
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 +14 -2
- package/dist/crypto.d.ts +1 -0
- package/dist/index.js +798 -624
- package/dist/index.umd.cjs +2 -2
- package/dist/initSession.d.ts +6 -2
- package/dist/initWorker.d.ts +11 -7
- package/dist/jwt.d.ts +6 -0
- package/dist/login.d.ts +1 -1
- package/dist/oidc.d.ts +1 -0
- package/dist/oidcClient.d.ts +1 -0
- package/dist/requests.d.ts +9 -9
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/crypto.ts +11 -6
- package/src/initSession.ts +29 -9
- package/src/initWorker.ts +37 -10
- package/src/jwt.ts +248 -0
- package/src/login.ts +61 -21
- package/src/oidc.ts +68 -29
- package/src/oidcClient.ts +4 -0
- package/src/requests.ts +43 -10
- package/src/types.ts +1 -0
- package/src/version.ts +1 -1
package/src/jwt.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// code base on https://coolaj86.com/articles/sign-jwt-webcrypto-vanilla-js/
|
|
2
|
+
|
|
3
|
+
// String (UCS-2) to Uint8Array
|
|
4
|
+
//
|
|
5
|
+
// because... JavaScript, Strings, and Buffers
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
function strToUint8(str) {
|
|
8
|
+
return new TextEncoder().encode(str);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Binary String to URL-Safe Base64
|
|
12
|
+
//
|
|
13
|
+
// btoa (Binary-to-Ascii) means "binary string" to base64
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
function binToUrlBase64(bin) {
|
|
16
|
+
return btoa(bin)
|
|
17
|
+
.replace(/\+/g, '-')
|
|
18
|
+
.replace(/\//g, '_')
|
|
19
|
+
.replace(/=+/g, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// UTF-8 to Binary String
|
|
23
|
+
//
|
|
24
|
+
// Because JavaScript has a strange relationship with strings
|
|
25
|
+
// https://coolaj86.com/articles/base64-unicode-utf-8-javascript-and-you/
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
function utf8ToBinaryString(str) {
|
|
28
|
+
const escstr = encodeURIComponent(str);
|
|
29
|
+
// replaces any uri escape sequence, such as %0A,
|
|
30
|
+
// with binary escape, such as 0x0A
|
|
31
|
+
const binstr = escstr.replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
|
32
|
+
return String.fromCharCode(parseInt(p1, 16));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return binstr;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Uint8Array to URL Safe Base64
|
|
39
|
+
//
|
|
40
|
+
// the shortest distant between two encodings... binary string
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
function uint8ToUrlBase64(uint8) {
|
|
43
|
+
let bin = '';
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
uint8.forEach(function(code) {
|
|
46
|
+
bin += String.fromCharCode(code);
|
|
47
|
+
});
|
|
48
|
+
return binToUrlBase64(bin);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// UCS-2 String to URL-Safe Base64
|
|
52
|
+
//
|
|
53
|
+
// btoa doesn't work on UTF-8 strings
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
function strToUrlBase64(str) {
|
|
56
|
+
return binToUrlBase64(utf8ToBinaryString(str));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export var JWT = {};
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
JWT.sign = (jwk, headers, claims, jwtHeaderType= 'dpop+jwt') => {
|
|
62
|
+
// Make a shallow copy of the key
|
|
63
|
+
// (to set ext if it wasn't already set)
|
|
64
|
+
jwk = Object.assign({}, jwk);
|
|
65
|
+
|
|
66
|
+
// The headers should probably be empty
|
|
67
|
+
headers.typ = jwtHeaderType;
|
|
68
|
+
headers.alg = 'ES256';
|
|
69
|
+
if (!headers.kid) {
|
|
70
|
+
// alternate: see thumbprint function below
|
|
71
|
+
headers.jwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const jws = {
|
|
75
|
+
// @ts-ignore
|
|
76
|
+
// JWT "headers" really means JWS "protected headers"
|
|
77
|
+
protected: strToUrlBase64(JSON.stringify(headers)),
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
// JWT "claims" are really a JSON-defined JWS "payload"
|
|
80
|
+
payload: strToUrlBase64(JSON.stringify(claims))
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// To import as EC (ECDSA, P-256, SHA-256, ES256)
|
|
84
|
+
const keyType = {
|
|
85
|
+
name: 'ECDSA',
|
|
86
|
+
namedCurve: 'P-256',
|
|
87
|
+
hash: {name: 'ES256'}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// To make re-exportable as JSON (or DER/PEM)
|
|
91
|
+
const exportable = true;
|
|
92
|
+
|
|
93
|
+
// Import as a private key that isn't black-listed from signing
|
|
94
|
+
const privileges = ['sign'];
|
|
95
|
+
|
|
96
|
+
// Actually do the import, which comes out as an abstract key type
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
return window.crypto.subtle
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
.importKey('jwk', jwk, keyType, exportable, privileges)
|
|
101
|
+
.then(function(privateKey) {
|
|
102
|
+
// Convert UTF-8 to Uint8Array ArrayBuffer
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
const data = strToUint8(jws.protected + '.' + jws.payload);
|
|
105
|
+
|
|
106
|
+
// The signature and hash should match the bit-entropy of the key
|
|
107
|
+
// https://tools.ietf.org/html/rfc7518#section-3
|
|
108
|
+
const signatureType = {name: 'ECDSA', hash: {name: 'SHA-256'}};
|
|
109
|
+
|
|
110
|
+
return window.crypto.subtle.sign(signatureType, privateKey, data).then(function(signature) {
|
|
111
|
+
// returns an ArrayBuffer containing a JOSE (not X509) signature,
|
|
112
|
+
// which must be converted to Uint8 to be useful
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
jws.signature = uint8ToUrlBase64(new Uint8Array(signature));
|
|
115
|
+
|
|
116
|
+
// JWT is just a "compressed", "protected" JWS
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
return jws.protected + '.' + jws.payload + '.' + jws.signature;
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
const EC = {};
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
EC.generate = function() {
|
|
127
|
+
const keyType = {
|
|
128
|
+
name: 'ECDSA',
|
|
129
|
+
namedCurve: 'P-256'
|
|
130
|
+
};
|
|
131
|
+
const exportable = true;
|
|
132
|
+
const privileges = ['sign', 'verify'];
|
|
133
|
+
// @ts-ignore
|
|
134
|
+
return window.crypto.subtle.generateKey(keyType, exportable, privileges).then(function(key) {
|
|
135
|
+
// returns an abstract and opaque WebCrypto object,
|
|
136
|
+
// which in most cases you'll want to export as JSON to be able to save
|
|
137
|
+
return window.crypto.subtle.exportKey('jwk', key.privateKey);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Create a Public Key from a Private Key
|
|
142
|
+
//
|
|
143
|
+
// chops off the private parts
|
|
144
|
+
// @ts-ignore
|
|
145
|
+
EC.neuter = function(jwk) {
|
|
146
|
+
const copy = Object.assign({}, jwk);
|
|
147
|
+
delete copy.d;
|
|
148
|
+
copy.key_ops = ['verify'];
|
|
149
|
+
return copy;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export var JWK = {};
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
JWK.thumbprint = function(jwk) {
|
|
155
|
+
// lexigraphically sorted, no spaces
|
|
156
|
+
const sortedPub = '{"crv":"CRV","kty":"EC","x":"X","y":"Y"}'
|
|
157
|
+
.replace('CRV', jwk.crv)
|
|
158
|
+
.replace('X', jwk.x)
|
|
159
|
+
.replace('Y', jwk.y);
|
|
160
|
+
|
|
161
|
+
// The hash should match the size of the key,
|
|
162
|
+
// but we're only dealing with P-256
|
|
163
|
+
return window.crypto.subtle
|
|
164
|
+
.digest({ name: 'SHA-256' }, strToUint8(sortedPub))
|
|
165
|
+
.then(function(hash) {
|
|
166
|
+
return uint8ToUrlBase64(new Uint8Array(hash));
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
const guid = function () {
|
|
172
|
+
// RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or
|
|
173
|
+
// pseudo-random numbers.
|
|
174
|
+
// The algorithm is as follows:
|
|
175
|
+
// Set the two most significant bits (bits 6 and 7) of the
|
|
176
|
+
// clock_seq_hi_and_reserved to zero and one, respectively.
|
|
177
|
+
// Set the four most significant bits (bits 12 through 15) of the
|
|
178
|
+
// time_hi_and_version field to the 4-bit version number from
|
|
179
|
+
// Section 4.1.3. Version4
|
|
180
|
+
// Set all the other bits to randomly (or pseudo-randomly) chosen
|
|
181
|
+
// values.
|
|
182
|
+
// UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node
|
|
183
|
+
// time-low = 4hexOctet
|
|
184
|
+
// time-mid = 2hexOctet
|
|
185
|
+
// time-high-and-version = 2hexOctet
|
|
186
|
+
// clock-seq-and-reserved = hexOctet:
|
|
187
|
+
// clock-seq-low = hexOctet
|
|
188
|
+
// node = 6hexOctet
|
|
189
|
+
// Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
190
|
+
// y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10
|
|
191
|
+
// y values are 8, 9, A, B
|
|
192
|
+
const guidHolder = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
|
193
|
+
const hex = '0123456789abcdef';
|
|
194
|
+
let r = 0;
|
|
195
|
+
let guidResponse = "";
|
|
196
|
+
for (let i = 0; i < 36; i++) {
|
|
197
|
+
if (guidHolder[i] !== '-' && guidHolder[i] !== '4') {
|
|
198
|
+
// each x and y needs to be random
|
|
199
|
+
r = Math.random() * 16 | 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (guidHolder[i] === 'x') {
|
|
203
|
+
guidResponse += hex[r];
|
|
204
|
+
} else if (guidHolder[i] === 'y') {
|
|
205
|
+
// clock-seq-and-reserved first hex is filtered and remaining hex values are random
|
|
206
|
+
r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0??
|
|
207
|
+
r |= 0x8; // set pos 3 to 1 as 1???
|
|
208
|
+
guidResponse += hex[r];
|
|
209
|
+
} else {
|
|
210
|
+
guidResponse += guidHolder[i];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return guidResponse;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
export const generateJwkAsync = () => {
|
|
219
|
+
// @ts-ignore
|
|
220
|
+
return EC.generate().then(function(jwk) {
|
|
221
|
+
// console.info('Private Key:', JSON.stringify(jwk));
|
|
222
|
+
// @ts-ignore
|
|
223
|
+
// console.info('Public Key:', JSON.stringify(EC.neuter(jwk)));
|
|
224
|
+
return jwk;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const generateJwtDemonstratingProofOfPossessionAsync = (jwk, method = 'POST', url: string, extrasClaims={}) => {
|
|
229
|
+
|
|
230
|
+
const claims = {
|
|
231
|
+
// https://www.rfc-editor.org/rfc/rfc9449.html#name-concept
|
|
232
|
+
jit: btoa(guid()),
|
|
233
|
+
htm: method,
|
|
234
|
+
htu: url,
|
|
235
|
+
iat: Math.round(Date.now() / 1000),
|
|
236
|
+
...extrasClaims,
|
|
237
|
+
};
|
|
238
|
+
// @ts-ignore
|
|
239
|
+
return JWK.thumbprint(jwk).then(function(kid) {
|
|
240
|
+
// @ts-ignore
|
|
241
|
+
return JWT.sign(jwk, { /*kid: kid*/ }, claims).then(function(jwt) {
|
|
242
|
+
// console.info('JWT:', jwt);
|
|
243
|
+
return jwt;
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export default EC;
|
package/src/login.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {generateRandom} from './crypto.js';
|
|
2
|
+
import {eventNames} from './events.js';
|
|
3
|
+
import {initSession} from './initSession.js';
|
|
4
|
+
import {initWorkerAsync} from './initWorker.js';
|
|
5
|
+
import {isTokensOidcValid} from './parseTokens.js';
|
|
6
|
+
import {
|
|
7
|
+
performAuthorizationRequestAsync,
|
|
8
|
+
performFirstTokenRequestAsync
|
|
9
|
+
} from './requests.js';
|
|
10
|
+
import {getParseQueryStringFromLocation} from './route-utils.js';
|
|
11
|
+
import {OidcConfiguration, StringMap} from './types.js';
|
|
12
|
+
import {generateJwkAsync, generateJwtDemonstratingProofOfPossessionAsync} from "./jwt";
|
|
9
13
|
|
|
10
14
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
11
|
-
export const defaultLoginAsync = (window, configurationName, configuration:OidcConfiguration, publishEvent :(string, any)=>void, initAsync:Function) => (callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined) => {
|
|
15
|
+
export const defaultLoginAsync = (window, configurationName:string, configuration:OidcConfiguration, publishEvent :(string, any)=>void, initAsync:Function) => (callbackPath:string = undefined, extras:StringMap = null, isSilentSignin = false, scope:string = undefined) => {
|
|
12
16
|
const originExtras = extras;
|
|
13
17
|
extras = { ...extras };
|
|
14
18
|
const loginLocalAsync = async () => {
|
|
@@ -42,14 +46,14 @@ export const defaultLoginAsync = (window, configurationName, configuration:OidcC
|
|
|
42
46
|
const oidcServerConfiguration = await initAsync(configuration.authority, configuration.authority_configuration);
|
|
43
47
|
let storage;
|
|
44
48
|
if (serviceWorker) {
|
|
45
|
-
serviceWorker.setLoginParams(
|
|
49
|
+
serviceWorker.setLoginParams({ callbackPath: url, extras: originExtras });
|
|
46
50
|
await serviceWorker.initAsync(oidcServerConfiguration, 'loginAsync', configuration);
|
|
47
51
|
await serviceWorker.setNonceAsync(nonce);
|
|
48
52
|
serviceWorker.startKeepAliveServiceWorker();
|
|
49
53
|
storage = serviceWorker;
|
|
50
54
|
} else {
|
|
51
55
|
const session = initSession(configurationName, configuration.storage ?? sessionStorage);
|
|
52
|
-
session.setLoginParams(
|
|
56
|
+
session.setLoginParams({ callbackPath: url, extras: originExtras });
|
|
53
57
|
await session.setNonceAsync(nonce);
|
|
54
58
|
storage = session;
|
|
55
59
|
}
|
|
@@ -91,7 +95,7 @@ export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => {
|
|
|
91
95
|
await serviceWorker.initAsync(oidcServerConfiguration, 'loginCallbackAsync', configuration);
|
|
92
96
|
await serviceWorker.setSessionStateAsync(sessionState);
|
|
93
97
|
nonceData = await serviceWorker.getNonceAsync();
|
|
94
|
-
getLoginParams = serviceWorker.getLoginParams(
|
|
98
|
+
getLoginParams = serviceWorker.getLoginParams();
|
|
95
99
|
state = await serviceWorker.getStateAsync();
|
|
96
100
|
serviceWorker.startKeepAliveServiceWorker();
|
|
97
101
|
storage = serviceWorker;
|
|
@@ -99,7 +103,7 @@ export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => {
|
|
|
99
103
|
const session = initSession(oidc.configurationName, configuration.storage ?? sessionStorage);
|
|
100
104
|
await session.setSessionStateAsync(sessionState);
|
|
101
105
|
nonceData = await session.getNonceAsync();
|
|
102
|
-
getLoginParams = session.getLoginParams(
|
|
106
|
+
getLoginParams = session.getLoginParams();
|
|
103
107
|
state = await session.getStateAsync();
|
|
104
108
|
storage = session;
|
|
105
109
|
}
|
|
@@ -135,8 +139,25 @@ export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => {
|
|
|
135
139
|
}
|
|
136
140
|
}
|
|
137
141
|
}
|
|
142
|
+
|
|
143
|
+
const url = oidcServerConfiguration.tokenEndpoint;
|
|
144
|
+
const headersExtras = {};
|
|
145
|
+
if(configuration.demonstrating_proof_of_possession) {
|
|
146
|
+
const jwk = await generateJwkAsync();
|
|
147
|
+
if (serviceWorker) {
|
|
148
|
+
await serviceWorker.setDemonstratingProofOfPossessionJwkAsync(jwk);
|
|
149
|
+
} else {
|
|
150
|
+
const session = initSession(oidc.configurationName, configuration.storage);
|
|
151
|
+
await session.setDemonstratingProofOfPossessionJwkAsync(jwk);
|
|
152
|
+
}
|
|
153
|
+
headersExtras['DPoP'] = await generateJwtDemonstratingProofOfPossessionAsync(jwk, 'POST', url);
|
|
154
|
+
}
|
|
138
155
|
|
|
139
|
-
const tokenResponse = await performFirstTokenRequestAsync(storage)(
|
|
156
|
+
const tokenResponse = await performFirstTokenRequestAsync(storage)(url,
|
|
157
|
+
{ ...data, ...extras },
|
|
158
|
+
headersExtras,
|
|
159
|
+
oidc.configuration.token_renew_mode,
|
|
160
|
+
tokenRequestTimeout);
|
|
140
161
|
|
|
141
162
|
if (!tokenResponse.success) {
|
|
142
163
|
throw new Error('Token request failed');
|
|
@@ -144,13 +165,8 @@ export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => {
|
|
|
144
165
|
|
|
145
166
|
let loginParams;
|
|
146
167
|
const formattedTokens = tokenResponse.data.tokens;
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
loginParams = serviceWorker.getLoginParams(oidc.configurationName);
|
|
150
|
-
} else {
|
|
151
|
-
const session = initSession(oidc.configurationName, configuration.storage);
|
|
152
|
-
loginParams = session.getLoginParams(oidc.configurationName);
|
|
153
|
-
}
|
|
168
|
+
const demonstratingProofOfPossessionNonce = tokenResponse.data.demonstratingProofOfPossessionNonce;
|
|
169
|
+
|
|
154
170
|
// @ts-ignore
|
|
155
171
|
if (tokenResponse.data.state !== extras.state) {
|
|
156
172
|
throw new Error('state is not valid');
|
|
@@ -159,6 +175,30 @@ export const loginCallbackAsync = (oidc) => async (isSilentSignin = false) => {
|
|
|
159
175
|
if (!isValid) {
|
|
160
176
|
throw new Error(`Tokens are not OpenID valid, reason: ${reason}`);
|
|
161
177
|
}
|
|
178
|
+
|
|
179
|
+
if(serviceWorker){
|
|
180
|
+
if(formattedTokens.refreshToken && !formattedTokens.refreshToken.includes("SECURED_BY_OIDC_SERVICE_WORKER")) {
|
|
181
|
+
throw new Error("Refresh token should be hidden by service worker");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if(demonstratingProofOfPossessionNonce && formattedTokens.accessToken && formattedTokens.accessToken.includes("SECURED_BY_OIDC_SERVICE_WORKER")) {
|
|
185
|
+
throw new Error("Demonstration of proof of possession require Access token not hidden by service worker");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (serviceWorker) {
|
|
190
|
+
await serviceWorker.initAsync(redirectUri, 'syncTokensAsync', configuration);
|
|
191
|
+
loginParams = serviceWorker.getLoginParams();
|
|
192
|
+
if(demonstratingProofOfPossessionNonce) {
|
|
193
|
+
await serviceWorker.setDemonstratingProofOfPossessionNonce(demonstratingProofOfPossessionNonce);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
const session = initSession(oidc.configurationName, configuration.storage);
|
|
197
|
+
loginParams = session.getLoginParams();
|
|
198
|
+
if(demonstratingProofOfPossessionNonce) {
|
|
199
|
+
await session.setDemonstratingProofOfPossessionNonce(demonstratingProofOfPossessionNonce);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
162
202
|
|
|
163
203
|
await oidc.startCheckSessionAsync(oidcServerConfiguration.checkSessionIframe, clientId, sessionState, isSilentSignin);
|
|
164
204
|
oidc.publishEvent(eventNames.loginCallbackAsync_end, {});
|
package/src/oidc.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from './parseTokens.js';
|
|
14
|
-
import { autoRenewTokens, renewTokensAndStartTimerAsync } from './renewTokens.js';
|
|
15
|
-
import { fetchFromIssuer, performTokenRequestAsync } from './requests.js';
|
|
16
|
-
import { getParseQueryStringFromLocation } from './route-utils.js';
|
|
17
|
-
import defaultSilentLoginAsync, { _silentLoginAsync } from './silentLogin.js';
|
|
1
|
+
import {startCheckSessionAsync as defaultStartCheckSessionAsync} from './checkSession.js';
|
|
2
|
+
import {CheckSessionIFrame} from './checkSessionIFrame.js';
|
|
3
|
+
import {eventNames} from './events.js';
|
|
4
|
+
import {initSession} from './initSession.js';
|
|
5
|
+
import {initWorkerAsync, sleepAsync} from './initWorker.js';
|
|
6
|
+
import {defaultLoginAsync, loginCallbackAsync} from './login.js';
|
|
7
|
+
import {destroyAsync, logoutAsync} from './logout.js';
|
|
8
|
+
import {computeTimeLeft, isTokensOidcValid, setTokens, TokenRenewMode, Tokens,} from './parseTokens.js';
|
|
9
|
+
import {autoRenewTokens, renewTokensAndStartTimerAsync} from './renewTokens.js';
|
|
10
|
+
import {fetchFromIssuer, performTokenRequestAsync} from './requests.js';
|
|
11
|
+
import {getParseQueryStringFromLocation} from './route-utils.js';
|
|
12
|
+
import defaultSilentLoginAsync, {_silentLoginAsync} from './silentLogin.js';
|
|
18
13
|
import timer from './timer.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
14
|
+
import {AuthorityConfiguration, Fetch, OidcConfiguration, StringMap} from './types.js';
|
|
15
|
+
import {userInfoAsync} from './user.js';
|
|
16
|
+
import {base64urlOfHashOfASCIIEncodingAsync} from "./crypto";
|
|
17
|
+
import {generateJwtDemonstratingProofOfPossessionAsync} from "./jwt";
|
|
21
18
|
|
|
22
19
|
export const getFetchDefault = () => {
|
|
23
20
|
return fetch;
|
|
@@ -93,12 +90,6 @@ export class Oidc {
|
|
|
93
90
|
if (refresh_time_before_tokens_expiration_in_second > 60) {
|
|
94
91
|
refresh_time_before_tokens_expiration_in_second = refresh_time_before_tokens_expiration_in_second - Math.floor(Math.random() * 40);
|
|
95
92
|
}
|
|
96
|
-
if (!configuration.logout_tokens_to_invalidate) {
|
|
97
|
-
configuration.logout_tokens_to_invalidate = ['access_token', 'refresh_token'];
|
|
98
|
-
}
|
|
99
|
-
if (!configuration.authority_timeout_wellknowurl_in_millisecond) {
|
|
100
|
-
configuration.authority_timeout_wellknowurl_in_millisecond = 10000;
|
|
101
|
-
}
|
|
102
93
|
this.configuration = {
|
|
103
94
|
...configuration,
|
|
104
95
|
silent_login_uri,
|
|
@@ -106,6 +97,9 @@ export class Oidc {
|
|
|
106
97
|
refresh_time_before_tokens_expiration_in_second,
|
|
107
98
|
silent_login_timeout: configuration.silent_login_timeout ?? 12000,
|
|
108
99
|
token_renew_mode: configuration.token_renew_mode ?? TokenRenewMode.access_token_or_id_token_invalid,
|
|
100
|
+
demonstrating_proof_of_possession: configuration.demonstrating_proof_of_possession ?? false,
|
|
101
|
+
authority_timeout_wellknowurl_in_millisecond: configuration.authority_timeout_wellknowurl_in_millisecond ?? 10000,
|
|
102
|
+
logout_tokens_to_invalidate: configuration.logout_tokens_to_invalidate ?? ['access_token', 'refresh_token'],
|
|
109
103
|
};
|
|
110
104
|
this.getFetch = getFetch ?? getFetchDefault;
|
|
111
105
|
this.configurationName = configurationName;
|
|
@@ -259,7 +253,7 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
259
253
|
if (tokens) {
|
|
260
254
|
// @ts-ignore
|
|
261
255
|
this.tokens = setTokens(tokens, null, configuration.token_renew_mode);
|
|
262
|
-
const getLoginParams = session.getLoginParams(
|
|
256
|
+
const getLoginParams = session.getLoginParams();
|
|
263
257
|
// @ts-ignore
|
|
264
258
|
this.timeoutId = autoRenewTokens(this, tokens.refreshToken, this.tokens.expiresAt, getLoginParams.extras);
|
|
265
259
|
const sessionState = await session.getSessionStateAsync();
|
|
@@ -373,10 +367,10 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
373
367
|
let loginParams;
|
|
374
368
|
const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
|
|
375
369
|
if (serviceWorker) {
|
|
376
|
-
loginParams = serviceWorker.getLoginParams(
|
|
370
|
+
loginParams = serviceWorker.getLoginParams();
|
|
377
371
|
} else {
|
|
378
372
|
const session = initSession(this.configurationName, configuration.storage);
|
|
379
|
-
loginParams = session.getLoginParams(
|
|
373
|
+
loginParams = session.getLoginParams();
|
|
380
374
|
}
|
|
381
375
|
const silent_token_response = await silentLoginAsync({
|
|
382
376
|
...loginParams.extras,
|
|
@@ -456,7 +450,19 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
456
450
|
};
|
|
457
451
|
const oidcServerConfiguration = await this.initAsync(authority, configuration.authority_configuration);
|
|
458
452
|
const timeoutMs = document.hidden ? 10000 : 30000 * 10;
|
|
459
|
-
const
|
|
453
|
+
const url = oidcServerConfiguration.tokenEndpoint;
|
|
454
|
+
const headersExtras = {};
|
|
455
|
+
if(configuration.demonstrating_proof_of_possession) {
|
|
456
|
+
headersExtras['DPoP'] = await this.generateDemonstrationOfProofOfPossessionAsync(tokens.accessToken, url, 'POST');
|
|
457
|
+
}
|
|
458
|
+
const tokenResponse = await performTokenRequestAsync(this.getFetch())(url,
|
|
459
|
+
details,
|
|
460
|
+
finalExtras,
|
|
461
|
+
tokens,
|
|
462
|
+
headersExtras,
|
|
463
|
+
configuration.token_renew_mode,
|
|
464
|
+
timeoutMs);
|
|
465
|
+
|
|
460
466
|
if (tokenResponse.success) {
|
|
461
467
|
const { isValid, reason } = isTokensOidcValid(tokenResponse.data, nonce.nonce, oidcServerConfiguration);
|
|
462
468
|
if (!isValid) {
|
|
@@ -465,6 +471,15 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
465
471
|
return { tokens: null, status: 'SESSION_LOST' };
|
|
466
472
|
}
|
|
467
473
|
updateTokens(tokenResponse.data);
|
|
474
|
+
if(tokenResponse.demonstratingProofOfPossessionNonce) {
|
|
475
|
+
const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
|
|
476
|
+
if(serviceWorker){
|
|
477
|
+
await serviceWorker.setDemonstratingProofOfPossessionNonce(tokenResponse.demonstratingProofOfPossessionNonce);
|
|
478
|
+
} else {
|
|
479
|
+
const session = initSession(this.configurationName, configuration.storage);
|
|
480
|
+
await session.setDemonstratingProofOfPossessionNonce(tokenResponse.demonstratingProofOfPossessionNonce);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
468
483
|
this.publishEvent(eventNames.refreshTokensAsync_end, { success: tokenResponse.success });
|
|
469
484
|
this.publishEvent(Oidc.eventNames.token_renewed, { reason: 'REFRESH_TOKEN' });
|
|
470
485
|
return { tokens: tokenResponse.data, status: 'LOGGED_IN' };
|
|
@@ -486,6 +501,30 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
486
501
|
}
|
|
487
502
|
}
|
|
488
503
|
|
|
504
|
+
async generateDemonstrationOfProofOfPossessionAsync(accessToken:string, url:string, method:string): Promise<string> {
|
|
505
|
+
|
|
506
|
+
const configuration = this.configuration;
|
|
507
|
+
const claimsExtras = {ath: await base64urlOfHashOfASCIIEncodingAsync(accessToken),};
|
|
508
|
+
|
|
509
|
+
const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
|
|
510
|
+
let demonstratingProofOfPossessionNonce:string = null;
|
|
511
|
+
let jwk;
|
|
512
|
+
if (serviceWorker) {
|
|
513
|
+
demonstratingProofOfPossessionNonce = await serviceWorker.getDemonstratingProofOfPossessionNonce();
|
|
514
|
+
jwk = await serviceWorker.getDemonstratingProofOfPossessionJwkAsync();
|
|
515
|
+
} else {
|
|
516
|
+
const session = initSession(this.configurationName, configuration.storage);
|
|
517
|
+
jwk = await session.getDemonstratingProofOfPossessionJwkAsync();
|
|
518
|
+
demonstratingProofOfPossessionNonce = await session.getDemonstratingProofOfPossessionNonce();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (demonstratingProofOfPossessionNonce) {
|
|
522
|
+
claimsExtras['nonce'] = demonstratingProofOfPossessionNonce;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return await generateJwtDemonstratingProofOfPossessionAsync(jwk, method, url, claimsExtras);
|
|
526
|
+
}
|
|
527
|
+
|
|
489
528
|
async syncTokensInfoAsync(configuration, configurationName, currentTokens, forceRefresh = false) {
|
|
490
529
|
// Service Worker can be killed by the browser (when it wants,for example after 10 seconds of inactivity, so we retreieve the session if it happen)
|
|
491
530
|
// const configuration = this.configuration;
|
package/src/oidcClient.ts
CHANGED
|
@@ -65,6 +65,10 @@ export class OidcClient {
|
|
|
65
65
|
return this._oidc.configuration;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
async generateDemonstrationOfProofOfPossessionAsync(accessToken:string, url:string, method:string) : Promise<string> {
|
|
69
|
+
return this._oidc.generateDemonstrationOfProofOfPossessionAsync(accessToken, url, method);
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
async getValidTokenAsync(waitMs = 200, numberWait = 50): Promise<ValidToken> {
|
|
69
73
|
return getValidTokenAsync(this._oidc, waitMs, numberWait);
|
|
70
74
|
}
|
package/src/requests.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { deriveChallengeAsync, generateRandom } from './crypto.js';
|
|
|
3
3
|
import { OidcAuthorizationServiceConfiguration } from './oidc.js';
|
|
4
4
|
import { parseOriginalTokens } from './parseTokens.js';
|
|
5
5
|
import { Fetch, StringMap } from './types.js';
|
|
6
|
+
import EC, {JWK, JWT} from './jwt';
|
|
6
7
|
|
|
7
8
|
const oneHourSecond = 60 * 60;
|
|
8
9
|
export const fetchFromIssuer = (fetch) => async (openIdIssuerUrl: string, timeCacheSecond = oneHourSecond, storage = window.sessionStorage, timeoutMs = 10000):
|
|
@@ -83,7 +84,21 @@ export const performRevocationRequestAsync = (fetch) => async (url, token, token
|
|
|
83
84
|
};
|
|
84
85
|
};
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
type PerformTokenRequestResponse = {
|
|
89
|
+
success: boolean;
|
|
90
|
+
status?: number;
|
|
91
|
+
data?: any;
|
|
92
|
+
demonstratingProofOfPossessionNonce?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const performTokenRequestAsync = (fetch:Fetch) => async (url:string,
|
|
96
|
+
details,
|
|
97
|
+
extras,
|
|
98
|
+
oldTokens,
|
|
99
|
+
headersExtras = {},
|
|
100
|
+
tokenRenewMode: string,
|
|
101
|
+
timeoutMs = 10000):Promise<PerformTokenRequestResponse> => {
|
|
87
102
|
for (const [key, value] of Object.entries(extras)) {
|
|
88
103
|
if (details[key] === undefined) {
|
|
89
104
|
details[key] = value;
|
|
@@ -97,21 +112,28 @@ export const performTokenRequestAsync = (fetch:Fetch) => async (url, details, ex
|
|
|
97
112
|
formBody.push(`${encodedKey}=${encodedValue}`);
|
|
98
113
|
}
|
|
99
114
|
const formBodyString = formBody.join('&');
|
|
100
|
-
|
|
115
|
+
|
|
101
116
|
const response = await internalFetch(fetch)(url, {
|
|
102
117
|
method: 'POST',
|
|
103
118
|
headers: {
|
|
104
119
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
120
|
+
...headersExtras
|
|
105
121
|
},
|
|
106
122
|
body: formBodyString,
|
|
107
123
|
}, timeoutMs);
|
|
108
124
|
if (response.status !== 200) {
|
|
109
|
-
return { success: false, status: response.status };
|
|
125
|
+
return { success: false, status: response.status, demonstratingProofOfPossessionNonce:null };
|
|
110
126
|
}
|
|
111
127
|
const tokens = await response.json();
|
|
128
|
+
|
|
129
|
+
let demonstratingProofOfPossessionNonce = null;
|
|
130
|
+
if( response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)){
|
|
131
|
+
demonstratingProofOfPossessionNonce = response.headers.get(demonstratingProofOfPossessionNonceResponseHeader);
|
|
132
|
+
}
|
|
112
133
|
return {
|
|
113
134
|
success: true,
|
|
114
135
|
data: parseOriginalTokens(tokens, oldTokens, tokenRenewMode),
|
|
136
|
+
demonstratingProofOfPossessionNonce: demonstratingProofOfPossessionNonce,
|
|
115
137
|
};
|
|
116
138
|
};
|
|
117
139
|
|
|
@@ -137,13 +159,18 @@ export const performAuthorizationRequestAsync = (storage: any) => async (url, ex
|
|
|
137
159
|
window.location.href = `${url}${queryString}`;
|
|
138
160
|
};
|
|
139
161
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
162
|
+
const demonstratingProofOfPossessionNonceResponseHeader = "DPoP-Nonce";
|
|
163
|
+
export const performFirstTokenRequestAsync = (storage:any) => async (url,
|
|
164
|
+
formBodyExtras,
|
|
165
|
+
headersExtras,
|
|
166
|
+
tokenRenewMode: string,
|
|
167
|
+
timeoutMs = 10000) => {
|
|
168
|
+
formBodyExtras = formBodyExtras ? { ...formBodyExtras } : {};
|
|
169
|
+
formBodyExtras.code_verifier = await storage.getCodeVerifierAsync();
|
|
143
170
|
const formBody = [];
|
|
144
|
-
for (const property in
|
|
171
|
+
for (const property in formBodyExtras) {
|
|
145
172
|
const encodedKey = encodeURIComponent(property);
|
|
146
|
-
const encodedValue = encodeURIComponent(
|
|
173
|
+
const encodedValue = encodeURIComponent(formBodyExtras[property]);
|
|
147
174
|
formBody.push(`${encodedKey}=${encodedValue}`);
|
|
148
175
|
}
|
|
149
176
|
const formBodyString = formBody.join('&');
|
|
@@ -151,6 +178,7 @@ export const performFirstTokenRequestAsync = (storage:any) => async (url, extras
|
|
|
151
178
|
method: 'POST',
|
|
152
179
|
headers: {
|
|
153
180
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
181
|
+
...headersExtras,
|
|
154
182
|
},
|
|
155
183
|
body: formBodyString,
|
|
156
184
|
}, timeoutMs);
|
|
@@ -158,12 +186,17 @@ export const performFirstTokenRequestAsync = (storage:any) => async (url, extras
|
|
|
158
186
|
if (response.status !== 200) {
|
|
159
187
|
return { success: false, status: response.status };
|
|
160
188
|
}
|
|
189
|
+
let demonstratingProofOfPossessionNonce:string= null;
|
|
190
|
+
if( response.headers.has(demonstratingProofOfPossessionNonceResponseHeader)){
|
|
191
|
+
demonstratingProofOfPossessionNonce = response.headers.get(demonstratingProofOfPossessionNonceResponseHeader);
|
|
192
|
+
}
|
|
161
193
|
const tokens = await response.json();
|
|
162
194
|
return {
|
|
163
195
|
success: true,
|
|
164
196
|
data: {
|
|
165
|
-
state:
|
|
197
|
+
state: formBodyExtras.state,
|
|
166
198
|
tokens: parseOriginalTokens(tokens, null, tokenRenewMode),
|
|
167
|
-
|
|
199
|
+
demonstratingProofOfPossessionNonce,
|
|
200
|
+
},
|
|
168
201
|
};
|
|
169
202
|
};
|
package/src/types.ts
CHANGED