Skip to content
Home
React Authentication: JWT, OAuth, and Session Management

React Authentication: JWT, OAuth, and Session Management

React React 8 min read 1512 words Beginner ExcellentWiki Editorial Team

Authentication is one of the most critical and complex features in any React application. Getting it wrong exposes user data and creates security vulnerabilities. This guide covers JWT-based authentication, OAuth 2.0 integration, session management patterns, and the security considerations every React developer should understand.

JWT Authentication in React

JSON Web Tokens (JWT) are the most common authentication mechanism in React applications. A JWT is a self-contained token that encodes a user’s identity and claims, signed by the server. The React application receives the token after login and includes it in subsequent API requests.

Login Flow

async function login(email, password) {
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
  });
  const { token } = await response.json();
  localStorage.setItem('token', token);
  return token;
---

Storing Tokens Safely

The debate between localStorage and cookies for token storage is ongoing. Auth0’s documentation recommends using HttpOnly cookies for production applications because they are not accessible to JavaScript, preventing XSS attacks. localStorage is simpler but vulnerable to XSS — if an attacker injects script, they can read all tokens.

For single-page applications, the common pattern is:

  • localStorage/sessionStorage: Simple, but XSS-vulnerable
  • HttpOnly cookies: More secure, requires CSRF protection
  • In-memory storage: Most secure (not persisted), but lost on page refresh

The React documentation does not prescribe a storage mechanism — it depends on your security requirements and backend architecture.

Sending Tokens with Requests

function apiClient() {
  const token = localStorage.getItem('token');
  return {
    get: (url) => fetch(url, {
      headers: { Authorization: `Bearer ${token}` },
    }),
  };
---

OAuth 2.0 Integration

OAuth 2.0 allows users to authenticate with third-party providers (Google, GitHub, Facebook) without sharing their password. The OAuth 2.0 authorization code flow (with PKCE for SPAs) is the recommended approach.

OAuth Authorization Code Flow with PKCE

For React applications, the authorization code flow with Proof Key for Code Exchange (PKCE) is the OAuth 2.0 best practice. The application generates a cryptographically random code_verifier and its SHA-256 hash (code_challenge). After the user authorizes, the server exchanges the authorization code for tokens.

async function loginWithGoogle() {
  const codeVerifier = generateRandomString();
  const codeChallenge = await sha256(codeVerifier);
  localStorage.setItem('code_verifier', codeVerifier);
  window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
---

Using Auth0

Auth0 simplifies OAuth integration by handling the flow server-side:

import { useAuth0 } from '@auth0/auth0-react';

function LoginButton() {
  const { loginWithRedirect, isAuthenticated, user } = useAuth0();
  return isAuthenticated ? <p>Welcome {user.name}</p> : <button onClick={loginWithRedirect}>Log In</button>;
---

Auth0 handles token storage, refresh, and session management. The Auth0 React SDK integrates with React Router for protected routes.

Using Firebase Authentication

Firebase Auth provides a complete authentication solution with multiple providers:

import { signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
import { auth } from './firebase';

function GoogleSignIn() {
  return <button onClick={() => signInWithPopup(auth, new GoogleAuthProvider())}>Sign In with Google</button>;
---

Firebase Auth handles token refresh automatically and integrates with Firebase’s other services (Firestore, Cloud Functions).

Route Protection

Protected routes redirect unauthenticated users to the login page:

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  return user ? children : <Navigate to="/login" replace />;
---

<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />

The replace prop prevents the user from navigating “back” to the login page after authentication. More sophisticated implementations check token expiration and try silent refresh before redirecting.

Session Management

Token Refresh

JWT access tokens typically expire within 15-60 minutes. A longer-lived refresh token (stored securely) obtains new access tokens without requiring the user to log in again:

async function refreshAccessToken() {
  const response = await fetch('/api/auth/refresh', {
    credentials: 'include', // Send HttpOnly cookie
  });
  if (response.ok) {
    const { token } = await response.json();
    return token;
  }
  throw new Error('Session expired');
---

Authorization Headers and Axios Interceptors

For applications using Axios, interceptors provide a clean way to attach authorization tokens to every outgoing request and handle 401 responses globally:

import axios from 'axios';

const api = axios.create({ baseURL: process.env.REACT_APP_API_URL });

api.interceptors.request.use((config) => {
  const token = getToken();
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
---);

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refreshed = await refreshToken();
      if (refreshed) return api(error.config); // Retry original request
      logout();
    }
    return Promise.reject(error);
  }
);

This pattern centralizes authentication logic and prevents every API call from needing manual token handling. The Axios interceptor can also queue failed requests during token refresh to avoid multiple simultaneous refresh attempts.

Silent Authentication

Auth0’s checkSession() and Firebase Auth’s onAuthStateChanged provide silent authentication — checking the user’s session on page load without redirecting. This creates a seamless experience where returning users are automatically recognized.

Logout

async function logout() {
  await fetch('/api/auth/logout', { method: 'POST' });
  localStorage.removeItem('token');
  setUser(null);
  navigate('/login');
---

Proper logout invalidates the session on the server (not just the client) to prevent token reuse.

Role-Based Access Control

Beyond authentication, most applications need authorization — determining what authenticated users can do. Role-Based Access Control (RBAC) maps naturally to React components:

function AdminButton({ roles, children }) {
  const { user } = useAuth();
  if (!user || !user.roles.some(r => roles.includes(r))) return null;
  return children;
---

// Usage
<AdminButton roles={['admin', 'superadmin']}>
  <button onClick={deleteUser}>Delete User</button>
</AdminButton>

For complex authorization, the React community has adopted access control libraries like casl (CanJS authorization library) and @casl/react integration. CASL allows defining abilities as a JSON object and checking them declaratively in components.

import { Ability } from '@casl/ability';
const ability = new Ability([
  { action: 'delete', subject: 'Post', conditions: { authorId: user.id } },
]);

Server-Side Authentication

When using Next.js or Remix, authentication often happens on the server before the page renders. Server Components can read session cookies and redirect unauthenticated users before any HTML is sent:

export default async function DashboardPage() {
  const session = await getSession();
  if (!session) redirect('/login');
  return <Dashboard user={session.user} />;
---

This pattern, documented in the Next.js authentication guide, provides a better security model because protected data is never sent to the client until the server validates the session.

Social Login and Provider Integration

Social login reduces friction and improves conversion rates. OAuth providers return user profile information that can pre-fill registration forms:

const handleGoogleLogin = async () => {
  const provider = new GoogleAuthProvider();
  const result = await signInWithPopup(auth, provider);
  const user = result.user;
  // User is authenticated with Google - create/update local user record
  await createUserProfile(user.uid, user.displayName, user.email);
---;

When implementing social login, consider account merging — what happens when a user signs in with Google, then later with email/password? The Auth0 documentation provides patterns for account linking that handle these edge cases.

For enterprise applications, SAML and OIDC providers (Okta, Azure AD, OneLogin) are common. Auth0 and Firebase support these enterprise providers, though they typically require paid plans.

Multi-Factor Authentication

MFA adds an additional layer of security beyond passwords. Common implementations include:

  • TOTP (Time-Based One-Time Password) via authenticator apps
  • SMS or email verification codes
  • WebAuthn / Passkeys for passwordless biometric authentication

WebAuthn is increasingly recommended by security experts because it is phishing-resistant. The WebAuthn API is supported in modern browsers and can be integrated into React applications via libraries like @simplewebauthn/browser.

Security Best Practices

The OWASP Authentication Cheat Sheet provides comprehensive guidance. Key points for React developers:

  • Never store secrets in client code — API keys and client secrets belong on the server
  • Validate tokens on every request — the server, not the client, enforces authorization
  • Use HTTPS everywhere — prevents token interception
  • Implement rate limiting — prevents brute force attacks
  • Use CSRF tokens — when using cookie-based authentication
  • Scrutinize redirect URIs — open redirectors enable phishing

XSS Protection

React escapes values in JSX by default, preventing most XSS attacks. However, dangerouslySetInnerHTML and direct DOM manipulation bypass this protection. The React documentation warns against these patterns unless absolutely necessary.

FAQ

Should I use localStorage or cookies for JWT storage?

Cookies with HttpOnly and Secure flags are more secure because JavaScript cannot access them. localStorage is simpler but vulnerable to XSS. For production applications handling sensitive data, use HttpOnly cookies with CSRF protection.

What is the difference between authentication and authorization?

Authentication verifies who the user is (login). Authorization determines what the user can access (permissions). React Router’s protected routes handle authorization at the UI level, but server-side authorization is essential for security.

How do I handle token expiration?

Implement a token refresh flow with Axios interceptors or fetch wrappers. When a 401 response arrives, try refreshing the token. If the refresh fails, redirect to login. Libraries like Auth0 and Firebase Auth handle this automatically.

Is OAuth 2.0 necessary for my React app?

OAuth 2.0 is necessary only if you need third-party login providers (Google, GitHub, etc.). For email/password authentication, JWT with a simple login API is sufficient.

Conclusion

Authentication in React involves understanding token storage, OAuth flows, route protection, and security best practices. Start with a simple JWT flow for email/password authentication and add OAuth providers as needed. Auth0 and Firebase Auth provide managed services that reduce implementation complexity. The OWASP Authentication Cheat Sheet and the Auth0 documentation are essential references. For integrating auth with routing, see React Router Guide, and for secure API communication patterns, see React Guide.

For a comprehensive overview, read our article on Next Js Guide.

For a comprehensive overview, read our article on React Career.

Section: React 1512 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top