Single Page Applications (SPAs) like React apps face unique challenges when handling OAuth 2.0 flows due to security concerns with exposing tokens in the browser. The Backend-for-Frontend (BFF) pattern provides an elegant solution by shifting sensitive OAuth token handling to a trusted backend while keeping the frontend lightweight.

This article walks you through implementing the OAuth 2.0 Authorization Code Flow with PKCE using React as the frontend and a Node.js/Express backend acting as the BFF.


Why Use BFF for OAuth 2.0 in React SPA?

  • SPAs are public clients; they cannot securely store client secrets.
  • Handling tokens only on the backend reduces XSS and CSRF risks.
  • BFF acts as a secure token proxy for API calls.
  • Enables easier session management and refresh token rotation.

Overview of the Architecture

User Browser (React SPA)
        |
(1) Login redirect         (2) OAuth server login and consent
        |                          |
        v                          v
Backend-for-Frontend (Node.js/Express)
        |
(3) Token exchange and storage
        |
(4) API requests with access token

Step 1: React SPA - Initiate Login

In React, you trigger the login by requesting the BFF to redirect:

// React snippet to call BFF login
function login() {
  window.location.href = 'http://localhost:3000/login';
}

The frontend does not handle tokens directly — it just redirects users.


Step 2: BFF - Redirect to OAuth Server

Backend prepares the authorization URL with PKCE parameters:

// Express /login route to redirect user to OAuth server
const crypto = require('crypto');
const querystring = require('querystring');

app.get('/login', (req, res) => {
  // Generate code_verifier and code_challenge for PKCE
  const codeVerifier = crypto.randomBytes(32).toString('hex');
  const codeChallenge = base64urlEncode(sha256(codeVerifier));

  // Store codeVerifier in session or secure cookie
  req.session.codeVerifier = codeVerifier;

  const params = {
    response_type: 'code',
    client_id: process.env.CLIENT_ID,
    redirect_uri: 'http://localhost:3000/callback',
    scope: 'openid profile email',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state: crypto.randomBytes(16).toString('hex')
  };

  const authUrl = `https://oauth-server.com/authorize?${querystring.stringify(params)}`;
  res.redirect(authUrl);
});

Step 3: BFF - Handle Callback and Exchange Code

app.get('/callback', async (req, res) => {
  const { code } = req.query;
  const codeVerifier = req.session.codeVerifier;

  try {
    const tokenResponse = await axios.post('https://oauth-server.com/token',
      querystring.stringify({
        grant_type: 'authorization_code',
        code,
        redirect_uri: 'http://localhost:3000/callback',
        client_id: process.env.CLIENT_ID,
        code_verifier: codeVerifier
      }),
      { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
    );

    // Store tokens securely in session
    req.session.tokens = tokenResponse.data;

    // Redirect to frontend SPA
    res.redirect('http://localhost:3001'); // React app URL
  } catch (error) {
    res.status(500).send('Token exchange failed');
  }
});

Step 4: React SPA Fetches Data via BFF

React SPA calls the BFF API to access protected resources without seeing tokens:

// React fetch example
async function fetchProfile() {
  const response = await fetch('http://localhost:3000/api/profile', {
    credentials: 'include'
  });
  const profile = await response.json();
  console.log(profile);
}

Step 5: BFF Proxy API Requests with Access Token

app.get('/api/profile', async (req, res) => {
  if (!req.session.tokens) return res.status(401).send('Unauthorized');

  try {
    const userInfo = await axios.get('https://oauth-server.com/userinfo', {
      headers: { Authorization: `Bearer ${req.session.tokens.access_token}` }
    });
    res.json(userInfo.data);
  } catch (error) {
    res.status(500).send('Failed to fetch user info');
  }
});

Security Considerations

  • Use HTTPS to protect data in transit.
  • Secure cookies and proper session handling on the backend.
  • Rotate tokens and implement refresh token logic on the BFF.
  • Protect endpoints from CSRF attacks.

Real-World Use Cases

The BFF pattern is increasingly popular for SPA apps that require OAuth 2.0 login flows without exposing tokens to the browser — used by enterprises with ForgeRock Identity Cloud, Auth0, and others.

👉 Related:

Understanding the Authorization Code Flow with PKCE in OAuth 2.0

How to Implement the OAuth 2.0 Authorization Code Flow in Java


Conclusion

The Backend-for-Frontend pattern enhances security and scalability for OAuth 2.0 in React SPAs. By delegating token handling to a trusted backend, you reduce attack surfaces and simplify token lifecycle management.

💡 What else can you explore?

  • How to implement refresh token rotation securely in the BFF?
  • What’s new in OAuth 2.1 that impacts SPA security models?
  • Can BFF be combined with decentralized identity solutions?

Stay tuned for deeper dives into these topics soon.