@axa-fr/oidc-client 7.4.0 → 7.5.0
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 +809 -639
- package/dist/index.umd.cjs +2 -2
- package/dist/initSession.d.ts +6 -2
- package/dist/initWorker.d.ts +6 -2
- 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 +31 -9
- package/src/jwt.ts +248 -0
- package/src/login.ts +61 -21
- package/src/oidc.ts +68 -34
- package/src/oidcClient.ts +4 -0
- package/src/parseTokens.ts +24 -15
- 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();
|
|
@@ -329,11 +323,6 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
329
323
|
if (!serviceWorker) {
|
|
330
324
|
const session = initSession(this.configurationName, this.configuration.storage);
|
|
331
325
|
session.setTokens(parsedTokens);
|
|
332
|
-
} else {
|
|
333
|
-
// @ts-ignore
|
|
334
|
-
if(!parsedTokens.fromServiceWorker){
|
|
335
|
-
throw Error('security issue, parsedTokens.fromServiceWorker is not defined');
|
|
336
|
-
}
|
|
337
326
|
}
|
|
338
327
|
this.publishEvent(Oidc.eventNames.token_aquired, parsedTokens);
|
|
339
328
|
// @ts-ignore
|
|
@@ -378,10 +367,10 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
378
367
|
let loginParams;
|
|
379
368
|
const serviceWorker = await initWorkerAsync(configuration.service_worker_relative_url, this.configurationName);
|
|
380
369
|
if (serviceWorker) {
|
|
381
|
-
loginParams = serviceWorker.getLoginParams(
|
|
370
|
+
loginParams = serviceWorker.getLoginParams();
|
|
382
371
|
} else {
|
|
383
372
|
const session = initSession(this.configurationName, configuration.storage);
|
|
384
|
-
loginParams = session.getLoginParams(
|
|
373
|
+
loginParams = session.getLoginParams();
|
|
385
374
|
}
|
|
386
375
|
const silent_token_response = await silentLoginAsync({
|
|
387
376
|
...loginParams.extras,
|
|
@@ -461,7 +450,19 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
461
450
|
};
|
|
462
451
|
const oidcServerConfiguration = await this.initAsync(authority, configuration.authority_configuration);
|
|
463
452
|
const timeoutMs = document.hidden ? 10000 : 30000 * 10;
|
|
464
|
-
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
|
+
|
|
465
466
|
if (tokenResponse.success) {
|
|
466
467
|
const { isValid, reason } = isTokensOidcValid(tokenResponse.data, nonce.nonce, oidcServerConfiguration);
|
|
467
468
|
if (!isValid) {
|
|
@@ -470,6 +471,15 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
470
471
|
return { tokens: null, status: 'SESSION_LOST' };
|
|
471
472
|
}
|
|
472
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
|
+
}
|
|
473
483
|
this.publishEvent(eventNames.refreshTokensAsync_end, { success: tokenResponse.success });
|
|
474
484
|
this.publishEvent(Oidc.eventNames.token_renewed, { reason: 'REFRESH_TOKEN' });
|
|
475
485
|
return { tokens: tokenResponse.data, status: 'LOGGED_IN' };
|
|
@@ -491,6 +501,30 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
|
|
|
491
501
|
}
|
|
492
502
|
}
|
|
493
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
|
+
|
|
494
528
|
async syncTokensInfoAsync(configuration, configurationName, currentTokens, forceRefresh = false) {
|
|
495
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)
|
|
496
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/parseTokens.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {sleepAsync} from './initWorker.js';
|
|
2
2
|
|
|
3
3
|
const b64DecodeUnicode = (str) =>
|
|
4
4
|
decodeURIComponent(Array.prototype.map.call(atob(str), (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
|
|
@@ -46,20 +46,29 @@ export const TokenRenewMode = {
|
|
|
46
46
|
id_token_invalid: 'id_token_invalid',
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
function extractedIssueAt(tokens, accessTokenPayload, _idTokenPayload) {
|
|
50
|
+
if (!tokens.issuedAt) {
|
|
51
|
+
if (accessTokenPayload && accessTokenPayload.iat) {
|
|
52
|
+
return accessTokenPayload.iat;
|
|
53
|
+
} else if (_idTokenPayload && _idTokenPayload.iat) {
|
|
54
|
+
return _idTokenPayload.iat;
|
|
55
|
+
} else {
|
|
56
|
+
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
57
|
+
return currentTimeUnixSecond;
|
|
58
|
+
}
|
|
59
|
+
} else if (typeof tokens.issuedAt == "string") {
|
|
60
|
+
return parseInt(tokens.issuedAt, 10);
|
|
61
|
+
}
|
|
62
|
+
return tokens.issuedAt;
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export const setTokens = (tokens, oldTokens = null, tokenRenewMode: string):Tokens => {
|
|
50
66
|
if (!tokens) {
|
|
51
67
|
return null;
|
|
52
68
|
}
|
|
53
69
|
let accessTokenPayload;
|
|
54
70
|
const expireIn = typeof tokens.expiresIn == "string" ? parseInt(tokens.expiresIn, 10) : tokens.expiresIn;
|
|
55
|
-
|
|
56
|
-
if (!tokens.issuedAt) {
|
|
57
|
-
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
58
|
-
tokens.issuedAt = currentTimeUnixSecond;
|
|
59
|
-
} else if (typeof tokens.issuedAt == "string") {
|
|
60
|
-
tokens.issuedAt = parseInt(tokens.issuedAt, 10);
|
|
61
|
-
}
|
|
62
|
-
|
|
71
|
+
|
|
63
72
|
if (tokens.accessTokenPayload !== undefined) {
|
|
64
73
|
accessTokenPayload = tokens.accessTokenPayload;
|
|
65
74
|
} else {
|
|
@@ -70,6 +79,8 @@ export const setTokens = (tokens, oldTokens = null, tokenRenewMode: string):Toke
|
|
|
70
79
|
const idTokenExpireAt = (_idTokenPayload && _idTokenPayload.exp) ? _idTokenPayload.exp : Number.MAX_VALUE;
|
|
71
80
|
const accessTokenExpiresAt = (accessTokenPayload && accessTokenPayload.exp) ? accessTokenPayload.exp : tokens.issuedAt + expireIn;
|
|
72
81
|
|
|
82
|
+
tokens.issuedAt = extractedIssueAt(tokens, accessTokenPayload, _idTokenPayload);
|
|
83
|
+
|
|
73
84
|
let expiresAt;
|
|
74
85
|
if(tokens.expiresAt)
|
|
75
86
|
{
|
|
@@ -127,18 +138,16 @@ export const parseOriginalTokens = (tokens, oldTokens, tokenRenewMode: string) =
|
|
|
127
138
|
// @ts-ignore
|
|
128
139
|
data.idTokenPayload = tokens.idTokenPayload;
|
|
129
140
|
}
|
|
130
|
-
|
|
131
|
-
if(tokens.fromServiceWorker !== undefined) {
|
|
132
|
-
// @ts-ignore
|
|
133
|
-
data.fromServiceWorker = tokens.fromServiceWorker;
|
|
134
|
-
}
|
|
135
141
|
|
|
136
142
|
return setTokens(data, oldTokens, tokenRenewMode);
|
|
137
143
|
};
|
|
138
144
|
|
|
139
145
|
export const computeTimeLeft = (refreshTimeBeforeTokensExpirationInSecond, expiresAt) => {
|
|
140
146
|
const currentTimeUnixSecond = new Date().getTime() / 1000;
|
|
141
|
-
|
|
147
|
+
|
|
148
|
+
const timeLeftSecond = expiresAt - currentTimeUnixSecond;
|
|
149
|
+
|
|
150
|
+
return Math.round(timeLeftSecond - refreshTimeBeforeTokensExpirationInSecond);
|
|
142
151
|
};
|
|
143
152
|
|
|
144
153
|
export const isTokensValid = (tokens) => {
|