React · Auth & Quality

Authentication

React itself does not authenticate anyone — your API does. The React side sends credentials, remembers who is signed in, restores that on reload and reacts when the session expires.

Advanced 7 min Lesson 32 of 42Examples target React 19 with Vite.
0/42 in React
On this page · 7 min · Advanced

Learning objectives#

  • Log in and out through an API
  • Choose where to keep the session (cookie vs token)
  • Restore the user on page load with a /me endpoint
  • Handle expired sessions (401) globally

Explanation#

Flow: the login form posts email and password to the API; the API verifies them and starts a session — usually by setting an httpOnly cookie, or by returning a JWT.

httpOnly cookies cannot be read by JavaScript, so an XSS bug cannot steal them. Send requests with credentials: "include" and protect POSTs against CSRF (SameSite cookies, anti-forgery tokens). Storing JWTs in localStorage is common but readable by any injected script.

React keeps only the current user (id, name, roles) in an AuthProvider. On startup it calls GET /api/auth/me to restore the session after a reload.

While that check runs, render a loading state — otherwise protected pages flash the login screen.

If any request returns 401, clear the user and send them to the login page. Hiding UI is not security: the API must check permissions on every request.

Syntax#

JSX Illustrative
const res = await fetch('/api/auth/login', {
  method: 'POST',
  credentials: 'include',                  // send/receive the session cookie
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
});

Code example: An AuthProvider backed by an API#

JSXauth.jsx Runnable
import { createContext, useContext, useEffect, useState } from 'react';


const API = import.meta.env?.VITE_API_URL ?? '/api';
const AuthContext = createContext(null);


async function api(path, options = {}) {
  const res = await fetch(API + path, {
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });
  if (res.status === 401) return null;                  // not signed in
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  return res.status === 204 ? null : res.json();
}


export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [checking, setChecking] = useState(true);


  useEffect(() => {                                      // restore session on load
    api('/auth/me').then(setUser).catch(() => setUser(null)).finally(() => setChecking(false));
  }, []);


  async function login(email, password) {
    const me = await api('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) });
    if (!me) throw new Error('Wrong email or password');
    setUser(me);
  }


  async function logout() {
    await api('/auth/logout', { method: 'POST' });
    setUser(null);
  }


  return <AuthContext value={{ user, checking, login, logout }}>{children}</AuthContext>;
}


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


export function LoginForm() {
  const { user, checking, login, logout } = useAuth();
  const [error, setError] = useState('');


  if (checking) return <p>Checking your session…</p>;
  if (user) return <p>Signed in as {user.name} <button onClick={logout}>Log out</button></p>;


  async function handleSubmit(e) {
    e.preventDefault();
    const { email, password } = Object.fromEntries(new FormData(e.currentTarget));
    try { setError(''); await login(email, password); }
    catch (err) { setError(err.message); }
  }


  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" aria-label="Email" required />
      <input name="password" type="password" aria-label="Password" required />
      <button>Log in</button>
      {error && <p role="alert">{error}</p>}
    </form>
  );
}


export default function App() {
  return <AuthProvider><LoginForm /></AuthProvider>;
}
Expected output
On load the app shows “Checking your session…” while it calls /api/auth/me. If the cookie is valid it shows “Signed in as Asha” with a Log out button; otherwise the login form. Wrong credentials show “Wrong email or password”. Logging out calls the API and returns to the form.

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. api() sends cookies with every request and turns 401 into null (“not signed in”).
  2. The provider checks /auth/me once on startup and exposes checking while it runs.
  3. login posts credentials; the server sets the httpOnly session cookie and returns the user.
  4. logout tells the server to clear the cookie, then clears the user in React.
  5. LoginForm handles the three states: checking, signed in, signed out.

Real-world use case#

A React front end for an ASP.NET Core API uses cookie authentication: the API sets a secure httpOnly cookie, React restores the user from /api/auth/me, and an axios interceptor redirects to /login on any 401.

Common mistakes#

Trusting the client

Hiding the Admin menu is cosmetic. The API must check the session and roles on every request.

Forgetting credentials: "include"

Without it the browser does not send the session cookie to another origin, so every request looks anonymous.

Flash of the login page

Rendering the login form before /me answers makes signed-in users see it briefly. Show a checking state.

Best practices#

  • Prefer secure httpOnly cookies set by the server
  • Restore the session with /me on load
  • Handle 401 in one place
  • Never store passwords; keep tokens out of logs and URLs

Remember this#

API authenticates; React remembers who is signed in.

httpOnly cookie → JS cannot steal it.

Startup: call /me, show “checking…” until it answers.

Practice exercise#

Your task: Write a RoleGate component that renders its children only if useAuth().user.roles includes a given role, otherwise "You do not have access".

Need a hint?
const { user } = useAuth(); user?.roles?.includes(role).
Show solution
JSX Runnable
import { createContext, useContext } from 'react';


const AuthContext = createContext({ user: { name: 'Asha', roles: ['Customer'] } });
const useAuth = () => useContext(AuthContext);


function RoleGate({ role, children }) {
  const { user } = useAuth();
  return user?.roles?.includes(role) ? children : <p>You do not have access.</p>;
}


export default function AdminArea() {
  return <RoleGate role="Admin"><p>Admin tools</p></RoleGate>;   // → You do not have access.
}

Mini quiz#

Authentication quizMultiple choice Question 1 of 3

Why are httpOnly cookies safer than localStorage for session tokens?

Score so far: 0/0

Want more? Practise all React MCQs · React interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.