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

feat: enabling access token renewal via refresh token #55

Merged
merged 2 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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: 1 addition & 1 deletion packages/auth-provider/bundlesize.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default {
*/
{
path: "dist/index.js",
limit: "9 kb",
limit: "10 kb",
},
],
};
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