.Net application development specialists
asp.net, c#, vb.net, html, javascript, jquery, html, xhtml, css, oop, design patterns, sql server, mvc and much more
contact: admin@paxium.co.uk

Paxium is the company owned by myself, Dave Amour and used for providing IT contract development services including


  • Application development - Desktop, Web, Services - with Classic ASP, Asp.net WebForms, Asp.net MVC, Asp.net Core
  • Html, Css, JavaScript, jQuery, React, C#, SQL Server, Ado.net, Entity Framework, NHibernate, TDD, WebApi, GIT, IIS
  • Database schema design, implementation & ETL activities
  • Website design and hosting including email hosting
  • Training - typically one to one sessions
  • Reverse Engineering and documentation of undocumented systems
  • Code Reviews
  • Performance Tuning
  • Located in Cannock, Staffordshire
Rugeley Chess Club Buying Butler Cuckooland Katmaid Pet Sitting Services Roland Garros 60 60 Golf cement Technical Conformity Goofy MaggieBears Vacc Track Find Your Smart Phone eBate Taylors Poultry Services Lafarge Rebates System Codemasters Grid Game eBate DOFF

Protecting an Admin Page in React Using JWT + Route Guards

This guide shows a minimal, production-ready pattern: log in to get a JWT, store it, expose auth state via context, guard routes with <RequireAuth> (and optional roles), and redirect users back to where they came from.

1) Environment Variable for Your API Base URL (Vite)

Keep the API base out of code and switch per environment.

# .env.local  (NOT committed)
VITE_USERSERVICE_BASEURL=https://your-user-service.example.com

2) Token Utilities (parse, expiry, roles)

Helpers to read the JWT from localStorage, decode it, check expiry, and detect roles (works with common ASP.NET claim names too).

// src/auth/token.js
export const TOKEN_KEY = "pb_jwt";

export function getToken() {
  return localStorage.getItem(TOKEN_KEY);
}

export function parseJwt(token) {
  try {
    const base64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
    return JSON.parse(atob(base64));
  } catch {
    return null;
  }
}

export function isExpired(token) {
  const payload = parseJwt(token);
  if (!payload?.exp) return true;
  const now = Math.floor(Date.now() / 1000);
  return payload.exp <= now;
}

export function hasRole(token, required) {
  const p = parseJwt(token);
  if (!p) return false;
  const all = []
    .concat(p.role ?? [], p.roles ?? [], p["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"] ?? [])
    .flat();
  return required ? all.includes(required) : false;
}

3) Auth Context (single source of truth)

Wrap the app in a provider; expose login / logout and isAuthenticated. This immediately updates consumers (e.g., route guards) after login.

// src/auth/AuthContext.jsx
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { TOKEN_KEY, getToken, isExpired } from "./token";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [token, setToken] = useState(() => getToken());

  useEffect(() => {
    if (token && isExpired(token)) {
      localStorage.removeItem(TOKEN_KEY);
      setToken(null);
    }
  }, [token]);

  useEffect(() => {
    const onStorage = (e) => { if (e.key === TOKEN_KEY) setToken(e.newValue); };
    window.addEventListener("storage", onStorage);
    return () => window.removeEventListener("storage", onStorage);
  }, []);

  const value = useMemo(() => ({
    token,
    isAuthenticated: Boolean(token) && !isExpired(token),
    login: (t) => { localStorage.setItem(TOKEN_KEY, t); setToken(t); },
    logout: () => { localStorage.removeItem(TOKEN_KEY); setToken(null); },
  }), [token]);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within <AuthProvider>.");
  return ctx;
}

4) Route Guards: RequireAuth and RequireRole

Redirect unauthenticated users to /login (preserving where they wanted to go). Optionally require a role.

// src/auth/RequireAuth.jsx
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./AuthContext";

export default function RequireAuth({ children }) {
  const { isAuthenticated } = useAuth();
  const location = useLocation();
  return isAuthenticated ? children : <Navigate to="/login" replace state={{ from: location }} />;
}

// src/auth/RequireRole.jsx (optional)
import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "./AuthContext";
import { hasRole } from "./token";

export default function RequireRole({ role, children }) {
  const { token, isAuthenticated } = useAuth();
  const location = useLocation();
  if (!isAuthenticated) return <Navigate to="/login" replace state={{ from: location }} />;
  if (!hasRole(token, role)) return <Navigate to="/forbidden" replace />;
  return children;
}

5) Router Wiring (provider wraps the router)

Protect /admin. Use a separate layout for auth pages if desired. Provider must wrap RouterProvider.

// src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

import "./index.css";
import Layout from "./layouts/Layout";
import AuthLayout from "./layouts/AuthLayout";

import Home from "./pages/Home";
import AllAboutMe from "./pages/AllAboutMe";
import Services from "./pages/Services";
import Pricing from "./pages/Pricing";
import Contact from "./pages/Contact";
import References from "./pages/References";
import PhotoGallery from "./pages/PhotoGallery";
import Admin from "./pages/Admin";
import Login from "./pages/Login";
import Register from "./components/Register/Register";
import NotFound from "./components/NotFound/NotFound";

import { AuthProvider } from "./auth/AuthContext";
import RequireAuth from "./auth/RequireAuth";
// import RequireRole from "./auth/RequireRole";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Layout />,
    errorElement: <NotFound />,
    children: [
      { index: true, element: <Home /> },
      { path: "allaboutme", element: <AllAboutMe /> },
      { path: "services", element: <Services /> },
      { path: "pricing", element: <Pricing /> },
      { path: "contact", element: <Contact /> },
      { path: "references", element: <References /> },
      { path: "photogallery", element: <PhotoGallery /> },
      {
        path: "admin",
        element: (
          <RequireAuth>
            {/* Or nest <RequireRole role="Admin"> ... */}
            <Admin />
          </RequireAuth>
        ),
      },
      { path: "*", element: <NotFound /> },
    ],
  },
  {
    element: <AuthLayout />,
    children: [
      { path: "/login", element: <Login /> },
      { path: "/register", element: <Register /> },
    ],
  },
]);

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <AuthProvider>
      <RouterProvider router={router} />
    </AuthProvider>
  </StrictMode>
);

6) Login Page (calls auth.login + smart redirect)

After success, update context and navigate back to the attempted URL, optional ?returnTo=, or default to /admin.

// src/pages/Login.jsx
import { useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";

const API_BASE = (import.meta?.env?.VITE_USERSERVICE_BASEURL) || "";

export default function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [showPassword, setShowPassword] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const navigate = useNavigate();
  const location = useLocation();
  const auth = useAuth();

  async function handleSubmit(e) {
    e.preventDefault();
    setError(""); setLoading(true);
    try {
      const url = `${API_BASE.replace(/\/$/, "")}/api/authentication/login`;
      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email, password })
      });
      if (!res.ok) {
        let msg = `Login failed (${res.status})`;
        try { const data = await res.json(); if (data?.message) msg = data.message; } catch {}
        throw new Error(msg);
      }
      const { token } = await res.json();
      if (!token) throw new Error("No token returned from server.");
      auth.login(token);

      const redirectTo =
        location.state?.from?.pathname ||
        new URLSearchParams(location.search).get("returnTo") ||
        "/admin";

      navigate(redirectTo, { replace: true });
    } catch (err) {
      setError(err.message || "Something went wrong.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="text-content">
      <div style={{ maxWidth: 420, margin: "2rem auto" }}>
        <h1 style={{ marginBottom: "1rem" }}>Login</h1>
        <form onSubmit={handleSubmit}>
          <label style={{ display: "block", marginBottom: 8 }}>Email
            <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
                   autoComplete="username" required style={{ width:"100%", padding:10, marginTop:4 }} />
          </label>

          <label style={{ display: "block", marginBottom: 8 }}>Password
            <div style={{ display: "flex", gap: 8 }}>
              <input type={showPassword ? "text" : "password"} value={password}
                     onChange={(e) => setPassword(e.target.value)}
                     autoComplete="current-password" required style={{ flex:1, padding:10 }} />
              <button type="button" onClick={() => setShowPassword((s) => !s)}>
                {showPassword ? "Hide" : "Show"}
              </button>
            </div>
          </label>

          {error && (
            <div role="alert" style={{ background:"#fee", border:"1px solid #f99", padding:"8px 12px", marginTop:8 }}>
              {error}
            </div>
          )}

          <button type="submit" disabled={loading} style={{ width:100, padding:12, marginTop:12 }}>
            {loading ? "Signing in…" : "Login"}
          </button>
        </form>
      </div>
    </div>
  );
}

7) Example Admin + Forbidden Pages

Only renders when authenticated (and optionally when role is satisfied).

// src/pages/Admin.jsx
import { useAuth } from "../auth/AuthContext";
export default function Admin() {
  const { logout } = useAuth();
  return (
    <div className="text-content">
      <h2>Admin Dashboard</h2>
      <p>Only visible when logged in.</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

// src/pages/Forbidden.jsx
export default function Forbidden() {
  return (
    <div className="text-content">
      <h2>403 – Forbidden</h2>
      <p>You don’t have permission to view this page.</p>
    </div>
  );
}

8) Calling Your API with the JWT

Attach the token in the Authorization header for protected endpoints.

// example: src/lib/api.js
import { getToken } from "../auth/token";

export async function apiGet(path) {
  const token = getToken();
  const res = await fetch(path, {
    headers: token ? { Authorization: `Bearer ${token}` } : {}
  });
  if (!res.ok) throw new Error(`API error ${res.status}`);
  return res.json();
}

9) Linking to Login with an Explicit Return URL

Sometimes you want a button or menu link to force login and then bounce back to a specific page.

<!-- Example link that goes to login and returns to /admin/reports afterwards -->
<a href="/login?returnTo=/admin/reports">Admin Reports</a>

10) Troubleshooting Checklist

Common issues and quick fixes.

- "Cannot destructure 'isAuthenticated'... useAuth() is null":
  * Ensure <AuthProvider> wraps <RouterProvider> in src/main.jsx.
  * Ensure every consumer imports useAuth from the SAME module path.
  * Do not create a second AuthContext.

- Login succeeds but not redirected:
  * Call auth.login(token) (not just localStorage.setItem).
  * Check your redirect chain: state.from → ?returnTo → "/admin".
  * Verify token has not already expired (exp in seconds).

- Role checks fail:
  * Inspect decoded JWT; confirm claim names and values.
  * Update hasRole() to include your claim shape.

- 401 from API:
  * Send Authorization: Bearer <token>.
  * Verify backend validates signature, issuer/audience, expiry, roles.

11) Production Security Essentials

Client guards are UX only; the server must enforce real security.

- Always validate JWTs on the server (signature, exp, iss, aud, roles/claims).
- Prefer HttpOnly, Secure, SameSite cookies for tokens (mitigates XSS). If using localStorage, be meticulous about XSS.
- Use HTTPS everywhere; never send tokens over HTTP.
- Rotate secrets/signing keys; set short token lifetimes and use refresh tokens if needed.
- Log auth failures and suspicious activity with correlation IDs.
Summary: Store the JWT in a centralized AuthContext, protect routes with <RequireAuth> (and roles if needed), and always enforce authorization on the API. After login, redirect back to the intended page or go to /admin by default.