A critical security vulnerability where OAuth access tokens, refresh tokens, and other authentication credentials are exposed in client-side JavaScript code, browser storage, or network requests. This allows attackers to intercept tokens through browser debugging tools, XSS attacks, or man-in-the-middle attacks, enabling unauthorized access to user accounts and protected resources.
Please wait while we complete your authentication.
);
}
// Redirect to profile after successful authentication
return ;
};
export default App;
💡 Why This Fix Works
The vulnerable React SPA stores OAuth tokens in localStorage, uses implicit flow, logs tokens to console, and exposes debug methods. The secure version implements Authorization Code flow with PKCE, uses backend token proxy, httpOnly cookies for sessions, and eliminates client-side token handling entirely.
Why it happens
Single Page Applications (SPAs) often store OAuth access tokens in browser localStorage or sessionStorage for convenience. This makes tokens accessible to any JavaScript code running on the page, including malicious scripts injected through XSS attacks. Unlike httpOnly cookies, localStorage is fully accessible to JavaScript, making it a vulnerable storage mechanism for sensitive tokens.
Root causes
Access Tokens Stored in Browser Local Storage
Single Page Applications (SPAs) often store OAuth access tokens in browser localStorage or sessionStorage for convenience. This makes tokens accessible to any JavaScript code running on the page, including malicious scripts injected through XSS attacks. Unlike httpOnly cookies, localStorage is fully accessible to JavaScript, making it a vulnerable storage mechanism for sensitive tokens.
OAuth Tokens Exposed in URL Parameters and Console Logs
OAuth implicit flow implementations often receive tokens as URL fragments or query parameters, which can be logged in browser history, server access logs, or analytics tools. Additionally, developers frequently log OAuth responses during debugging, inadvertently exposing tokens in console logs that may be visible in production environments or browser developer tools.
Preview example – JAVASCRIPT
// VULNERABLE: OAuth implicit flow with token exposure
class OAuthHandler {
constructor() {
this.clientId = 'your_oauth_client_id';
this.redirectUri = 'https://yourapp.com/callback';
}
initiateOAuthFlow() {
const authUrl = 'https://oauth.provider.com/auth?' +
`client_id=${this.clientId}&` +
'response_type=token&' + // VULNERABLE: Implicit flow
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` +
'scope=read write';
window.location.href = authUrl;
}
handleOAuthCallback() {
// VULNERABLE: Tokens in URL fragment
const urlParams = new URLSearchParams(window.location.hash.substring(1));
const accessToken = urlParams.get('access_token');
const refreshToken = urlParams.get('refresh_token');
const tokenType = urlParams.get('token_type');
// VULNERABLE: Logging tokens to console
console.log('OAuth callback received:', {
access_token: accessToken, // Exposed in browser console!
refresh_token: refreshToken,
token_type: tokenType,
url: window.location.href // Full URL with tokens in logs!
});
// VULNERABLE: Token remains in browser history
// URL: https://yourapp.com/callback#access_token=ya29.a0AfH6SMC...
// Store tokens (also vulnerable)
localStorage.setItem('oauth_access_token', accessToken);
localStorage.setItem('oauth_refresh_token', refreshToken);
// VULNERABLE: Analytics tracking with tokens in URL
if (window.gtag) {
gtag('config', 'GA_TRACKING_ID', {
page_location: window.location.href // Sends tokens to Google Analytics!
});
}
return { accessToken, refreshToken };
}
}
Client-Side Token Refresh and Management
JavaScript applications that handle OAuth token refresh on the client-side often expose refresh tokens and the refresh process to potential attackers. This includes storing refresh tokens in browser storage, making refresh requests from client-side code, and handling token rotation logic where sensitive credentials can be intercepted or manipulated.
Use Authorization Code Flow with PKCE and Backend Token Handling
Replace OAuth implicit flow with Authorization Code flow with PKCE (Proof Key for Code Exchange). Handle token exchange on the backend server and use secure, httpOnly cookies for session management. This keeps access tokens away from client-side JavaScript and prevents exposure through XSS attacks or browser developer tools.
View implementation – JAVASCRIPT
// SECURE: OAuth Authorization Code flow with PKCE
class SecureOAuthService {
constructor() {
this.authEndpoint = 'https://oauth.provider.com/auth';
this.clientId = 'your_public_client_id'; // Public client ID only
this.redirectUri = 'https://yourapp.com/callback';
// No client secret in frontend code!
}
// Generate PKCE challenge
async generatePKCEChallenge() {
const codeVerifier = this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(codeVerifier);
// Store code verifier securely (session storage is okay for PKCE)
sessionStorage.setItem('pkce_code_verifier', codeVerifier);
return { codeVerifier, codeChallenge };
}
generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async initiateSecureOAuthFlow() {
const { codeChallenge } = await this.generatePKCEChallenge();
const state = crypto.getRandomValues(new Uint8Array(16));
const stateString = btoa(String.fromCharCode.apply(null, state));
// Store state for CSRF protection
sessionStorage.setItem('oauth_state', stateString);
const authUrl = new URL(this.authEndpoint);
authUrl.searchParams.set('client_id', this.clientId);
authUrl.searchParams.set('response_type', 'code'); // Authorization code, not token
authUrl.searchParams.set('redirect_uri', this.redirectUri);
authUrl.searchParams.set('scope', 'read write');
authUrl.searchParams.set('state', stateString);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
window.location.href = authUrl.toString();
}
async handleOAuthCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
// Validate state parameter for CSRF protection
const storedState = sessionStorage.getItem('oauth_state');
if (state !== storedState) {
throw new Error('Invalid OAuth state parameter - possible CSRF attack');
}
if (error) {
throw new Error(`OAuth error: ${error}`);
}
if (!code) {
throw new Error('No authorization code received');
}
// SECURE: Send authorization code to backend for token exchange
const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
const response = await fetch('/api/auth/oauth/callback', {
method: 'POST',
credentials: 'include', // Include cookies
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
code_verifier: codeVerifier,
redirect_uri: this.redirectUri
})
});
if (!response.ok) {
throw new Error('Token exchange failed');
}
// Clean up temporary storage
sessionStorage.removeItem('pkce_code_verifier');
sessionStorage.removeItem('oauth_state');
// Backend sets httpOnly cookie with session info
// No tokens stored in frontend!
return { success: true };
}
async makeAuthenticatedRequest(url, options = {}) {
// SECURE: No token handling in frontend - uses httpOnly cookies
return fetch(url, {
...options,
credentials: 'include', // Send httpOnly cookies
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
async logout() {
// SECURE: Logout through backend
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
// Redirect to login or home page
window.location.href = '/';
}
}
}
2
Implement Backend Proxy for OAuth Token Management
Create a backend proxy service that handles all OAuth token operations, including storage, refresh, and API calls. The frontend communicates with this proxy using session-based authentication, keeping OAuth tokens completely isolated from client-side code. This approach provides better security and centralized token management.
Use Secure Token Storage and Content Security Policy
If client-side token storage is absolutely necessary, implement additional security layers including encrypted storage, Content Security Policy (CSP) headers, and token binding. Use short-lived tokens with automatic refresh, implement token introspection, and add monitoring for suspicious token usage patterns.