Skip to content

Commit

Permalink
feat: enabling access token renewal via refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
aversini committed Jun 28, 2024
1 parent 48c8a3b commit a4a90e1
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 17 deletions.
18 changes: 13 additions & 5 deletions examples/code-flow/src/main.tsx
Original file line number Diff line number Diff line change
@@ -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: "" });

Expand Down Expand Up @@ -43,14 +44,23 @@ export const App = ({ timeout }: { timeout: string }) => {
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getAccessToken()}`,
Authorization: `Bearer ${await getAccessToken()}`,
},
},
);
const data = await response.json();
setApiResponse(data);
};

useEffect(() => {
if (isAuthenticated) {
(async () => {
accessTokenRef.current = await getAccessToken();
})();
}
accessTokenRef.current = "";
}, [getAccessToken, isAuthenticated]);

return (
<div className="prose prose-dark dark:prose-lighter">
<Header>
Expand Down Expand Up @@ -108,9 +118,7 @@ export const App = ({ timeout }: { timeout: string }) => {
<pre className="text-xs">{JSON.stringify(useAuth(), null, 2)}</pre>

<h2>Access Token</h2>
<pre className="text-xs">
{JSON.stringify(useAuth().getAccessToken(), null, 2)}
</pre>
<pre className="text-xs">{accessTokenRef.current}</pre>
</Main>
<Footer row1={<p>Timeout: {timeout}</p>} />
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/auth-provider/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <AuthProvider>.";
Expand Down
10 changes: 8 additions & 2 deletions packages/auth-provider/src/common/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@ export type AuthState = {
userId?: string;
};

export type LoginType = (
username: string,
password: string,
type?: string,
) => Promise<boolean>;

export type AuthContextProps = {
login: (username: string, password: string) => Promise<boolean>;
login: LoginType;
logout: () => void;
getAccessToken: () => string;
getAccessToken: () => Promise<string>;
getIdToken: () => string;
} & AuthState;
49 changes: 49 additions & 0 deletions packages/auth-provider/src/common/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
};
58 changes: 48 additions & 10 deletions packages/auth-provider/src/components/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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@@`,
});

Expand Down Expand Up @@ -111,11 +117,7 @@ export const AuthProvider = ({
removeStateAndLocalStorage,
]);

const login = async (
username: string,
password: string,
type?: string,
): Promise<boolean> => {
const login: LoginType = async (username, password, type) => {
const _nonce = uuidv4();
setNonce(_nonce);

Expand Down Expand Up @@ -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 "";
}
};

Expand Down

0 comments on commit a4a90e1

Please sign in to comment.