Skip to content

Commit

Permalink
feature(oidc): token renew only when required (release) (#1327)
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaume-chervet authored Mar 24, 2024
1 parent db67095 commit 9a3ad3a
Show file tree
Hide file tree
Showing 15 changed files with 83 additions and 57 deletions.
3 changes: 2 additions & 1 deletion examples/react-oidc-demo/src/configurations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TokenRenewMode } from '@axa-fr/react-oidc';
import { TokenRenewMode, TokenAutomaticRenewMode } from '@axa-fr/react-oidc';

export const configurationIdentityServer = {
client_id: 'interactive.public.short',
Expand All @@ -16,6 +16,7 @@ export const configurationIdentityServer = {
// monitor_session: true,
extras: { youhou_demo: 'youhou' },
token_renew_mode: TokenRenewMode.access_token_invalid,
token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted,
demonstrating_proof_of_possession: false,
};

Expand Down
6 changes: 5 additions & 1 deletion packages/oidc-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ oidcClient.tryKeepExistingSessionAsync().then(() => {
<h1>@axa-fr/oidc-client demo</h1>
<h2>Loading</h2>
</div>`;
return
return;
}

let tokens = oidcClient.tokens;
Expand Down Expand Up @@ -226,6 +226,10 @@ const configuration = {
authority_timeout_wellknowurl_in_millisecond: 10000, // Timeout in milliseconds of the openid well-known URL, default is 10 seconds, then an error is thrown
monitor_session: Boolean, // Add OpenID monitor session, default is false (more information https://openid.net/specs/openid-connect-session-1_0.html), if you need to set it to true consider https://infi.nl/nieuws/spa-necromancy/
token_renew_mode: String, // Optional, update tokens based on the selected token(s) lifetime: "access_token_or_id_token_invalid" (default), "access_token_invalid", "id_token_invalid"
token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted, // Optional, default is TokenAutomaticRenewMode.AutomaticBeforeTokensExpiration
// TokenAutomaticRenewMode.AutomaticBeforeTokensExpiration: renew tokens automatically before they expire
// TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted: renew tokens automatically only when fetch is executed
// It requires you to use fetch given by oidcClient.fetchWithTokens(fetch) or to use oidcClient.getValidTokenAsync()
logout_tokens_to_invalidate: Array<string>, // Optional tokens to invalidate during logout, default: ['access_token', 'refresh_token']
location: ILOidcLocation, // Optional, default is window.location, you can inject your own location object respecting the ILOidcLocation interface
demonstrating_proof_of_possession: Boolean, // Optional, default is false, if true, the the Demonstrating Proof of Possession will be activated //https://www.rfc-editor.org/rfc/rfc9449.html#name-protected-resource-access
Expand Down
1 change: 1 addition & 0 deletions packages/oidc-client/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export const eventNames = {
syncTokensAsync_lock_not_available: 'syncTokensAsync_lock_not_available',
syncTokensAsync_end: 'syncTokensAsync_end',
syncTokensAsync_error: 'syncTokensAsync_error',
tokensInvalidAndWaitingActionsToRefresh: 'tokensInvalidAndWaitingActionsToRefresh',
};
6 changes: 3 additions & 3 deletions packages/oidc-client/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Fetch} from "./types";
import {OidcClient} from "./oidcClient";
import {getValidTokenAsync} from "./parseTokens";

// @ts-ignore
export const fetchWithTokens = (fetch: Fetch, oidcClient: OidcClient | null) : Fetch => async (...params: Parameters<Fetch>) :Promise<Response> => {
export const fetchWithTokens = (fetch: Fetch, oidcClient: Oidc | null) : Fetch => async (...params: Parameters<Fetch>) :Promise<Response> => {
const [url, options, ...rest] = params;
const optionTmp = options ? { ...options } : { method: 'GET' };
let headers = new Headers();
Expand All @@ -14,9 +15,8 @@ export const fetchWithTokens = (fetch: Fetch, oidcClient: OidcClient | null) : F
const oidc = oidcClient;

// @ts-ignore
const getValidToken = await oidc.getValidTokenAsync();
const getValidToken = await getValidTokenAsync(oidc);
const accessToken = getValidToken?.tokens?.accessToken;

if (!headers.has('Accept')) {
headers.set('Accept', 'application/json');
}
Expand Down
9 changes: 7 additions & 2 deletions packages/oidc-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {ILOidcLocation} from "./location";

export { getFetchDefault } from './oidc.js';
export { TokenRenewMode } from './parseTokens.js';
export { getParseQueryStringFromLocation, getPath } from './route-utils';
export type {
AuthorityConfiguration,
Fetch,
OidcConfiguration,
StringMap,
StringMap
} from './types.js';
export { type ILOidcLocation, OidcLocation } from './location.js';

export { OidcLocation } from './location.js';
export type { ILOidcLocation } from './location.js';
export { TokenAutomaticRenewMode } from './types.js';
export { type OidcUserInfo, OidcClient } from './oidcClient.js';
13 changes: 6 additions & 7 deletions packages/oidc-client/src/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,16 @@ import {initSession} from './initSession.js';
import {defaultServiceWorkerUpdateRequireCallback, initWorkerAsync, sleepAsync} from './initWorker.js';
import {defaultLoginAsync, loginCallbackAsync} from './login.js';
import {destroyAsync, logoutAsync} from './logout.js';
import {isTokensOidcValid, TokenRenewMode, Tokens,} from './parseTokens.js';
import {TokenRenewMode, Tokens,} from './parseTokens.js';
import {
autoRenewTokens,
renewTokensAndStartTimerAsync,
synchroniseTokensStatus,
syncTokensInfoAsync
renewTokensAndStartTimerAsync
} from './renewTokens.js';
import {fetchFromIssuer, performTokenRequestAsync} from './requests.js';
import {fetchFromIssuer} from './requests.js';
import {getParseQueryStringFromLocation} from './route-utils.js';
import defaultSilentLoginAsync, {_silentLoginAsync} from './silentLogin.js';
import defaultSilentLoginAsync from './silentLogin.js';
import timer from './timer.js';
import {AuthorityConfiguration, Fetch, OidcConfiguration, StringMap} from './types.js';
import {AuthorityConfiguration, Fetch, OidcConfiguration, StringMap, TokenAutomaticRenewMode} from './types.js';
import {userInfoAsync} from './user.js';
import {base64urlOfHashOfASCIIEncodingAsync} from "./crypto";
import {
Expand Down Expand Up @@ -112,6 +110,7 @@ export class Oidc {
this.configuration = {
...configuration,
silent_login_uri,
token_automatic_renew_mode: configuration.token_automatic_renew_mode ?? TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration,
monitor_session: configuration.monitor_session ?? false,
refresh_time_before_tokens_expiration_in_second,
silent_login_timeout: configuration.silent_login_timeout ?? 12000,
Expand Down
6 changes: 6 additions & 0 deletions packages/oidc-client/src/parseTokens.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
setTokens,
TokenRenewMode
} from "./parseTokens";
import {StringMap, TokenAutomaticRenewMode} from "./types";
import {sleepAsync} from "./initWorker";

describe('ParseTokens test Suite', () => {
const currentTimeUnixSecond = new Date().getTime() / 1000;
Expand All @@ -26,6 +28,10 @@ describe('ParseTokens test Suite', () => {
expiresAt,
issuedAt,
},
configuration: { token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration},
renewTokensAsync: async (extras: StringMap) => {
await sleepAsync({milliseconds:10});
}
};
const result = await getValidTokenAsync(oidc, 1, 1);
expect(result.isTokensValid).toEqual(expectIsValidToken);
Expand Down
10 changes: 9 additions & 1 deletion packages/oidc-client/src/parseTokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {sleepAsync} from './initWorker.js';
import {OidcConfiguration, StringMap, TokenAutomaticRenewMode} from "./types";

const b64DecodeUnicode = (str) =>
decodeURIComponent(Array.prototype.map.call(atob(str), (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
Expand Down Expand Up @@ -174,6 +175,8 @@ export type ValidToken = {

export interface OidcToken{
tokens?: Tokens;
configuration: { token_automatic_renew_mode?: TokenAutomaticRenewMode; },
renewTokensAsync: (extras: StringMap) => Promise<void>;
}

export const getValidTokenAsync = async (oidc: OidcToken, waitMs = 200, numberWait = 50): Promise<ValidToken> => {
Expand All @@ -182,7 +185,12 @@ export const getValidTokenAsync = async (oidc: OidcToken, waitMs = 200, numberWa
return null;
}
while (!isTokensValid(oidc.tokens) && numberWaitTemp > 0) {
await sleepAsync({milliseconds: waitMs});
if(oidc.configuration.token_automatic_renew_mode == TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted){
await oidc.renewTokensAsync({});
break;
} else {
await sleepAsync({milliseconds: waitMs});
}
numberWaitTemp = numberWaitTemp - 1;
}
const isValid = isTokensValid(oidc.tokens);
Expand Down
33 changes: 22 additions & 11 deletions packages/oidc-client/src/renewTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {initWorkerAsync, sleepAsync} from './initWorker.js';
import Oidc from './oidc.js';
import {computeTimeLeft, isTokensOidcValid, setTokens, Tokens} from './parseTokens.js';
import timer from './timer.js';
import {OidcConfiguration, StringMap} from './types.js';
import {OidcConfiguration, StringMap, TokenAutomaticRenewMode} from './types.js';
import {_silentLoginAsync} from "./silentLogin";
import {performTokenRequestAsync} from "./requests";
import {eventNames} from "./events";
Expand Down Expand Up @@ -86,15 +86,16 @@ export const autoRenewTokens = (oidc:Oidc, expiresAt, extras:StringMap = null) =
};

export const synchroniseTokensStatus ={
'SESSION_LOST': 'SESSION_LOST',
'NOT_CONNECTED':'NOT_CONNECTED',
'TOKENS_VALID':'TOKENS_VALID',
'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID': 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID',
'LOGOUT_FROM_ANOTHER_TAB': 'LOGOUT_FROM_ANOTHER_TAB',
'REQUIRE_SYNC_TOKENS': 'REQUIRE_SYNC_TOKENS'
FORCE_REFRESH: 'FORCE_REFRESH',
SESSION_LOST: 'SESSION_LOST',
NOT_CONNECTED:'NOT_CONNECTED',
TOKENS_VALID:'TOKENS_VALID',
TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID: 'TOKEN_UPDATED_BY_ANOTHER_TAB_TOKENS_VALID',
LOGOUT_FROM_ANOTHER_TAB: 'LOGOUT_FROM_ANOTHER_TAB',
REQUIRE_SYNC_TOKENS: 'REQUIRE_SYNC_TOKENS'
};

export const syncTokensInfoAsync = (oidc: Oidc) => async (configuration:OidcConfiguration, configurationName: string, currentTokens, forceRefresh = false) => {
export const syncTokensInfoAsync = (oidc: Oidc) => async (configuration:OidcConfiguration, configurationName: string, currentTokens: Tokens, forceRefresh = false) => {
// 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)
// const configuration = this.configuration;
const nullNonce = { nonce: null };
Expand Down Expand Up @@ -147,8 +148,6 @@ export const syncTokensInfoAsync = (oidc: Oidc) => async (configuration:OidcConf
}




const synchroniseTokensAsync = (oidc:Oidc) => async (index = 0, forceRefresh = false, extras:StringMap = null, updateTokens) =>{

while (!navigator.onLine && document.hidden) {
Expand Down Expand Up @@ -210,14 +209,14 @@ const synchroniseTokensAsync = (oidc:Oidc) => async (index = 0, forceRefresh = f

if (index > 4) {
if(isDocumentHidden){
//oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token' });
return { tokens: oidc.tokens, status: 'GIVE_UP' };
} else{
updateTokens(null);
oidc.publishEvent(eventNames.refreshTokensAsync_error, { message: 'refresh token' });
return { tokens: null, status: 'SESSION_LOST' };
}
}

try {
const { status, tokens, nonce } = await syncTokensInfoAsync(oidc)(configuration, oidc.configurationName, oidc.tokens, forceRefresh);
switch (status) {
Expand All @@ -240,9 +239,21 @@ const synchroniseTokensAsync = (oidc:Oidc) => async (index = 0, forceRefresh = f
oidc.publishEvent(eventNames.logout_from_another_tab, { status: 'session syncTokensAsync' });
return { tokens: null, status: 'LOGGED_OUT' };
case synchroniseTokensStatus.REQUIRE_SYNC_TOKENS:

if(configuration.token_automatic_renew_mode == TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted && synchroniseTokensStatus.FORCE_REFRESH !== status ){
oidc.publishEvent(eventNames.tokensInvalidAndWaitingActionsToRefresh, {});
return { tokens: oidc.tokens, status: 'GIVE_UP' };
}

oidc.publishEvent(eventNames.refreshTokensAsync_begin, { tryNumber: index });
return await localsilentLoginAsync();
default: {

if(configuration.token_automatic_renew_mode == TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted && synchroniseTokensStatus.FORCE_REFRESH !== status ){
oidc.publishEvent(eventNames.tokensInvalidAndWaitingActionsToRefresh, {});
return { tokens: oidc.tokens, status: 'GIVE_UP' };
}

oidc.publishEvent(eventNames.refreshTokensAsync_begin, { refreshToken: tokens.refreshToken, status, tryNumber: index });
if (!tokens.refreshToken) {
return await localsilentLoginAsync();
Expand Down
6 changes: 6 additions & 0 deletions packages/oidc-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type ServiceWorkerUpdateRequireCallback = (registration:any, stopKeepAliv
export type ServiceWorkerRegister = (serviceWorkerRelativeUrl:string) => Promise<ServiceWorkerRegistration>;
export type ServiceWorkerActivate = () => boolean;

export enum TokenAutomaticRenewMode {
AutomaticBeforeTokenExpiration = 'AutomaticBeforeTokensExpiration',
AutomaticOnlyWhenFetchExecuted = 'AutomaticOnlyWhenFetchExecuted'
}

export type OidcConfiguration = {
client_id: string;
redirect_uri: string;
Expand All @@ -18,6 +23,7 @@ export type OidcConfiguration = {
authority_timeout_wellknowurl_in_millisecond?: number;
authority_configuration?: AuthorityConfiguration;
refresh_time_before_tokens_expiration_in_second?: number;
token_automatic_renew_mode?: TokenAutomaticRenewMode;
token_request_timeout?: number;
service_worker_relative_url?:string;
service_worker_register?:ServiceWorkerRegister;
Expand Down
34 changes: 7 additions & 27 deletions packages/oidc-client/src/user.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,22 @@
import { sleepAsync } from './initWorker.js';
import { isTokensValid } from './parseTokens.js';
import Oidc from "./oidc";
import {fetchWithTokens} from "./fetch";

export const userInfoAsync = (oidc:Oidc) => async (noCache = false) => {
if (oidc.userInfo != null && !noCache) {
return oidc.userInfo;
}

// We wait the synchronisation before making a request
while (oidc.tokens && !isTokensValid(oidc.tokens)) {
await sleepAsync({milliseconds: 200});
}

if (!oidc.tokens) {
return null;
}
const accessToken = oidc.tokens.accessToken;
if (!accessToken) {
return null;
}

const configuration = oidc.configuration;
const oidcServerConfiguration = await oidc.initAsync(configuration.authority, configuration.authority_configuration);
const url = oidcServerConfiguration.userInfoEndpoint;
const fetchUserInfo = async (accessToken) => {
const res = await fetch(url, {
headers: {
authorization: `Bearer ${accessToken}`,
},
});

if (res.status !== 200) {
const fetchUserInfo = async () => {
const oidcFetch = fetchWithTokens(fetch, oidc);
const response = await oidcFetch(url);
if (response.status !== 200) {
return null;
}

return res.json();
return response.json();
};
const userInfo = await fetchUserInfo(accessToken);
const userInfo = await fetchUserInfo();
oidc.userInfo = userInfo;
return userInfo;
};
4 changes: 4 additions & 0 deletions packages/react-oidc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ const configuration = {
onLogoutFromAnotherTab: Function, // Optional, can be set to override the default behavior, this function is triggered when a user with the same subject is logged out from another tab when session_monitor is active
onLogoutFromSameTab: Function, // Optional, can be set to override the default behavior, this function is triggered when a user is logged out from the same tab when session_monitor is active
token_renew_mode: String, // Optional, update tokens based on the selected token(s) lifetime: "access_token_or_id_token_invalid" (default), "access_token_invalid", "id_token_invalid"
token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted, // Optional, default is TokenAutomaticRenewMode.AutomaticBeforeTokensExpiration
// TokenAutomaticRenewMode.AutomaticBeforeTokensExpiration: renew tokens automatically before they expire
// TokenAutomaticRenewMode.AutomaticOnlyWhenFetchExecuted: renew tokens automatically only when fetch is executed
// It requires you to use fetch given by hook useOidcFetch(fetch) or HOC withOidcFetch(fetch)(Component)
logout_tokens_to_invalidate: Array<string>, // Optional tokens to invalidate during logout, default: ['access_token', 'refresh_token']
location: ILOidcLocation, // Optional, default is window.location, you can inject your own location object respecting the ILOidcLocation interface
demonstrating_proof_of_possession: Boolean, // Optional, default is false, if true, the the Demonstrating Proof of Possession will be activated //https://www.rfc-editor.org/rfc/rfc9449.html#name-protected-resource-access
Expand Down
3 changes: 2 additions & 1 deletion packages/react-oidc/src/ReactOidc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,12 @@ export const useOidcAccessToken = (configurationName = defaultConfigurationName)
return state;
};

const idTokenInitialState = { idToken: null, idTokenPayload: null };
const idTokenInitialState = { idToken: null, idTokenPayload: null};

const initIdToken = (configurationName: string) => {
const getOidc = OidcClient.get;
const oidc = getOidc(configurationName);

if (oidc.tokens) {
const tokens = oidc.tokens;
return { idToken: tokens.idToken, idTokenPayload: tokens.idTokenPayload };
Expand Down
3 changes: 1 addition & 2 deletions packages/react-oidc/src/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export type OidcUser<T extends OidcUserInfo = OidcUserInfo> = {
export const useOidcUser = <T extends OidcUserInfo = OidcUserInfo>(configurationName = 'default') => {
const [oidcUser, setOidcUser] = useState<OidcUser<T>>({ user: null, status: OidcUserStatus.Unauthenticated });
const [oidcUserId, setOidcUserId] = useState<string>('');



useEffect(() => {
const oidc = OidcClient.get(configurationName);
let isMounted = true;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-oidc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export type {
Fetch,
OidcConfiguration,
StringMap,
ILOidcLocation
} from '@axa-fr/oidc-client';
export { type OidcUserInfo, TokenRenewMode, OidcClient } from '@axa-fr/oidc-client';
export { type OidcUserInfo, TokenRenewMode, OidcClient, TokenAutomaticRenewMode, OidcLocation } from '@axa-fr/oidc-client';

0 comments on commit 9a3ad3a

Please sign in to comment.