JWT authentication with react
Categories
JavaScriptIn this article, we will set up JWT (Json Web Token) authentication with React
. JWT is a secure authentication method that has the characteristic of being able to include information in its token. This information is regularly updated during the session because the token has a very short expiration time, ranging from a few tens of seconds to several minutes depending on the needs. It is regenerated each time from a refresh_token
, which expires after several days, weeks, or even months. Due to the frequent token changes, risks are limited in the event of interception. The refresh token, however, is much more sensitive, and additional verifications can be considered when it is used (such as checking the IP address).
How does a JWT work?
A JWT contains encoded data and consists of three parts: the header
, the payload
, and the signature
.
- The header contains information about the nature of the token.
- The payload contains useful data, called claims. Some are reserved for specific uses, such as
iat
, which is the token's issuance timestamp, orsub
, which references the user. You can find the complete list of these reserved claims here. You can add as many claims as you want, but be careful not to include sensitive information, as these can be easily read by anyone. - The signature simply certifies the content of the token; if any information is modified, the verification will fail.
Here's what a token looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoded Representation:
// header
{
"alg": "HS256",
"typ": "JWT"
}
// PAYLOAD
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
// Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
Installation
Setting Up a React Project
Install React with Vitejs, preferably with TypeScript.
npm create vite@latest
Installing Prerequisites
React Router Dom for routing in our React application.
Zod for schema declaration and validation.
React Hook form for handling forms in React.
Axios for HTTP requests.
npm install react-router-dom
npm install zod
npm install react-hook-form
npm install axios
Setting Up a React Context
A context is a component that allows data to be shared across the component tree without having to use React's props. It can store tokens in localStorage to verify if the user is authenticated.
import {createContext, ReactNode, useContext, useEffect, useMemo, useState} from "react";
import axios from "axios";
import {BASE_URL} from "../../config/axios.ts";
interface AuthContext {
token: string | null;
refreshToken: string | null;
setToken: (newToken: string | null) => void;
setRefreshToken: (newRefreshToken: string | null) => void;
setAllTokens: (token: string, refreshToken: string) => void;
login: (data: {token: string, refresh_token: string } ) => void;
logout: () => void;
}
export enum AuthEnum {
TOKEN = 'token',
REFRESH_TOKEN = 'refresh_token'
}
export const AuthContext = createContext<AuthContext | null>(null)
export const AuthProvider = ({children}: { children: ReactNode }) => {
const [token, setToken] = useState<string | null>(localStorage.getItem(AuthEnum.TOKEN));
const [refreshToken, setRefreshToken] = useState<string | null>(localStorage.getItem(AuthEnum.REFRESH_TOKEN));
const login = async (data: { token: string, refresh_token: string }) => {
setAllTokens(data.token, data.refresh_token)
}
const logout = async () => {
let response = await axios.post(BASE_URL + "/auth/logout", {refresh_token: refreshToken})
if (response.status === 200) {
setToken(null)
setRefreshToken(null)
localStorage.clear()
}
}
const setAllTokens = (token: string, refreshToken: string) => {
setToken(token)
setRefreshToken(refreshToken)
localStorage.setItem(AuthEnum.TOKEN, token);
localStorage.setItem(AuthEnum.REFRESH_TOKEN, refreshToken);
}
useEffect(() => {
if (token && refreshToken) {
localStorage.setItem(AuthEnum.TOKEN, token);
localStorage.setItem(AuthEnum.REFRESH_TOKEN, refreshToken);
}
}, [token, refreshToken]);
const contextValue = useMemo(
() => ({
token,
refreshToken,
setToken,
setRefreshToken,
setAllTokens,
login,
logout
}),
[token, refreshToken]
);
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext)
if (context === null) {
throw new Error('useAuth must be used within a AuthProvider')
}
return context
}
To make the context functional, the router configuration is mandatory.
import React from 'react'
import ReactDOM from 'react-dom/client'
import './assets/style/app.css'
import {AuthProvider} from "./context/modules/AuthContext.tsx";
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider> <-- contexte
</AuthProvider>
</React.StrictMode>
)
View login
import {SubmitHandler, useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {z} from "zod";
import {useNavigate} from "react-router-dom";
import {useAuth} from "../../context/modules/AuthContext.tsx";
import {BASE_URL} from "../../config/axios.ts";
// Check schema for object
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
export const loginSchemaType = z.object({
token: z.string(),
refresh_token: z.string()
})
type FormFields = z.infer<typeof schema>
export default function Login() {
const {login} = useAuth()
const nav = useNavigate()
const {
register,
handleSubmit,
formState: {errors}
} = useForm<FormFields>({
resolver: zodResolver(schema)
})
const onSubmit: SubmitHandler<FormFields> = async (data: {email: string, password: string}) => {
try {
const response = await fetch(BASE_URL + '/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: data.email,
password: data.password
})
});
const json = await response.json();
login(loginSchemaType.parse(json));
nav("/");
} catch (error) {
console.log('Error:', error);
}
}
return <div className="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<img src="/logo.png" alt="Logo" height="100" width="175" className="mx-auto"/>
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
Connectez-vous à votre compte
</h2>
</div>
<div className="mt-5 sm:mx-auto sm:w-full sm:max-w-sm">
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="email">Email</label>
<input
{...register('email')}
type="text"
placeholder="john.doe@exemple.com"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
{errors.email &&
<span>{errors.email.message}</span>
}
</div>
<div className="mt-3">
<label htmlFor="password">Password</label>
<input
{...register('password')}
type="password"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/>
{errors.password &&
<span>{errors.password.message}</span>
}
</div>
<div className='mt-3'>
<button type="submit"
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Login
</button>
</div>
</form>
</div>
</div>
}
Routes protected:
Protected routes will redirect to the login page if the user is not authenticated.
import {Navigate, Outlet} from "react-router-dom";
import {useAuth} from "../modules/AuthContext.tsx";
export function ProtectedRouteProvider() {
const {token} = useAuth()
if (!token) {
return <Navigate replace to="/login"/>
}
return <>
<Outlet/>
</>
}
Here is an example implementation of ProtectedRouteProvider:
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<ProtectedRouteProvider/>}>
<Route path="/" element={<App/>} />
</Route>
<Route path="/login" element={<Login/>}/>
</Routes>
</BrowserRouter>
</AuthProvider>
</React.StrictMode>
)
Token Refresh on 401 Error
For this, axios perfectly meets our needs. We will use some of its features, such as global variables and request interceptors. When a request fails with a 401 status code, axios will send a refresh request using our refresh token. It will then retry the original request with the freshly obtained token.
import axios from "axios";
import {useAuth, AuthEnum} from "../context/modules/AuthContext.tsx";
export const BASE_URL = "http://localhost:8080";
export const useAxios = () => {
const {setAllTokens, logout} = useAuth()
const token = localStorage.getItem(AuthEnum.TOKEN)
const axiosInstance = axios.create({
baseURL: BASE_URL,
headers: {
Authorization: `Bearer ${token}`
}
})
axiosInstance.defaults.headers.post["Content-Type"] = "application/json";
axiosInstance.interceptors.request.use(
(config) => {
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
axiosInstance.interceptors.response.use(
(res) => res,
async (error) => {
const baseReq = error.config
if (error.response && error.response.status === 401 && !baseReq._retry) {
baseReq._retry = true
try {
const response = await axios.post(BASE_URL + "/auth/refresh", {
refresh_token: localStorage.getItem(AuthEnum.REFRESH_TOKEN)
})
if (response.data.token == null) {
logout()
return Promise.reject(error)
}
setAllTokens(response.data.token, response.data.refresh_token)
baseReq.headers.Authorization = `Bearer ${response.data.token}`
return axios(baseReq)
} catch (e) {
return Promise.reject(error)
}
}
return Promise.reject(error)
}
)
return axiosInstance
}