OAuth React Integration: Implementing Secure Authentication with OAuth 2.0 and PKCE

Implement OAuth 2.0 in React applications using the Authorization Code Grant with PKCE flow, storing tokens in memory via React Context and securing routes with context-aware wrapper components.

OAuth React integration enables secure delegation of authentication to external identity providers like Google, GitHub, or Auth0. The facebook/react repository provides the essential building blocks—useState, useEffect, and useContext hooks implemented in packages/react/src/ReactHooks.js and packages/react/src/ReactContext.js—to manage authentication state declaratively. This guide demonstrates how to wire these core APIs into a complete PKCE-based OAuth implementation.

Why Use Authorization Code Grant with PKCE for OAuth React Integration

Single-page applications (SPAs) are public clients that cannot securely store client secrets. The Authorization Code Grant with PKCE (Proof-Key for Code Exchange) mitigates authorization code interception attacks by requiring a cryptographically random verifier that never leaves the browser.

This flow aligns with React’s declarative model: the authentication state transitions from unauthenticatedredirectingauthenticatingauthenticated through predictable state changes managed by React hooks.

Core React APIs for Authentication State Management

The facebook/react source code exposes stable APIs that form the foundation of any OAuth React integration.

Managing Authentication State with useState and useContext

According to packages/react/src/ReactHooks.js, useState and useContext are the primary hooks for holding authentication tokens and sharing them across the component tree. The createContext function defined in packages/react/src/ReactContext.js produces the context object consumed by useContext.

// packages/react/src/ReactContext.js
export function createContext(defaultValue) {
  const context = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    // ... internal fields
  };
  return context;
}

Handling Side Effects with useEffect

The useEffect hook, also implemented in packages/react/src/ReactHooks.js, manages the OAuth callback handling. When the identity provider redirects back to your application, useEffect parses the authorization code and triggers the token exchange without blocking the UI render.

Step-by-Step OAuth React Integration Implementation

The following implementation uses only React core APIs and the Web Crypto API available in modern browsers. No external OAuth libraries are required.

Step 1: Generate PKCE Verifier and Challenge

Create src/auth/pkce.js to generate the cryptographically random verifier and its SHA-256 challenge:

// Generate a random string and its SHA‑256 challenge (PKCE)
export async function generatePKCE() {
  const verifier = crypto.getRandomValues(new Uint8Array(32))
    .reduce((s, b) => s + b.toString(16).padStart(2, '0'), '');
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await crypto.subtle.digest('SHA-256', data);
  const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
  return { verifier, challenge };
}

Step 2: Configure OAuth Settings

Create src/auth/authConfig.js to centralize your identity provider endpoints:

export const authConfig = {
  clientId: 'YOUR_CLIENT_ID',                // ← register with IdP
  authorizationEndpoint: 'https://example.com/oauth/authorize',
  tokenEndpoint: 'https://example.com/oauth/token',
  redirectUri: `${window.location.origin}/auth/callback`,
  scopes: ['openid', 'profile', 'email'],
};

Step 3: Create the AuthProvider Component

Implement src/auth/AuthProvider.jsx using createContext and useState to manage the authentication lifecycle:

import React, { createContext, useState, useEffect } from 'react';
import jwt_decode from 'jwt-decode';
import { authConfig } from './authConfig';
import { generatePKCE } from './pkce';

export const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [accessToken, setAccessToken] = useState(null);

  // Called after IdP redirects back to our /auth/callback route
  const handleCallback = async (code, storedVerifier) => {
    const body = new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: authConfig.clientId,
      redirect_uri: authConfig.redirectUri,
      code,
      code_verifier: storedVerifier,
    });

    const resp = await fetch(authConfig.tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body,
    });

    const data = await resp.json(); // { access_token, id_token, expires_in, … }
    setAccessToken(data.access_token);
    const decoded = jwt_decode(data.id_token);
    setUser(decoded);
  };

  // Utility to start the login flow
  const login = async () => {
    const { verifier, challenge } = await generatePKCE();
    sessionStorage.setItem('pkce_verifier', verifier);

    const params = new URLSearchParams({
      client_id: authConfig.clientId,
      response_type: 'code',
      redirect_uri: authConfig.redirectUri,
      scope: authConfig.scopes.join(' '),
      code_challenge: challenge,
      code_challenge_method: 'S256',
    });

    window.location = `${authConfig.authorizationEndpoint}?${params}`;
  };

  const logout = () => {
    setUser(null);
    setAccessToken(null);
    sessionStorage.removeItem('pkce_verifier');
  };

  // Expose context value
  const value = { user, accessToken, login, logout, handleCallback };
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

Step 4: Handle the OAuth Callback

Create src/pages/AuthCallback.jsx to process the authorization code when the identity provider redirects back:

import React, { useEffect, useContext } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { AuthContext } from '../auth/AuthProvider';

export default function AuthCallback() {
  const { handleCallback } = useContext(AuthContext);
  const navigate = useNavigate();
  const query = new URLSearchParams(useLocation().search);
  const code = query.get('code');

  useEffect(() => {
    const verifier = sessionStorage.getItem('pkce_verifier');
    if (code && verifier) {
      handleCallback(code, verifier).then(() => navigate('/'));
    }
  }, [code, handleCallback, navigate]);

  return <p>Finishing login…</p>;
}

Step 5: Protect Routes with RequireAuth

Wire everything together in src/App.jsx using React.lazy (defined in packages/react/src/ReactLazy.js) for code-splitting and React.memo (from packages/react/src/ReactMemo.js) for performance optimization:

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider, AuthContext } from './auth/AuthProvider';
import AuthCallback from './pages/AuthCallback';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';

function RequireAuth({ children }) {
  const { user } = React.useContext(AuthContext);
  return user ? children : <p>Please <button onClick={() => React.useContext(AuthContext).login()}>log in</button>.</p>;
}

export default function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/auth/callback" element={<AuthCallback />} />
          <Route
            path="/dashboard"
            element={
              <RequireAuth>
                <Dashboard />
              </RequireAuth>
            }
          />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

Security Best Practices for OAuth React Integration

When implementing OAuth React integration, never store access tokens or refresh tokens in localStorage or sessionStorage unless you have implemented aggressive XSS mitigations. Instead, keep tokens in memory using React state, or proxy token requests through a backend that sets httpOnly cookies.

The ReactTransitionType definitions in packages/react/src/ReactTransitionType.js enable smooth UI transitions between authenticated and unauthenticated states without jank. Use React.memo (from packages/react/src/ReactMemo.js) to prevent unnecessary re-renders of authentication-heavy components, and React.lazy (from packages/react/src/ReactLazy.js) to code-split authentication routes.

Summary

  • OAuth React integration relies on the Authorization Code Grant with PKCE flow to securely authenticate users in browser-based SPAs.
  • React core APIs from packages/react/src/ReactHooks.js and packages/react/src/ReactContext.js—specifically useState, useEffect, and createContext—provide the state management foundation.
  • Store the PKCE verifier in sessionStorage temporarily, but keep access tokens in React Context memory to mitigate XSS risks.
  • Protect routes using a context-aware wrapper component that checks authentication state before rendering sensitive UI.
  • Use React.lazy and React.memo from packages/react/src/ReactLazy.js and packages/react/src/ReactMemo.js to optimize performance of authentication flows.

Frequently Asked Questions

What is PKCE and why is it required for OAuth React integration?

PKCE (Proof-Key for Code Exchange) is an extension to the OAuth 2.0 Authorization Code flow that prevents authorization code interception attacks. In OAuth React integration scenarios, the application runs as a public client in the browser where a client secret cannot be securely stored. PKCE replaces the client secret with a dynamically generated cryptographically random verifier, ensuring that even if an attacker intercepts the authorization code, they cannot exchange it for tokens without the original verifier stored in sessionStorage.

How should I store access tokens securely in a React application?

Store access tokens in React Context memory using useState rather than localStorage or sessionStorage. According to the implementation in packages/react/src/ReactHooks.js, useState persists values only for the component lifecycle, making tokens inaccessible to XSS attacks that target persistent storage. If you need persistent sessions, implement a minimal backend proxy that exchanges tokens and sets httpOnly cookies, which JavaScript cannot read, rather than exposing tokens to the browser's storage APIs.

Can I implement OAuth React integration without a backend server?

Yes, you can implement a complete OAuth React integration using only client-side JavaScript with the PKCE flow. The browser's Web Crypto API generates the PKCE challenge, and the React application handles the authorization code exchange directly with the identity provider's token endpoint. However, without a backend, you cannot use httpOnly cookies for refresh tokens, limiting you to short-lived access tokens or requiring re-authentication when tokens expire. For long-lived sessions, a lightweight backend remains recommended.

How do I handle token refresh in a React OAuth implementation?

Handle token refresh by storing a refresh token (if provided by your identity provider for public clients) in memory and implementing a refreshAccessToken method within your AuthProvider component. Use useEffect in packages/react/src/ReactHooks.js to schedule refresh calls before the access token expires, or intercept API calls to trigger refreshes when receiving 401 responses. If your identity provider does not issue refresh tokens to SPAs, implement a "silent login" by redirecting to the authorization endpoint with prompt=none in a hidden iframe or by using the identity provider's session cookie to obtain new tokens without user interaction.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →