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

> Learn how to implement OAuth 2.0 and PKCE for secure user authentication in your React app. Secure routes and manage tokens effectively with clear examples and best practices.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: how-to-guide
- Published: 2026-02-16

---

**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`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) and [`packages/react/src/ReactContext.js`](https://github.com/facebook/react/blob/main/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 `unauthenticated` → `redirecting` → `authenticating` → `authenticated` 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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/packages/react/src/ReactContext.js) produces the context object consumed by `useContext`.

```javascript
// 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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/src/auth/pkce.js) to generate the cryptographically random verifier and its SHA-256 challenge:

```javascript
// 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`](https://github.com/facebook/react/blob/main/src/auth/authConfig.js) to centralize your identity provider endpoints:

```javascript
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`](https://github.com/facebook/react/blob/main/src/auth/AuthProvider.jsx) using `createContext` and `useState` to manage the authentication lifecycle:

```jsx
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`](https://github.com/facebook/react/blob/main/src/pages/AuthCallback.jsx) to process the authorization code when the identity provider redirects back:

```jsx
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`](https://github.com/facebook/react/blob/main/src/App.jsx) using `React.lazy` (defined in [`packages/react/src/ReactLazy.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactLazy.js)) for code-splitting and `React.memo` (from [`packages/react/src/ReactMemo.js`](https://github.com/facebook/react/blob/main/packages/react/src/ReactMemo.js)) for performance optimization:

```jsx
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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/packages/react/src/ReactMemo.js)) to prevent unnecessary re-renders of authentication-heavy components, and `React.lazy` (from [`packages/react/src/ReactLazy.js`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js) and [`packages/react/src/ReactContext.js`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/packages/react/src/ReactLazy.js) and [`packages/react/src/ReactMemo.js`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/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`](https://github.com/facebook/react/blob/main/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.