Authentication and authorization are critical components of nearly every modern web application. In a typical React app, ensuring that users can only access the parts of the application they're allowed to is essential for both security and user experience.
In this tutorial, we’ll implement authentication using JSON Web Tokens (JWT) and enforce role-based access control (RBAC) in a React application. You’ll learn how to protect routes, restrict access based on user roles (like admin
or user
), and manage user sessions efficiently.
We'll cover the full flow from logging in, storing and decoding JWTs, protecting routes, and displaying UI components based on user roles. By the end of this guide, you’ll have a solid understanding of how to:
-
Authenticate users in a React frontend using JWT
-
Store and decode JWT tokens securely
-
Protect frontend routes and redirect unauthorized users
-
Implement role-based access restrictions (RBAC)
-
Build a practical login flow with session handling
Whether you're building a dashboard, admin panel, or multi-role platform, this tutorial will give you the foundational tools to secure your React app properly.
Let’s get started!
1. Prerequisites
Before diving into this tutorial, make sure you have the following tools and knowledge in place:
✅ Tools Installed
-
Node.js and npm – Download from https://nodejs.org/
-
A modern code editor, such as Visual Studio Code
-
Git (optional, but recommended)
✅ Basic Knowledge
-
Familiarity with React and React Router
-
Understanding of RESTful APIs
-
Basic concepts of JWT (JSON Web Token) and how authentication flows work
💡 If you're unfamiliar with JWT, it's a compact, URL-safe token format used to securely transmit information between parties. In our case, it helps validate users without storing session data on the server.
✅ Backend API
We’ll be connecting this React frontend to a backend that supports JWT authentication and returns user information, including roles. If you don’t have one, you can use our simple Express.js-based mock backend or connect to any existing API that returns tokens like this:
{
"accessToken": "your.jwt.token.here",
"user": {
"username": "djamware",
"role": "admin"
}
}
If you're interested, we’ll link to a ready-to-use backend later in this tutorial.
2. Project Setup
To get started, let’s create a new React project and install the necessary dependencies for routing, HTTP requests, and JWT decoding.
🔧 Step 1: Create a New React App
We’ll use Vite for a faster development experience, but feel free to use Create React App if preferred.
Run the following commands in your terminal:
npm create vite@latest react-auth-jwt -- --template react
cd react-auth-jwt
npm install
Then start the dev server to make sure everything works:
npm run dev
📦 Step 2: Install Required Dependencies
We’ll use the following packages:
-
react-router-dom
– for navigation and route protection -
axios
– for making HTTP requests -
jwt-decode
– to decode the JWT and extract user info
Install them all at once:
npm install react-router-dom axios jwt-decode
📁 Step 3: Project Structure Overview
Here’s the suggested folder structure we'll use:
src/
├── components/
│ ├── Login.jsx
│ ├── Dashboard.jsx
│ ├── AdminPanel.jsx
│ └── ProtectedRoute.jsx
├── contexts/
│ └── AuthContext.jsx
├── pages/
│ └── NotFound.jsx
├── App.jsx
├── main.jsx
└── utils/
└── auth.js
🗂️ Don't worry—we’ll walk through each file step by step.
>3. Creating the Auth Context
To handle authentication globally in our React app, we’ll use the Context API to create an AuthContext
that stores and manages the user’s JWT token, role, and login state.
🧠 What AuthContext Will Handle:
-
Logging in and storing the JWT
-
Decoding the JWT to extract user info and role
-
Providing authentication state to the rest of the app
-
Logging out and clearing the token
📁 Step 1: Create AuthContext.jsx
Create a new file in src/contexts/AuthContext.jsx
and add the following code:
import { createContext, useContext, useState, useEffect } from "react";
import jwtDecode from "jwt-decode";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null); // user object with role
const [token, setToken] = useState(null);
useEffect(() => {
const savedToken = localStorage.getItem("token");
if (savedToken) {
const decoded = jwtDecode(savedToken);
setToken(savedToken);
setUser(decoded.user || decoded);
}
}, []);
const login = (token) => {
localStorage.setItem("token", token);
const decoded = jwtDecode(token);
setToken(token);
setUser(decoded.user || decoded);
};
const logout = () => {
localStorage.removeItem("token");
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ user, token, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// Custom hook for easy access
export const useAuth = () => useContext(AuthContext);
🧪 Example Decoded JWT Payload
Assuming your backend returns a JWT like this:
{
"user": {
"username": "djamware",
"role": "admin"
},
"exp": 1721234567
}
The context will store the decoded user
and make it available across your app.
🧩 Step 2: Wrap the App with AuthProvider
In src/main.jsx
, wrap your root component with the AuthProvider
:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.jsx";
import { AuthProvider } from "./contexts/AuthContext.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</StrictMode>
);
Now your entire app has access to the user
, token
, login
, and logout
functions.
4. Implementing the Login Page
Now that we have our AuthContext
set up, let's build a simple Login Page that authenticates users by calling the backend API and storing the JWT.
🧱 Step 1: Create the Login Component
Create a new file at src/components/Login.jsx
and add the following:
import { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
const Login = () => {
const { login } = useAuth();
const navigate = useNavigate();
const [credentials, setCredentials] = useState({
username: "",
password: ""
});
const [error, setError] = useState("");
const handleChange = (e) => {
setCredentials({ ...credentials, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
try {
const response = await axios.post(
"http://localhost:4000/api/login",
credentials
);
const { accessToken } = response.data;
login(accessToken);
navigate("/dashboard");
} catch (err) {
setError("Invalid username or password.");
}
};
return (
<div className="login-page">
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
name="username"
placeholder="Username"
value={credentials.username}
onChange={handleChange}
required
/>
<br />
<input
type="password"
name="password"
placeholder="Password"
value={credentials.password}
onChange={handleChange}
required
/>
<br />
<button type="submit">Login</button>
{error && <p className="error">{error}</p>}
</form>
</div>
);
};
export default Login;
🌐 Step 2: Sample Backend Response
Make sure your backend login endpoint returns a response like this:
{
"accessToken": "your.jwt.token",
"user": {
"username": "djamware",
"role": "admin"
}
}
The accessToken
will be decoded and stored in the AuthContext
.
🧭 Step 3: Add a Route for Login
In your App.jsx
, make sure you have React Router set up and include a route for the login page:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./components/Login";
import Dashboard from "./components/Dashboard";
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
{/* Other routes */}
</Routes>
</Router>
);
}
export default App;
Now your login page is fully functional! If the credentials are valid, the user will be redirected to the Dashboard and their token will be stored.
5. Creating Protected Routes
We don’t want unauthenticated users to access sensitive parts of the app (like the Dashboard or Admin Panel). To enforce this, we’ll create a reusable ProtectedRoute
component that checks if the user is logged in.
🔒 Step 1: Create ProtectedRoute.jsx
In src/components/ProtectedRoute.jsx
, add the following code:
import { Navigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
const ProtectedRoute = ({ children }) => {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
};
export default ProtectedRoute;
This component checks whether a user is authenticated (user
exists in AuthContext
). If not, it redirects them to the login page.
🛠 Step 2: Use ProtectedRoute
in App.jsx
Now wrap your private routes using this component:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./components/Login";
import ProtectedRoute from "./components/ProtectedRoute";
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* Add more protected routes as needed */}
</Routes>
</Router>
);
}
export default App;
✅ Now if a user tries to access
/dashboard
without logging in, they’ll be redirected to/login
.
✅ Bonus: Improve User Experience
You can show a loading spinner or check for token
in memory while your app is bootstrapping to avoid flashes of unauthenticated content. We’ll explore that in enhancements later.
With this in place, your app now supports route-level protection for authenticated users.
6. Role-Based Route Guard
Now that we’ve protected routes for authenticated users, let’s go further and restrict access based on user roles (e.g., admin
, user
, etc.). We’ll create a RoleBasedRoute
component to wrap around views meant for specific roles only.
🔒 Step 1: Create RoleBasedRoute.jsx
In src/components/RoleBasedRoute.jsx
, add the following:
import { Navigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
const RoleBasedRoute = ({ children, role }) => {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
if (user.role !== role) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
export default RoleBasedRoute;
This component:
- Redirects unauthenticated users to /login
- Redirects authenticated users with the wrong role to an /unauthorized page
🚫 Step 2: Create Unauthorized.jsx
In src/pages/Unauthorized.jsx
, create a simple page:
const Unauthorized = () => {
return (
<div>
<h2>Unauthorized</h2>
<p>You do not have permission to view this page.</p>
</div>
);
};
export default Unauthorized;
🛣 Step 3: Use Role-Based Route in App.jsx
Update your router with a protected admin-only route:
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Login from "./components/Login";
import ProtectedRoute from "./components/ProtectedRoute";
import RoleBasedRoute from "./components/RoleBasedRoute";
import Unauthorized from "./pages/Unauthorized";
import AdminPanel from "./components/AdminPanel";
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/unauthorized" element={<Unauthorized />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<RoleBasedRoute role="admin">
<AdminPanel />
</RoleBasedRoute>
}
/>
</Routes>
</Router>
);
}
export default App;
✅ Now only users with the role
admin
can access/admin
.
With role-based routing in place, you can now build different views for users based on their access level.
7. Creating User and Admin Views
Now that route guards are in place, let’s create two basic components:
-
A Dashboard for all authenticated users
-
An Admin Panel for
admin
users only
🧑💼 Step 1: Create Dashboard.jsx
In src/components/Dashboard.jsx
, add:
import { useAuth } from "../contexts/AuthContext";
const Dashboard = () => {
const { user, logout } = useAuth();
return (
<div>
<h2>Welcome, {user?.username}!</h2>
<p>Your role: <strong>{user?.role}</strong></p>
<p>This is the dashboard accessible to all authenticated users.</p>
<button onClick={logout}>Logout</button>
</div>
);
};
export default Dashboard;
👑 Step 2: Create AdminPanel.jsx
In src/components/AdminPanel.jsx
, add:
const AdminPanel = () => {
return (
<div>
<h2>Admin Panel</h2>
<p>This page is only accessible by users with the <strong>admin</strong> role.</p>
</div>
);
};
export default AdminPanel;
🧪 Step 3: Test It All
-
Log in with a user whose role is
"user"
– you should access/dashboard
, but get redirected from/admin
. -
Log in with a user whose role is
"admin"
– you should be able to access both/dashboard
and/admin
.
💡 You can extend these components later with real dashboards, admin features, and data visualizations as needed.
You now have a complete working system for authentication + role-based authorization in a React app!
8. Logout and Token Expiry Handling
It’s important to allow users to log out manually and also automatically handle cases where their JWT has expired. This section will cover both.
🔓 Step 1: Logout Functionality (Already Done)
You’ve already implemented logout in the AuthContext
:
const logout = () => {
localStorage.removeItem("token");
setToken(null);
setUser(null);
};
And used it in the Dashboard
like this:
<button onClick={logout}>Logout</button>
✅ Users can now manually log out and be redirected to the login page (we’ll automate that redirect below).
⏳ Step 2: Detect Expired Token with jwt-decode
Update AuthContext.jsx
to check if the token is expired on app load and automatically log the user out:
useEffect(() => {
const savedToken = localStorage.getItem("token");
if (savedToken) {
try {
const decoded = jwtDecode(savedToken);
const isExpired = decoded.exp * 1000 < Date.now();
if (isExpired) {
logout();
} else {
setToken(savedToken);
setUser(decoded.user || decoded);
}
} catch (err) {
logout(); // invalid token
}
}
}, []);
This ensures:
-
Expired or invalid tokens are cleared automatically
-
Users won’t access protected views with outdated tokens
🛑 Optional: Auto-Logout on Token Expiration During Session
To go further, you can set a timeout to log the user out when the token expires:
useEffect(() => {
if (token) {
const decoded = jwtDecode(token);
const expiresIn = decoded.exp * 1000 - Date.now();
const timeout = setTimeout(() => {
logout();
alert("Session expired. Please log in again.");
}, expiresIn);
return () => clearTimeout(timeout);
}
}, [token]);
This improves UX by automatically logging out users when their session expires.
Now your app handles both manual logout and token expiry, improving security and session handling.
9. Final Demo and Testing
With all components in place, it’s time to test the full authentication and role-based access flow of your React app. Here's what you should verify:
✅ Full Authentication Flow
-
Start your frontend (React):
npm run dev
-
Start your backend (Express or any API that returns JWT).
-
Visit
/login
:-
Enter valid credentials.
-
On success, you’re redirected to
/dashboard
.
-
-
Dashboard view should show:
-
Logged-in user’s
username
androle
-
Logout button
-
-
Try visiting
/admin
:-
If logged in as a
user
, you’re redirected to/unauthorized
-
If logged in as an
admin
, you see the Admin Panel
-
-
Logout and attempt to access.
/dashboard
again:-
You should be redirected to
/login
-
-
Refresh the browser:
-
User stays logged in if the token is valid
-
User is auto-logged out if the token expires
-
🧪 Example Test Accounts
Username | Password | Role |
---|---|---|
admin1 |
admin123 |
admin |
user1 |
user123 |
user |
(These should match your backend credentials. You can hardcode or use mock users for testing.)
🔐 Token Storage and Security Notes
-
We used
localStorage
for simplicity, but consider HTTP-only cookies for better security in production. -
Never store sensitive user data directly in localStorage.
-
Use HTTPS in production to secure token transmission.
🛠 Optional Enhancements
-
Refresh tokens and session persistence
-
Auto-refresh tokens before expiration
-
Multi-role support (e.g., editor, manager, superadmin)
-
Dynamic navigation based on role
-
Integration with real OAuth/JWT providers (e.g., Auth0, Firebase, Keycloak)
Your React app now supports:
✅ JWT-based login
✅ Context-powered authentication
✅ Protected and role-based routing
✅ Token expiration handling
✅ Clean logout flow
10. Conclusion
In this tutorial, you’ve built a fully functional React authentication system using JWT (JSON Web Token) and Role-Based Access Control (RBAC). You learned how to:
-
Set up a secure login flow in React
-
Store and decode JWT tokens to manage user sessions
-
Protect routes from unauthenticated access
-
Restrict access based on user roles like
admin
oruser
-
Handle logout and token expiration
These are foundational techniques for any secure single-page application (SPA). Whether you’re building dashboards, portals, or enterprise tools, adding RBAC gives you the power to tailor access and functionality based on user identity.
🎯 Next Steps
-
Implement refresh tokens for long-lived sessions
-
Store tokens in HTTP-only cookies for enhanced security
-
Integrate with third-party auth providers (Google, Auth0, Firebase)
-
Add unit tests for route protection and auth context
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about MERN Stack, React.js, or React Native, you can take the following cheap course:
- Mastering React JS
- Master React Native Animations
- React: React Native Mobile Development: 3-in-1
- MERN Stack Front To Back: Full Stack React, Redux & Node. js
- Learning React Native Development
Thanks!