@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/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 { 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 { performAuthorizationRequestAsync, performFirstTokenRequestAsync } from './requests.js';
7
- import { getParseQueryStringFromLocation } from './route-utils.js';
8
- import { OidcConfiguration, StringMap } from './types.js';
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(configurationName, { callbackPath: url, extras: originExtras });
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(configurationName, { callbackPath: url, extras: originExtras });
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(oidc.configurationName);
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(oidc.configurationName);
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)(oidcServerConfiguration.tokenEndpoint, { ...data, ...extras }, oidc.configuration.token_renew_mode, tokenRequestTimeout);
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
- if (serviceWorker) {
148
- await serviceWorker.initAsync(redirectUri, 'syncTokensAsync', configuration);
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 { 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 {
9
- computeTimeLeft,
10
- isTokensOidcValid,
11
- setTokens, TokenRenewMode,
12
- Tokens,
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 { AuthorityConfiguration, Fetch, OidcConfiguration, StringMap } from './types.js';
20
- import { userInfoAsync } from './user.js';
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(this.configurationName);
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(this.configurationName);
370
+ loginParams = serviceWorker.getLoginParams();
377
371
  } else {
378
372
  const session = initSession(this.configurationName, configuration.storage);
379
- loginParams = session.getLoginParams(this.configurationName);
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 tokenResponse = await performTokenRequestAsync(this.getFetch())(oidcServerConfiguration.tokenEndpoint, details, finalExtras, tokens, configuration.token_renew_mode, timeoutMs);
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
- export const performTokenRequestAsync = (fetch:Fetch) => async (url, details, extras, oldTokens, tokenRenewMode: string, timeoutMs = 10000) => {
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
- export const performFirstTokenRequestAsync = (storage:any) => async (url, extras, tokenRenewMode: string, timeoutMs = 10000) => {
141
- extras = extras ? { ...extras } : {};
142
- extras.code_verifier = await storage.getCodeVerifierAsync();
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 extras) {
171
+ for (const property in formBodyExtras) {
145
172
  const encodedKey = encodeURIComponent(property);
146
- const encodedValue = encodeURIComponent(extras[property]);
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: extras.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
@@ -27,6 +27,7 @@ export type OidcConfiguration = {
27
27
  monitor_session?: boolean;
28
28
  token_renew_mode?: string;
29
29
  logout_tokens_to_invalidate?:Array<LogoutToken>;
30
+ demonstrating_proof_of_possession?:boolean;
30
31
  };
31
32
 
32
33
  export interface StringMap {