T

TechIdea

Ecosystem

Back to react Projects
Intermediate Level

React JWT Authentication & Login System Boilerplate

A production-ready React login system featuring JWT (JSON Web Token) authentication, protected routes, persistent sessions, and clean error handling.

The Problem

Every modern web application requires a secure authentication flow. This boilerplate acts as the universal entry point for SaaS products, internal dashboards, and B2C portals.

Real-World Use Case

Every modern web application requires a secure authentication flow. This boilerplate acts as the universal entry point for SaaS products, internal dashboards, and B2C portals.

Technology Stack

React Router v6

Prerequisite

Understanding of React Context API

Prerequisite

Basic knowledge of HTTP headers and Local Storage

Prerequisite

REST API basics

Prerequisite

Architecture & Design

Folder Structure

src/
├── context/
│   └── AuthContext.jsx
├── components/
│   ├── ProtectedRoute.jsx
│   └── LoginForm.jsx
├── pages/
│   ├── Login.jsx
│   └── Dashboard.jsx
└── App.jsx

API Design

IntegrationAPI

Integrates with a generic /api/auth/login endpoint. It sends JSON { email, password } and expects a { token, user } response.

Step-by-Step Implementation

1

Create an AuthContext to hold the user data and token globally.

### Step 1: Project Setup & Dependencies Initialize a React project using Vite (`npm create vite@latest`). Install React Router DOM for navigation (`npm install react-router-dom`).

react
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { useState } from 'react';

// --- LoginForm Component ---
function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();

  const handleLogin = (e) => {
    e.preventDefault();
    // Simulate API Call
    if (email === 'admin@demo.com' && password === 'password') {
      login('fake-jwt-token-12345');
    } else {
      setError('Invalid email or password');
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50">
      <form onSubmit={handleLogin} className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 w-96">
        <h2 className="text-2xl font-bold mb-6 text-slate-800">Sign In</h2>
        {error && <div className="p-3 bg-red-50 text-red-600 rounded-lg mb-4 text-sm">{error}</div>}
        <input 
          className="w-full p-3 mb-4 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="email" placeholder="admin@demo.com"
          value={email} onChange={(e) => setEmail(e.target.value)} required 
        />
        <input 
          className="w-full p-3 mb-6 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="password" placeholder="password"
          value={password} onChange={(e) => setPassword(e.target.value)} required 
        />
        <button type="submit" className="w-full bg-blue-600 text-white font-bold p-3 rounded-lg hover:bg-blue-700 transition">
          Login
        </button>
      </form>
    </div>
  );
}

// --- Dashboard Component (Protected) ---
function Dashboard() {
  const { logout } = useAuth();
  return (
    <div className="p-10">
      <h1 className="text-3xl font-bold mb-4">Secure Dashboard</h1>
      <p className="text-slate-600 mb-6">Welcome to the protected area.</p>
      <button onClick={logout} className="px-4 py-2 bg-slate-200 rounded-lg hover:bg-slate-300 font-bold">Logout</button>
    </div>
  );
}

// --- Main App ---
export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
          <Route path="*" element={<Navigate to="/login" />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Code Explanation

Implementation step

2

Build a LoginForm component that manages local state (email/password) and submits via Axios/Fetch.

### Step 2: Global Auth Context Create `AuthContext.jsx` to manage the authentication state across your entire application. ```jsx import { createContext, useState, useEffect, useContext } from 'react'; const AuthContext = createContext(null); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); useEffect(() => { // Check for existing token on mount const token = localStorage.getItem('token'); if (token) setUser({ token }); // In a real app, validate token here }, []); const login = (token) => { localStorage.setItem('token', token); setUser({ token }); }; const logout = () => { localStorage.removeItem('token'); setUser(null); }; return ( <AuthContext.Provider value={{ user, login, logout }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => useContext(AuthContext); ```

react
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { useState } from 'react';

// --- LoginForm Component ---
function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();

  const handleLogin = (e) => {
    e.preventDefault();
    // Simulate API Call
    if (email === 'admin@demo.com' && password === 'password') {
      login('fake-jwt-token-12345');
    } else {
      setError('Invalid email or password');
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50">
      <form onSubmit={handleLogin} className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 w-96">
        <h2 className="text-2xl font-bold mb-6 text-slate-800">Sign In</h2>
        {error && <div className="p-3 bg-red-50 text-red-600 rounded-lg mb-4 text-sm">{error}</div>}
        <input 
          className="w-full p-3 mb-4 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="email" placeholder="admin@demo.com"
          value={email} onChange={(e) => setEmail(e.target.value)} required 
        />
        <input 
          className="w-full p-3 mb-6 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="password" placeholder="password"
          value={password} onChange={(e) => setPassword(e.target.value)} required 
        />
        <button type="submit" className="w-full bg-blue-600 text-white font-bold p-3 rounded-lg hover:bg-blue-700 transition">
          Login
        </button>
      </form>
    </div>
  );
}

// --- Dashboard Component (Protected) ---
function Dashboard() {
  const { logout } = useAuth();
  return (
    <div className="p-10">
      <h1 className="text-3xl font-bold mb-4">Secure Dashboard</h1>
      <p className="text-slate-600 mb-6">Welcome to the protected area.</p>
      <button onClick={logout} className="px-4 py-2 bg-slate-200 rounded-lg hover:bg-slate-300 font-bold">Logout</button>
    </div>
  );
}

// --- Main App ---
export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
          <Route path="*" element={<Navigate to="/login" />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Code Explanation

Implementation step

3

On successful response, save the JWT to localStorage and update the AuthContext.

### Step 3: Protected Route & Login UI Create the ProtectedRoute wrapper to guard your private pages. ```jsx import { Navigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; export const ProtectedRoute = ({ children }) => { const { user } = useAuth(); if (!user) { return <Navigate to="/login" />; } return children; }; ```

react
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { useState } from 'react';

// --- LoginForm Component ---
function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();

  const handleLogin = (e) => {
    e.preventDefault();
    // Simulate API Call
    if (email === 'admin@demo.com' && password === 'password') {
      login('fake-jwt-token-12345');
    } else {
      setError('Invalid email or password');
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50">
      <form onSubmit={handleLogin} className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 w-96">
        <h2 className="text-2xl font-bold mb-6 text-slate-800">Sign In</h2>
        {error && <div className="p-3 bg-red-50 text-red-600 rounded-lg mb-4 text-sm">{error}</div>}
        <input 
          className="w-full p-3 mb-4 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="email" placeholder="admin@demo.com"
          value={email} onChange={(e) => setEmail(e.target.value)} required 
        />
        <input 
          className="w-full p-3 mb-6 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="password" placeholder="password"
          value={password} onChange={(e) => setPassword(e.target.value)} required 
        />
        <button type="submit" className="w-full bg-blue-600 text-white font-bold p-3 rounded-lg hover:bg-blue-700 transition">
          Login
        </button>
      </form>
    </div>
  );
}

// --- Dashboard Component (Protected) ---
function Dashboard() {
  const { logout } = useAuth();
  return (
    <div className="p-10">
      <h1 className="text-3xl font-bold mb-4">Secure Dashboard</h1>
      <p className="text-slate-600 mb-6">Welcome to the protected area.</p>
      <button onClick={logout} className="px-4 py-2 bg-slate-200 rounded-lg hover:bg-slate-300 font-bold">Logout</button>
    </div>
  );
}

// --- Main App ---
export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
          <Route path="*" element={<Navigate to="/login" />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Code Explanation

Implementation step

4

Create a ProtectedRoute wrapper that checks the AuthContext; if no user, redirect to /login.

### Step 4: Login Error Handling Ensure your login form catches 401 Unauthorized errors and displays a clear message (e.g., "Invalid credentials") instead of failing silently.

react
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { useState } from 'react';

// --- LoginForm Component ---
function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();

  const handleLogin = (e) => {
    e.preventDefault();
    // Simulate API Call
    if (email === 'admin@demo.com' && password === 'password') {
      login('fake-jwt-token-12345');
    } else {
      setError('Invalid email or password');
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50">
      <form onSubmit={handleLogin} className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 w-96">
        <h2 className="text-2xl font-bold mb-6 text-slate-800">Sign In</h2>
        {error && <div className="p-3 bg-red-50 text-red-600 rounded-lg mb-4 text-sm">{error}</div>}
        <input 
          className="w-full p-3 mb-4 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="email" placeholder="admin@demo.com"
          value={email} onChange={(e) => setEmail(e.target.value)} required 
        />
        <input 
          className="w-full p-3 mb-6 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="password" placeholder="password"
          value={password} onChange={(e) => setPassword(e.target.value)} required 
        />
        <button type="submit" className="w-full bg-blue-600 text-white font-bold p-3 rounded-lg hover:bg-blue-700 transition">
          Login
        </button>
      </form>
    </div>
  );
}

// --- Dashboard Component (Protected) ---
function Dashboard() {
  const { logout } = useAuth();
  return (
    <div className="p-10">
      <h1 className="text-3xl font-bold mb-4">Secure Dashboard</h1>
      <p className="text-slate-600 mb-6">Welcome to the protected area.</p>
      <button onClick={logout} className="px-4 py-2 bg-slate-200 rounded-lg hover:bg-slate-300 font-bold">Logout</button>
    </div>
  );
}

// --- Main App ---
export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
          <Route path="*" element={<Navigate to="/login" />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Code Explanation

Implementation step

5

Wrap the application routes with the AuthProvider in App.jsx.

### Step 4: Login Error Handling Ensure your login form catches 401 Unauthorized errors and displays a clear message (e.g., "Invalid credentials") instead of failing silently.

react
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ProtectedRoute } from './components/ProtectedRoute';
import { useState } from 'react';

// --- LoginForm Component ---
function Login() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const { login } = useAuth();

  const handleLogin = (e) => {
    e.preventDefault();
    // Simulate API Call
    if (email === 'admin@demo.com' && password === 'password') {
      login('fake-jwt-token-12345');
    } else {
      setError('Invalid email or password');
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-slate-50">
      <form onSubmit={handleLogin} className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200 w-96">
        <h2 className="text-2xl font-bold mb-6 text-slate-800">Sign In</h2>
        {error && <div className="p-3 bg-red-50 text-red-600 rounded-lg mb-4 text-sm">{error}</div>}
        <input 
          className="w-full p-3 mb-4 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="email" placeholder="admin@demo.com"
          value={email} onChange={(e) => setEmail(e.target.value)} required 
        />
        <input 
          className="w-full p-3 mb-6 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500"
          type="password" placeholder="password"
          value={password} onChange={(e) => setPassword(e.target.value)} required 
        />
        <button type="submit" className="w-full bg-blue-600 text-white font-bold p-3 rounded-lg hover:bg-blue-700 transition">
          Login
        </button>
      </form>
    </div>
  );
}

// --- Dashboard Component (Protected) ---
function Dashboard() {
  const { logout } = useAuth();
  return (
    <div className="p-10">
      <h1 className="text-3xl font-bold mb-4">Secure Dashboard</h1>
      <p className="text-slate-600 mb-6">Welcome to the protected area.</p>
      <button onClick={logout} className="px-4 py-2 bg-slate-200 rounded-lg hover:bg-slate-300 font-bold">Logout</button>
    </div>
  );
}

// --- Main App ---
export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/dashboard" element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          } />
          <Route path="*" element={<Navigate to="/login" />} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Code Explanation

Implementation step

Common Errors

Page flashes /login before redirecting to /dashboard on refresh.

Add a 'loading' state to AuthContext that waits for the token to be read from Local Storage before rendering routes.

Token expires but frontend doesn't know.

Implement an Axios interceptor to catch 401 responses and automatically call the logout() function.

Security & Performance

Attempt to visit /dashboard while logged out; verify redirection to /login.

Enter incorrect credentials and verify the error message appears.

Enter correct credentials and verify successful redirection to /dashboard.

Refresh the page while on /dashboard; verify you remain logged in.


Replace Local Storage with HttpOnly cookies for better XSS protection.

Add a 'Remember Me' checkbox that toggles between Local Storage and Session Storage.

Implement a Refresh Token flow.

Interview Questions

Q: Is Local Storage secure for JWTs?

A: Local Storage is vulnerable to Cross-Site Scripting (XSS). For highly sensitive apps, it is recommended to store tokens in HttpOnly cookies.

Q: How do I decode a JWT on the frontend?

A: You can use the 'jwt-decode' library to decode the token payload without verifying the signature.

Growth Newsletter

Get practical AI tools, SEO tips, and growth guides weekly.

Join creators, students, and businesses scaling with TechIdea.