Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui): GEN-1690 sso token refresh issue for OIDC #18668

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

import { isEmpty } from 'lodash';
import { UserManager, WebStorageStateStore } from 'oidc-client';
import { User, UserManager, WebStorageStateStore } from 'oidc-client';
import React, {
ComponentType,
forwardRef,
Expand Down Expand Up @@ -97,10 +97,22 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(

// Performs silent signIn and returns with IDToken
const signInSilently = async () => {
const user = await userManager.signinSilent();
// For OIDC token will be coming as silent-callback as an IFram hence not returning new token here
await userManager.signinSilent();
};

const handleSilentSignInSuccess = (user: User) => {
// On success update token in store and update axios interceptors
setOidcToken(user.id_token);
updateAxiosInterceptors();
};

const handleSilentSignInFailure = (error: unknown) => {
// eslint-disable-next-line no-console
console.error(error);

return user.id_token;
onLogoutSuccess();
history.push(ROUTES.SIGNIN);
};

useImperativeHandle(ref, () => ({
Expand Down Expand Up @@ -154,22 +166,11 @@ const OidcAuthenticator = forwardRef<AuthenticatorRef, Props>(
<Route
path={ROUTES.SILENT_CALLBACK}
render={() => (
<>
<Callback
userManager={userManager}
onError={(error) => {
// eslint-disable-next-line no-console
console.error(error);

onLogoutSuccess();
history.push(ROUTES.SIGNIN);
}}
onSuccess={(user) => {
setOidcToken(user.id_token);
updateAxiosInterceptors();
}}
/>
</>
<Callback
userManager={userManager}
onError={handleSilentSignInFailure}
onSuccess={handleSilentSignInSuccess}
/>
)}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ export type OidcUser = {
export interface AuthenticatorRef {
invokeLogin: () => void;
invokeLogout: () => void;
renewIdToken: () => Promise<string> | Promise<AccessTokenResponse>;
renewIdToken: () =>
| Promise<string>
| Promise<AccessTokenResponse>
| Promise<void>;
}

export enum JWT_PRINCIPAL_CLAIMS {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
isProtectedRoute,
prepareUserProfileFromClaims,
} from '../../../utils/AuthProvider.util';
import { getOidcToken } from '../../../utils/LocalStorageUtils';
import { getPathNameFromWindowLocation } from '../../../utils/RouterUtils';
import { escapeESReservedCharacters } from '../../../utils/StringsUtils';
import { showErrorToast, showInfoToast } from '../../../utils/ToastUtils';
Expand Down Expand Up @@ -131,7 +132,6 @@ export const AuthProvider = ({
setJwtPrincipalClaimsMapping,
removeRefreshToken,
removeOidcToken,
getOidcToken,
getRefreshToken,
isApplicationLoading,
setApplicationLoading,
Expand Down Expand Up @@ -265,39 +265,6 @@ export const AuthProvider = ({
}
};

/**
* Renew Id Token handler for all the SSOs.
* This method will be called when the id token is about to expire.
*/
const renewIdToken = async () => {
try {
if (!tokenService.current?.isTokenUpdateInProgress()) {
await tokenService.current?.refreshToken();
} else {
// wait for renewal to complete
const wait = new Promise((resolve) => {
setTimeout(() => {
return resolve(true);
}, 500);
});
await wait;

// should have updated token after renewal
return getOidcToken();
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
`Error while refreshing token: `,
(error as AxiosError).message
);

throw error;
}

return getOidcToken();
};

/**
* This method will try to signIn silently when token is about to expire
* if it's not succeed then it will proceed for logout
Expand All @@ -311,10 +278,10 @@ export const AuthProvider = ({
return;
}

try {
// Try to renew token
const newToken = await renewIdToken();

if (!tokenService.current?.isTokenUpdateInProgress()) {
// For OIDC we won't be getting newToken immediately hence not updating token here
const newToken = await tokenService.current?.refreshToken();
// Start expiry timer on successful silent signIn
if (newToken) {
// Start expiry timer on successful silent signIn
// eslint-disable-next-line @typescript-eslint/no-use-before-define
Expand All @@ -325,13 +292,9 @@ export const AuthProvider = ({
await getLoggedInUserDetails();
failedLoggedInUserRequest = null;
}
} else {
// reset user details if silent signIn fails
resetUserDetails(forceLogout);
} else if (forceLogout) {
resetUserDetails(true);
}
} catch (error) {
// reset user details if silent signIn fails
resetUserDetails(forceLogout);
}
};

Expand Down Expand Up @@ -611,9 +574,11 @@ export const AuthProvider = ({
} else {
// get the user details if token is present and route is not auth callback and saml callback
if (
![ROUTES.AUTH_CALLBACK, ROUTES.SAML_CALLBACK].includes(
location.pathname
)
![
ROUTES.AUTH_CALLBACK,
ROUTES.SAML_CALLBACK,
ROUTES.SILENT_CALLBACK,
].includes(location.pathname)
) {
getLoggedInUserDetails();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AxiosError } from 'axios';
import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { AccessTokenResponse } from '../../../rest/auth-API';
import { extractDetailsFromToken } from '../../AuthProvider.util';
import { getOidcToken } from '../../LocalStorageUtils';

class TokenService {
channel: BroadcastChannel;
renewToken: () => Promise<string> | Promise<AccessTokenResponse>;
renewToken: () =>
| Promise<string>
| Promise<AccessTokenResponse>
| Promise<void>;
chirag-madlani marked this conversation as resolved.
Show resolved Hide resolved
tokeUpdateInProgress: boolean;

constructor(
renewToken: () => Promise<string> | Promise<AccessTokenResponse>
renewToken: () =>
| Promise<string>
| Promise<AccessTokenResponse>
| Promise<void>
) {
this.channel = new BroadcastChannel('auth_channel');
this.renewToken = renewToken;
Expand Down Expand Up @@ -50,9 +57,10 @@ class TokenService {
// Refresh the token if it is expired
async refreshToken() {
const token = getOidcToken();
const { isExpired } = extractDetailsFromToken(token);
const { isExpired, timeoutExpiry } = extractDetailsFromToken(token);

if (isExpired) {
// If token is expired or timeoutExpiry is less than 0 then try to silent signIn
if (isExpired || timeoutExpiry <= 0) {
// Logic to refresh the token
const newToken = await this.fetchNewToken();
// To update all the tabs on updating channel token
Expand All @@ -66,14 +74,17 @@ class TokenService {

// Call renewal method according to the provider
async fetchNewToken() {
let response: string | AccessTokenResponse | null = null;
let response: string | AccessTokenResponse | null | void = null;
if (typeof this.renewToken === 'function') {
try {
this.tokeUpdateInProgress = true;
response = await this.renewToken();
this.tokeUpdateInProgress = false;
} catch (error) {
useApplicationStore.getState().onLogoutHandler();
// Silent Frame window timeout error since it doesn't affect refresh token process
if ((error as AxiosError).message !== 'Frame window timed out') {
// Perform logout for any error
useApplicationStore.getState().onLogoutHandler();
}
// Do nothing
} finally {
this.tokeUpdateInProgress = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import { isDev } from './EnvironmentUtils';

const cookieStorage = new CookieStorage();

// 1 minutes for client auth approach
export const EXPIRY_THRESHOLD_MILLES = 1 * 60 * 1000;
// 2 minutes for client auth approach
export const EXPIRY_THRESHOLD_MILLES = 2 * 60 * 1000;

const subPath = process.env.APP_SUB_PATH ?? '';

Expand Down Expand Up @@ -340,6 +340,7 @@ export const extractDetailsFromToken = (token: string) => {
return {
exp,
isExpired: false,
timeoutExpiry: 0,
};
}
const threshouldMillis = EXPIRY_THRESHOLD_MILLES;
Expand Down
Loading