My react app has the following routes and contexts:
const App = () =>
<AuthContextProvider>
<IntelContextProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="login" element={<Authentication />} />
<Route path="register" element={<Registration />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Home />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</IntelContextProvider>
</AuthContextProvider>
Authentication uses an access token stored in memory (AuthContextProvider) and a refresh token stored in a HttpOnly cookie.
My home path uses a protected route implemented as follows:
export const RequireAuth = () => {
const location = useLocation()
const {auth} = useAuth()
return (auth?.username ?
<Outlet/> :
<Navigate to='/login' state={{from: location}} replace/> )
}
If the user is not authenticated, it is redirected to /login without any issue. However, I need to do the opposite too:
If the user has a valid refresh token, when the page is refreshed or the user requests '/register' or '/login pages', then I want the route to be redirected to Home component again.
I tried to put do the following in the Authentication component:
const Authentication = () => {
const [authenticated, setAuthenticated] = useState(null)
const {values, errors, handleChange, handleSubmit} = useAuthenticationForm(validate)
const silentlySignIn = useSilentSignIn()
useEffect(() => {
const silentlyLogin = async () => {
const isAuthenticated = await silentlySignIn()
if(isAuthenticated) setAuthenticated(true)
}
silentlyLogin()
// eslint-disable-next-line
}, [])
return (
authenticated ? <Navigate to='/'/> :
<main className="authentication">
<form>
...
</form>
</main>
);
}
Here is my AuthContext:
export const AuthContextProvider = ({ children }) => {
const [auth, setAuth] = useState()
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
And Here is my useSignIn hook:
const useSignIn = () => {
const [success, setSuccess] = useState(false)
const [error, setError] = useState()
const { setAuth } = useAuth()
const signIn = async (payload) => {
try {
setError(null)
const { headers: { authorization: token }, data: { uuid } } = await axiosPrivate.post(`/login`, payload)
setAuth({ token, uuid, username: payload.username })
setSuccess(true)
} catch (error) {
console.log(error.message)
if (!error.response) setError('Sistema temporariamente indisponível.')
else if (error.response.status === 401) setError('Usuário ou senha inválidos.')
else setError('Algo deu errado.')
}
}
return [signIn, success, error]
}
Here is my useSilentSignIn (to get a new access token if the refresh token is still valid):
const useSilentSignIn = () => {
const { auth, setAuth } = useAuth()
const silentlySignIn = async () => {
try {
if (auth?.uuid) return false
const response = await axiosPrivate.get('/refresh-token')
const token = response.headers.authorization
const uuid = response.data.uuid
const username = response.data.username
setAuth(prev => ({ ...prev, token, uuid, username }))
return true
} catch (error) {
console.log('Logged out. Please sign in.')
return false
}
}
return silentlySignIn
}
I "solved" the problem, but it first renders the login, then navigates to '/' (due to React component lifecycle). It does not seem like a good solution, it is ugly, and I would need to do the same for '/register' or any similar route.
How to implement something efficient for such a problem?
Github of the project: https://github.com/lucas-ifsp/CTruco-front
Thanks