React Authentication with JWT and Role-Based Access Control

by Didin J. on Jul 23, 2025 React Authentication with JWT and Role-Based Access Control

Learn how to implement JWT authentication and role-based access control in React with protected routes, context, and user role validation.

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

React Authentication with JWT and Role-Based Access Control - AWS

📦 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

  1. Log in with a user whose role is "user" – you should access /dashboard, but get redirected from /admin.

  2. 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

  1. Start your frontend (React): npm run dev

  2. Start your backend (Express or any API that returns JWT).

  3. Visit /login:

    • Enter valid credentials.

    • On success, you’re redirected to /dashboard.

  4. Dashboard view should show:

    • Logged-in user’s username and role

    • Logout button

  5. 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

  6. Logout and attempt to access. /dashboard again:

    • You should be redirected to /login

  7. 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 or user

  • 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:

Thanks!