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.