Wanted to add a concrete example to this question. This is how I am handling it with SolidJS using a context provider/ their way of handling global state:
My routes.tsx
looks like this:
import { lazy } from 'solid-js';
import { Route, Router } from '@solidjs/router';
import { getCurrentUser } from '~/apis/current-user.api';
import AuthContextProvider from '~/contexts/Auth.context';
import HomeRoutes from '~/pages/auth/routes';
const LandingPage = lazy(() => import('./pages/landing/index'));
const user = await getCurrentUser();
export const Routes = () => {
return (<AuthContextProvider
user={user}>
<Router>
<Route path='/'>
<Route path='/' component={LandingPage} />,
<HomeRoutes/>,
<Route path='/*' component={LandingPage} />,
</Route>,
</Router>
</AuthContextProvider>)
};
I'm getting the current user and passing it to my AuthContextProvider
, because it's at the top level of all of the routes every component will have access to it and will be able to call const { user } = useAuthContext();
to get the user information.
The ~/apis/current-user.api looks something like this:
import { API_URL, COOKIES, getHeadersWithAuth } from "~/constants/api";
import { emptyUser, User } from "~/models/user.model";
import { firebaseAuth } from '~/utils/firebase';
const TOKEN_EXPIRATION_TIME = 55 * 60 * 1000; // 55 minutes in milliseconds
export const refreshUserToken = async () => {
const userToken = localStorage.getItem("userToken");
const tokenCreationTime = parseInt(localStorage.getItem("tokenCreationTime"), 10);
const currentTime = Date.now();
if (userToken && tokenCreationTime && currentTime - tokenCreationTime < TOKEN_EXPIRATION_TIME) {
sessionStorage.setItem(COOKIES.ACCESS_TOKEN, userToken);
return true;
}
try {
// Force a token refresh
const user = firebaseAuth.currentUser;
if (!user) {
return false;
}
const userToken = await user.getIdToken(true);
sessionStorage.setItem(COOKIES.ACCESS_TOKEN, userToken);
// Update the local storage with new token and creation time
localStorage.setItem("userToken", userToken);
localStorage.setItem("tokenCreationTime", Date.now().toString());
return true;
} catch (error) {
return false;
}
};
export const getCurrentUser = async () => {
const result = await refreshUserToken();
if (!result) {
return emptyUser();
}
if (!sessionStorage.getItem(COOKIES.ACCESS_TOKEN)) {
return emptyUser();
}
const endPoint = `${API_URL}/auth/current-user`;
const res = await fetch(endPoint, {
method: 'GET',
headers: getHeadersWithAuth(),
// credentials: 'include',
}).catch(err => {
console.error(`Error: getCurrentUser - ${endPoint}`, err);
return err;
});
if (res.status !== 200) {
sessionStorage.removeItem(COOKIES.ACCESS_TOKEN);
// Remove tokens from local storage
localStorage.removeItem("userToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("tokenCreationTime");
return emptyUser();
}
type data = {
user: User;
}
const { user: _user }: data = await res.json();
const user = _user ? new User(_user) : emptyUser();
return user;
}
This way whenever the client refreshes or goes to a new page, the getCurrentUser
function is called and will check to see if the token has expired or not. If it hasn't it will use the found in the session token. If not it will create a new one.
I'm using local storage so that the user stays logged in even when they close the window and come back to it. I did that using setPersistence
with the browserLocalPersistence
option.
When the user signs out, I am deleting the data from local storage and calling revokeRefreshTokens
which causes the refreshToken to no longer be valid.
You can for sure go a step further to make things even more secure if you want, but this should be a good jump off point.