From b23f0031c9bd77514c6377f1aa3a12232b100087 Mon Sep 17 00:00:00 2001 From: Arno V Date: Fri, 28 Jun 2024 11:00:00 -0400 Subject: [PATCH] feat: enabling access token renewal via refresh token (#55) * feat: enabling access token renewal via refresh token * Update bundlesize.config.js --- examples/code-flow/src/main.tsx | 18 ++++-- packages/auth-provider/bundlesize.config.js | 2 +- .../auth-provider/src/common/constants.ts | 2 + packages/auth-provider/src/common/types.d.ts | 10 +++- .../auth-provider/src/common/utilities.ts | 49 ++++++++++++++++ .../components/AuthProvider/AuthProvider.tsx | 58 +++++++++++++++---- 6 files changed, 121 insertions(+), 18 deletions(-) diff --git a/examples/code-flow/src/main.tsx b/examples/code-flow/src/main.tsx index 5c16614..d592e01 100644 --- a/examples/code-flow/src/main.tsx +++ b/examples/code-flow/src/main.tsx @@ -1,9 +1,10 @@ import { AUTH_TYPES, useAuth } from "@versini/auth-provider"; import { Button, Footer, Header, Main } from "@versini/ui-components"; import { Flexgrid, FlexgridItem } from "@versini/ui-system"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; export const App = ({ timeout }: { timeout: string }) => { + const accessTokenRef = useRef(""); const { login, logout, isAuthenticated, getAccessToken } = useAuth(); const [apiResponse, setApiResponse] = useState({ data: "" }); @@ -43,7 +44,7 @@ export const App = ({ timeout }: { timeout: string }) => { { headers: { "Content-Type": "application/json", - Authorization: `Bearer ${getAccessToken()}`, + Authorization: `Bearer ${await getAccessToken()}`, }, }, ); @@ -51,6 +52,15 @@ export const App = ({ timeout }: { timeout: string }) => { setApiResponse(data); }; + useEffect(() => { + if (isAuthenticated) { + (async () => { + accessTokenRef.current = await getAccessToken(); + })(); + } + accessTokenRef.current = ""; + }, [getAccessToken, isAuthenticated]); + return (
@@ -108,9 +118,7 @@ export const App = ({ timeout }: { timeout: string }) => {
{JSON.stringify(useAuth(), null, 2)}

Access Token

-
-					{JSON.stringify(useAuth().getAccessToken(), null, 2)}
-				
+
{accessTokenRef.current}
Timeout: {timeout}

} />
diff --git a/packages/auth-provider/bundlesize.config.js b/packages/auth-provider/bundlesize.config.js index e13837e..663cc57 100644 --- a/packages/auth-provider/bundlesize.config.js +++ b/packages/auth-provider/bundlesize.config.js @@ -9,7 +9,7 @@ export default { */ { path: "dist/index.js", - limit: "9 kb", + limit: "10 kb", }, ], }; diff --git a/packages/auth-provider/src/common/constants.ts b/packages/auth-provider/src/common/constants.ts index ddc4347..03ef232 100644 --- a/packages/auth-provider/src/common/constants.ts +++ b/packages/auth-provider/src/common/constants.ts @@ -2,6 +2,8 @@ export const EXPIRED_SESSION = "Oops! It looks like your session has expired. For your security, please log in again to continue."; export const LOGOUT_SESSION = "Your session has been successfully terminated."; export const LOGIN_ERROR = "Login failed. Please try again."; +export const ACCESS_TOKEN_ERROR = + "Error getting access token, please re-authenticate."; export const AUTH_CONTEXT_ERROR = "You forgot to wrap your component in ."; diff --git a/packages/auth-provider/src/common/types.d.ts b/packages/auth-provider/src/common/types.d.ts index fcf67e9..deb151c 100644 --- a/packages/auth-provider/src/common/types.d.ts +++ b/packages/auth-provider/src/common/types.d.ts @@ -18,9 +18,15 @@ export type AuthState = { userId?: string; }; +export type LoginType = ( + username: string, + password: string, + type?: string, +) => Promise; + export type AuthContextProps = { - login: (username: string, password: string) => Promise; + login: LoginType; logout: () => void; - getAccessToken: () => string; + getAccessToken: () => Promise; getIdToken: () => string; } & AuthState; diff --git a/packages/auth-provider/src/common/utilities.ts b/packages/auth-provider/src/common/utilities.ts index 926f47b..07c1317 100644 --- a/packages/auth-provider/src/common/utilities.ts +++ b/packages/auth-provider/src/common/utilities.ts @@ -167,3 +167,52 @@ export const getPreAuthCode = async ({ }; } }; + +export const getAccessTokenSilently = async ({ + clientId, + userId, + nonce, + refreshToken, + accessToken, +}: { + clientId: string; + userId: string; + nonce: string; + refreshToken: string; + accessToken: string; +}) => { + try { + const response = await serviceCall({ + type: API_TYPE.AUTHENTICATE, + clientId, + params: { + type: AUTH_TYPES.REFRESH_TOKEN, + userId, + nonce, + refreshToken, + accessToken, + }, + }); + const jwt = await verifyAndExtractToken(response.data.accessToken); + if ( + jwt && + jwt.payload[JWT.USER_ID_KEY] !== "" && + jwt.payload[JWT.NONCE_KEY] === nonce + ) { + return { + accessToken: response.data.accessToken, + refreshToken: response.data.refreshToken, + userId: jwt.payload[JWT.USER_ID_KEY] as string, + status: true, + }; + } else { + return { + status: false, + }; + } + } catch (_error) { + return { + status: false, + }; + } +}; diff --git a/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx b/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx index 25db92d..c9f8584 100644 --- a/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx +++ b/packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx @@ -9,14 +9,20 @@ import { useCallback, useEffect, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import { + ACCESS_TOKEN_ERROR, EXPIRED_SESSION, LOCAL_STORAGE_PREFIX, LOGIN_ERROR, LOGOUT_SESSION, } from "../../common/constants"; -import type { AuthProviderProps, AuthState } from "../../common/types"; +import type { + AuthProviderProps, + AuthState, + LoginType, +} from "../../common/types"; import { authenticateUser, + getAccessTokenSilently, getPreAuthCode, logoutUser, } from "../../common/utilities"; @@ -38,7 +44,7 @@ export const AuthProvider = ({ key: `${LOCAL_STORAGE_PREFIX}::${clientId}::@@refresh@@`, }, ); - const [, setNonce, , removeNonce] = useLocalStorage({ + const [nonce, setNonce, , removeNonce] = useLocalStorage({ key: `${LOCAL_STORAGE_PREFIX}::${clientId}::@@nonce@@`, }); @@ -111,11 +117,7 @@ export const AuthProvider = ({ removeStateAndLocalStorage, ]); - const login = async ( - username: string, - password: string, - type?: string, - ): Promise => { + const login: LoginType = async (username, password, type) => { const _nonce = uuidv4(); setNonce(_nonce); @@ -190,9 +192,45 @@ export const AuthProvider = ({ }); }; - const getAccessToken = () => { - if (authState.isAuthenticated && accessToken) { - return accessToken; + const getAccessToken = async () => { + const { isAuthenticated, userId } = authState; + try { + if (isAuthenticated && userId && accessToken) { + const jwtAccess = await verifyAndExtractToken(accessToken); + if (jwtAccess && jwtAccess.payload[JWT.USER_ID_KEY] !== "") { + return accessToken; + } + /** + * accessToken is not valid, so we need to refresh it using the + * refreshToken - this is a silent refresh. + */ + const jwtRefresh = await verifyAndExtractToken(refreshToken); + if (jwtRefresh && jwtRefresh.payload[JWT.USER_ID_KEY] !== "") { + const response = await getAccessTokenSilently({ + clientId, + userId, + nonce, + refreshToken, + accessToken, + }); + if (response.status) { + setAccessToken(response.accessToken); + setRefreshToken(response.refreshToken); + return response.accessToken; + } + removeStateAndLocalStorage(ACCESS_TOKEN_ERROR); + } + /** + * refreshToken is not valid, so we need to re-authenticate the user. + */ + removeStateAndLocalStorage(ACCESS_TOKEN_ERROR); + console.error(ACCESS_TOKEN_ERROR); + return ""; + } + } catch (_error) { + removeStateAndLocalStorage(ACCESS_TOKEN_ERROR); + console.error(ACCESS_TOKEN_ERROR); + return ""; } };