OAuth Access Tokens Exposed in Client-Side JavaScript

High Risk Secrets Exposure
oauthjavascriptclient-sideaccess-tokensbrowser-securityauthenticationspafrontend

What it is

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.

// VULNERABLE: React SPA with OAuth tokens in localStorage // AuthContext.js - Vulnerable context with token exposure import React, { createContext, useContext, useState, useEffect } from 'react'; const AuthContext = createContext(); export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); // VULNERABLE: OAuth service with localStorage const oauthService = { clientId: 'your-spa-client-id', redirectUri: window.location.origin + '/callback', initiateLogin() { 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 profile'; window.location.href = authUrl; }, handleCallback() { // VULNERABLE: Tokens in URL hash const hash = window.location.hash.substring(1); const params = new URLSearchParams(hash); const accessToken = params.get('access_token'); const refreshToken = params.get('refresh_token'); const tokenType = params.get('token_type'); const expiresIn = params.get('expires_in'); if (accessToken) { // VULNERABLE: Store in localStorage localStorage.setItem('access_token', accessToken); localStorage.setItem('refresh_token', refreshToken || ''); localStorage.setItem('token_type', tokenType || 'Bearer'); localStorage.setItem('expires_at', Date.now() + (parseInt(expiresIn) * 1000)); // VULNERABLE: Console logging tokens console.log('OAuth tokens received:', { access_token: accessToken, // Visible in browser console! refresh_token: refreshToken, expires_in: expiresIn }); // Clear URL hash (but too late, already in browser history) window.location.hash = ''; return true; } return false; }, async getUserInfo() { const token = localStorage.getItem('access_token'); if (!token) return null; try { const response = await fetch('https://api.provider.com/user', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { return await response.json(); } } catch (error) { // VULNERABLE: Error logging with token console.error('User info fetch failed:', { error: error.message, token: token // Token exposed in error logs! }); } return null; }, async makeAPICall(endpoint, options = {}) { const token = localStorage.getItem('access_token'); const tokenType = localStorage.getItem('token_type'); if (!token) { throw new Error('No access token available'); } // Check if token is expired const expiresAt = localStorage.getItem('expires_at'); if (Date.now() >= parseInt(expiresAt)) { console.log('Token expired, attempting refresh...'); await this.refreshToken(); } return fetch(endpoint, { ...options, headers: { 'Authorization': `${tokenType} ${token}`, 'Content-Type': 'application/json', ...options.headers } }); }, async refreshToken() { const refreshToken = localStorage.getItem('refresh_token'); if (!refreshToken) { throw new Error('No refresh token available'); } try { // VULNERABLE: Refresh tokens from client-side const response = await fetch('https://oauth.provider.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: this.clientId // Some implementations might even include client_secret here! }) }); if (response.ok) { const tokenData = await response.json(); // VULNERABLE: Update localStorage with new tokens localStorage.setItem('access_token', tokenData.access_token); if (tokenData.refresh_token) { localStorage.setItem('refresh_token', tokenData.refresh_token); } localStorage.setItem('expires_at', Date.now() + (tokenData.expires_in * 1000)); console.log('Token refreshed:', tokenData); // Logged tokens! } else { throw new Error('Token refresh failed'); } } catch (error) { console.error('Token refresh error:', error); this.logout(); throw error; } }, logout() { // Clear tokens from localStorage localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); localStorage.removeItem('token_type'); localStorage.removeItem('expires_at'); setUser(null); window.location.href = '/'; }, // VULNERABLE: Debug method that exposes tokens getTokenDebugInfo() { return { access_token: localStorage.getItem('access_token'), refresh_token: localStorage.getItem('refresh_token'), expires_at: localStorage.getItem('expires_at') }; } }; useEffect(() => { // Check for OAuth callback if (window.location.hash.includes('access_token')) { if (oauthService.handleCallback()) { // Load user info oauthService.getUserInfo().then(userInfo => { setUser(userInfo); setIsLoading(false); }); } } else { // Check for existing token const token = localStorage.getItem('access_token'); if (token) { oauthService.getUserInfo().then(userInfo => { setUser(userInfo); setIsLoading(false); }); } else { setIsLoading(false); } } }, []); const login = () => oauthService.initiateLogin(); const logout = () => oauthService.logout(); const makeAPICall = (endpoint, options) => oauthService.makeAPICall(endpoint, options); return ( {children} ); }; // UserProfile.js - Component using vulnerable auth const UserProfile = () => { const { user, makeAPICall, debugTokens } = useAuth(); const [profile, setProfile] = useState(null); useEffect(() => { if (user) { makeAPICall('https://api.provider.com/profile') .then(response => response.json()) .then(data => setProfile(data)) .catch(error => { console.error('Profile fetch failed:', error); // VULNERABLE: Could log tokens in error details }); } }, [user]); // VULNERABLE: Debug function accessible to users const showTokens = () => { console.log('Current tokens:', debugTokens()); alert('Tokens logged to console'); }; return (

User Profile

{profile && (

Name: {profile.name}

Email: {profile.email}

{/* VULNERABLE: Debug button in production */}
)}
); };
// SECURE: React SPA with proper OAuth implementation // SecureAuthContext.js - Backend-proxied authentication import React, { createContext, useContext, useState, useEffect } from 'react'; const AuthContext = createContext(); export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }; export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); // SECURE: OAuth service using backend proxy const authService = { async initiateLogin() { try { // Generate PKCE parameters const { codeVerifier, codeChallenge } = await this.generatePKCE(); const state = this.generateState(); // Store PKCE verifier and state securely sessionStorage.setItem('pkce_code_verifier', codeVerifier); sessionStorage.setItem('oauth_state', state); // Build authorization URL const authUrl = new URL('https://oauth.provider.com/auth'); authUrl.searchParams.set('client_id', process.env.REACT_APP_OAUTH_CLIENT_ID); authUrl.searchParams.set('response_type', 'code'); // Authorization Code flow authUrl.searchParams.set('redirect_uri', `${window.location.origin}/callback`); authUrl.searchParams.set('scope', 'read write profile'); authUrl.searchParams.set('state', state); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); window.location.href = authUrl.toString(); } catch (error) { console.error('Login initiation failed:', error.message); throw new Error('Unable to initiate login'); } }, async handleCallback() { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const state = urlParams.get('state'); const error = urlParams.get('error'); if (error) { throw new Error(`OAuth error: ${error}`); } // Validate state parameter const storedState = sessionStorage.getItem('oauth_state'); if (state !== storedState) { throw new Error('Invalid OAuth state - possible CSRF attack'); } if (!code) { throw new Error('No authorization code received'); } try { // SECURE: Send 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', // Send cookies headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ code, code_verifier: codeVerifier, redirect_uri: `${window.location.origin}/callback` }) }); if (!response.ok) { throw new Error('Token exchange failed'); } const result = await response.json(); // Clean up temporary storage sessionStorage.removeItem('pkce_code_verifier'); sessionStorage.removeItem('oauth_state'); // Clear URL parameters window.history.replaceState({}, document.title, window.location.pathname); return result; } catch (error) { console.error('OAuth callback handling failed:', error.message); throw error; } }, async getCurrentUser() { try { // SECURE: Get user info through backend session const response = await fetch('/api/auth/user', { credentials: 'include' // httpOnly cookies }); if (response.ok) { return await response.json(); } else if (response.status === 401) { return null; // Not authenticated } else { throw new Error('Failed to fetch user info'); } } catch (error) { console.error('User info fetch failed:', error.message); return null; } }, async makeAPICall(endpoint, options = {}) { try { // SECURE: All API calls go through backend proxy const proxyEndpoint = `/api/proxy${endpoint.replace('https://api.provider.com', '')}`; const response = await fetch(proxyEndpoint, { ...options, credentials: 'include', // Include session cookies headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', // CSRF protection ...options.headers } }); if (response.status === 401) { // Session expired setIsAuthenticated(false); setUser(null); throw new Error('Authentication expired'); } return response; } catch (error) { console.error('API call failed:', error.message); throw error; } }, async logout() { try { // SECURE: Logout through backend await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); setUser(null); setIsAuthenticated(false); // Clear any remaining session storage sessionStorage.clear(); // Redirect to home window.location.href = '/'; } catch (error) { console.error('Logout failed:', error.message); // Force logout on client side even if backend call fails setUser(null); setIsAuthenticated(false); window.location.href = '/'; } }, async generatePKCE() { // Generate code verifier const codeVerifier = this.generateCodeVerifier(); // Generate code challenge const encoder = new TextEncoder(); const data = encoder.encode(codeVerifier); const digest = await crypto.subtle.digest('SHA-256', data); const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); return { codeVerifier, codeChallenge }; }, generateCodeVerifier() { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); }, generateState() { const array = new Uint8Array(16); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } }; // Check authentication status on mount useEffect(() => { const initAuth = async () => { try { // Handle OAuth callback if present if (window.location.search.includes('code=')) { await authService.handleCallback(); } // Check current authentication status const userInfo = await authService.getCurrentUser(); if (userInfo) { setUser(userInfo); setIsAuthenticated(true); } } catch (error) { console.error('Authentication initialization failed:', error.message); } finally { setIsLoading(false); } }; initAuth(); }, []); // Context value const contextValue = { user, isLoading, isAuthenticated, login: authService.initiateLogin, logout: authService.logout, makeAPICall: authService.makeAPICall // No debug methods exposed in production }; return ( {children} ); }; // SecureUserProfile.js - Component using secure authentication const SecureUserProfile = () => { const { user, isAuthenticated, makeAPICall } = useAuth(); const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { const fetchProfile = async () => { if (!isAuthenticated || !user) return; setLoading(true); setError(null); try { const response = await makeAPICall('/profile'); if (response.ok) { const profileData = await response.json(); setProfile(profileData); } else { throw new Error('Failed to fetch profile'); } } catch (err) { console.error('Profile fetch failed:', err.message); setError('Unable to load profile. Please try again later.'); } finally { setLoading(false); } }; fetchProfile(); }, [user, isAuthenticated, makeAPICall]); if (!isAuthenticated) { return (

Please log in to view your profile

); } if (loading) { return
Loading profile...
; } if (error) { return (

Error

{error}

); } return (

User Profile

{profile ? (

Name: {profile.name}

Email: {profile.email}

Member since: {new Date(profile.created_at).toLocaleDateString()}

{/* No debug functionality in secure implementation */}
) : (

No profile data available.

)}
); }; // App.js - Main application with secure routing const App = () => { return (
} /> } /> } />
); }; // ProtectedRoute.js - Route protection const ProtectedRoute = ({ children }) => { const { isAuthenticated, isLoading } = useAuth(); if (isLoading) { return
Loading...
; } if (!isAuthenticated) { return ; } return children; }; // CallbackHandler.js - OAuth callback handling const CallbackHandler = () => { const { isLoading } = useAuth(); if (isLoading) { return (

Processing login...

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.

Preview example – JAVASCRIPT
// VULNERABLE: Storing OAuth tokens in localStorage
class AuthService {
    constructor() {
        this.apiUrl = 'https://api.example.com';
    }
    
    async loginWithGoogle() {
        try {
            const response = await this.initiateGoogleOAuth();
            
            // VULNERABLE: Storing sensitive tokens in localStorage
            localStorage.setItem('access_token', response.access_token);
            localStorage.setItem('refresh_token', response.refresh_token);
            localStorage.setItem('id_token', response.id_token);
            localStorage.setItem('user_info', JSON.stringify(response.user));
            
            console.log('Login successful', response); // Also logs tokens!
            return response;
        } catch (error) {
            console.error('OAuth login failed:', error);
        }
    }
    
    getAccessToken() {
        // VULNERABLE: Retrieving token from localStorage
        return localStorage.getItem('access_token');
    }
    
    makeAuthenticatedRequest(url, options = {}) {
        const token = this.getAccessToken();
        return fetch(url, {
            ...options,
            headers: {
                'Authorization': `Bearer ${token}`,
                'Content-Type': 'application/json',
                ...options.headers
            }
        });
    }
}

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.

Preview example – JAVASCRIPT
// VULNERABLE: Client-side token refresh management
class TokenManager {
    constructor() {
        this.apiUrl = 'https://api.example.com';
        this.tokenEndpoint = 'https://oauth.provider.com/token';
        this.clientId = 'your_client_id';
        this.clientSecret = 'your_client_secret'; // NEVER put this in client code!
    }
    
    async refreshAccessToken() {
        const refreshToken = localStorage.getItem('refresh_token');
        
        if (!refreshToken) {
            throw new Error('No refresh token available');
        }
        
        try {
            // VULNERABLE: Client secret exposed in frontend code
            const response = await fetch(this.tokenEndpoint, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: new URLSearchParams({
                    grant_type: 'refresh_token',
                    refresh_token: refreshToken,  // Sent from client-side
                    client_id: this.clientId,
                    client_secret: this.clientSecret // EXPOSED in client code!
                })
            });
            
            const tokenData = await response.json();
            
            // VULNERABLE: Logging token refresh response
            console.log('Token refresh successful:', tokenData);
            
            // VULNERABLE: Storing new tokens in localStorage again
            localStorage.setItem('access_token', tokenData.access_token);
            if (tokenData.refresh_token) {
                localStorage.setItem('refresh_token', tokenData.refresh_token);
            }
            
            return tokenData;
        } catch (error) {
            // VULNERABLE: Error logging might expose tokens
            console.error('Token refresh failed:', error, {
                refresh_token: refreshToken  // Logged on error!
            });
            
            // Clear tokens on refresh failure (but they were already exposed)
            this.clearTokens();
            throw error;
        }
    }
    
    async makeAuthenticatedRequest(url, options = {}) {
        let token = localStorage.getItem('access_token');
        
        try {
            const response = await fetch(url, {
                ...options,
                headers: {
                    'Authorization': `Bearer ${token}`,
                    ...options.headers
                }
            });
            
            if (response.status === 401) {
                // Token expired, try to refresh
                console.log('Token expired, refreshing...');
                const newTokenData = await this.refreshAccessToken();
                token = newTokenData.access_token;
                
                // Retry request with new token
                return fetch(url, {
                    ...options,
                    headers: {
                        'Authorization': `Bearer ${token}`,
                        ...options.headers
                    }
                });
            }
            
            return response;
        } catch (error) {
            console.error('Authenticated request failed:', error);
            throw error;
        }
    }
    
    clearTokens() {
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        localStorage.removeItem('id_token');
    }
    
    // VULNERABLE: Debug method that exposes all tokens
    debugTokenInfo() {
        return {
            access_token: localStorage.getItem('access_token'),
            refresh_token: localStorage.getItem('refresh_token'),
            id_token: localStorage.getItem('id_token')
        };
    }
}

Fixes

1

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.

View implementation – JAVASCRIPT
// SECURE: Backend OAuth proxy service (Node.js/Express)
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const axios = require('axios');
const crypto = require('crypto');

class OAuthProxyService {
    constructor() {
        this.app = express();
        this.clientId = process.env.OAUTH_CLIENT_ID;
        this.clientSecret = process.env.OAUTH_CLIENT_SECRET; // Secure on backend
        this.redirectUri = process.env.OAUTH_REDIRECT_URI;
        this.tokenEndpoint = 'https://oauth.provider.com/token';
        this.apiBaseUrl = 'https://api.provider.com';
        
        this.setupMiddleware();
        this.setupRoutes();
    }
    
    setupMiddleware() {
        // Secure session configuration
        this.app.use(session({
            store: new RedisStore({ host: 'localhost', port: 6379 }),
            secret: process.env.SESSION_SECRET,
            resave: false,
            saveUninitialized: false,
            cookie: {
                secure: process.env.NODE_ENV === 'production', // HTTPS only in prod
                httpOnly: true, // Not accessible to JavaScript
                maxAge: 24 * 60 * 60 * 1000, // 24 hours
                sameSite: 'strict'
            }
        }));
        
        this.app.use(express.json());
    }
    
    setupRoutes() {
        // Handle OAuth callback and token exchange
        this.app.post('/api/auth/oauth/callback', async (req, res) => {
            try {
                const { code, code_verifier, redirect_uri } = req.body;
                
                if (!code || !code_verifier) {
                    return res.status(400).json({ error: 'Missing required parameters' });
                }
                
                // Exchange authorization code for tokens
                const tokenResponse = await axios.post(this.tokenEndpoint, {
                    grant_type: 'authorization_code',
                    client_id: this.clientId,
                    client_secret: this.clientSecret, // Secure on backend
                    code: code,
                    code_verifier: code_verifier,
                    redirect_uri: redirect_uri
                });
                
                const tokenData = tokenResponse.data;
                
                // SECURE: Store tokens in server session, not client
                req.session.oauth = {
                    access_token: tokenData.access_token,
                    refresh_token: tokenData.refresh_token,
                    token_type: tokenData.token_type,
                    expires_at: Date.now() + (tokenData.expires_in * 1000),
                    scope: tokenData.scope
                };
                
                // Get user info
                const userInfo = await this.getUserInfo(tokenData.access_token);
                req.session.user = userInfo;
                
                res.json({ success: true, user: userInfo });
                
            } catch (error) {
                console.error('OAuth callback error:', error.message); // Don't log tokens
                res.status(500).json({ error: 'Authentication failed' });
            }
        });
        
        // Proxy API requests with automatic token refresh
        this.app.all('/api/proxy/*', async (req, res) => {
            try {
                if (!req.session.oauth) {
                    return res.status(401).json({ error: 'Not authenticated' });
                }
                
                // Check if token needs refresh
                if (Date.now() >= req.session.oauth.expires_at - 60000) { // Refresh 1 minute before expiry
                    await this.refreshAccessToken(req.session);
                }
                
                // Extract API path
                const apiPath = req.path.replace('/api/proxy', '');
                const targetUrl = this.apiBaseUrl + apiPath;
                
                // Make authenticated request to API
                const apiResponse = await axios({
                    method: req.method,
                    url: targetUrl,
                    headers: {
                        'Authorization': `${req.session.oauth.token_type} ${req.session.oauth.access_token}`,
                        'Content-Type': req.headers['content-type'],
                        'User-Agent': 'OAuth-Proxy-Service/1.0'
                    },
                    data: req.body,
                    params: req.query
                });
                
                res.json(apiResponse.data);
                
            } catch (error) {
                if (error.response?.status === 401) {
                    // Token invalid, clear session
                    req.session.destroy();
                    res.status(401).json({ error: 'Authentication expired' });
                } else {
                    console.error('API proxy error:', error.message);
                    res.status(error.response?.status || 500).json({
                        error: 'API request failed'
                    });
                }
            }
        });
        
        // Logout endpoint
        this.app.post('/api/auth/logout', (req, res) => {
            req.session.destroy(err => {
                if (err) {
                    console.error('Logout error:', err);
                    return res.status(500).json({ error: 'Logout failed' });
                }
                res.json({ success: true });
            });
        });
        
        // User info endpoint
        this.app.get('/api/auth/user', (req, res) => {
            if (!req.session.user) {
                return res.status(401).json({ error: 'Not authenticated' });
            }
            res.json(req.session.user);
        });
    }
    
    async refreshAccessToken(session) {
        try {
            const refreshResponse = await axios.post(this.tokenEndpoint, {
                grant_type: 'refresh_token',
                client_id: this.clientId,
                client_secret: this.clientSecret,
                refresh_token: session.oauth.refresh_token
            });
            
            const newTokenData = refreshResponse.data;
            
            // Update session with new tokens
            session.oauth.access_token = newTokenData.access_token;
            if (newTokenData.refresh_token) {
                session.oauth.refresh_token = newTokenData.refresh_token;
            }
            session.oauth.expires_at = Date.now() + (newTokenData.expires_in * 1000);
            
            console.log('Token refreshed successfully for session');
            
        } catch (error) {
            console.error('Token refresh failed:', error.message);
            throw new Error('Token refresh failed');
        }
    }
    
    async getUserInfo(accessToken) {
        try {
            const userResponse = await axios.get('https://api.provider.com/user', {
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            });
            return userResponse.data;
        } catch (error) {
            console.error('User info fetch failed:', error.message);
            throw error;
        }
    }
}

// SECURE: Frontend service using backend proxy
class SecureFrontendService {
    constructor() {
        this.baseUrl = '/api/proxy'; // Routes through secure backend proxy
    }
    
    async getCurrentUser() {
        const response = await fetch('/api/auth/user', {
            credentials: 'include' // Send httpOnly session cookies
        });
        
        if (!response.ok) {
            throw new Error('Not authenticated');
        }
        
        return response.json();
    }
    
    async makeAPICall(endpoint, options = {}) {
        // SECURE: All API calls go through backend proxy
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            ...options,
            credentials: 'include', // Always include session cookies
            headers: {
                'Content-Type': 'application/json',
                ...options.headers
            }
        });
        
        if (!response.ok) {
            if (response.status === 401) {
                // Redirect to login
                window.location.href = '/login';
                return;
            }
            throw new Error(`API call failed: ${response.statusText}`);
        }
        
        return response.json();
    }
    
    async logout() {
        await fetch('/api/auth/logout', {
            method: 'POST',
            credentials: 'include'
        });
        
        window.location.href = '/login';
    }
}

// Usage in frontend
const apiService = new SecureFrontendService();

// Make API calls without handling tokens
apiService.makeAPICall('/user/profile')
    .then(profile => console.log('User profile:', profile))
    .catch(error => console.error('API error:', error));
3

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.

View implementation – JAVASCRIPT
// SECURE: Enhanced client-side security (when backend proxy isn't feasible)

// Secure token storage with encryption
class SecureTokenStorage {
    constructor() {
        this.storageKey = 'secure_auth_data';
        this.encryptionKey = null;
        this.initializeEncryption();
    }
    
    async initializeEncryption() {
        // Generate or retrieve encryption key
        const keyData = sessionStorage.getItem('enc_key');
        if (keyData) {
            this.encryptionKey = await crypto.subtle.importKey(
                'raw',
                this.base64ToArrayBuffer(keyData),
                { name: 'AES-GCM' },
                false,
                ['encrypt', 'decrypt']
            );
        } else {
            // Generate new key for session
            this.encryptionKey = await crypto.subtle.generateKey(
                { name: 'AES-GCM', length: 256 },
                true,
                ['encrypt', 'decrypt']
            );
            
            const exportedKey = await crypto.subtle.exportKey('raw', this.encryptionKey);
            sessionStorage.setItem('enc_key', this.arrayBufferToBase64(exportedKey));
        }
    }
    
    async encryptData(data) {
        const encoder = new TextEncoder();
        const dataBuffer = encoder.encode(JSON.stringify(data));
        const iv = crypto.getRandomValues(new Uint8Array(12));
        
        const encrypted = await crypto.subtle.encrypt(
            { name: 'AES-GCM', iv: iv },
            this.encryptionKey,
            dataBuffer
        );
        
        return {
            iv: this.arrayBufferToBase64(iv),
            data: this.arrayBufferToBase64(encrypted)
        };
    }
    
    async decryptData(encryptedData) {
        const iv = this.base64ToArrayBuffer(encryptedData.iv);
        const data = this.base64ToArrayBuffer(encryptedData.data);
        
        const decrypted = await crypto.subtle.decrypt(
            { name: 'AES-GCM', iv: iv },
            this.encryptionKey,
            data
        );
        
        const decoder = new TextDecoder();
        return JSON.parse(decoder.decode(decrypted));
    }
    
    async storeTokens(tokenData) {
        // Add timestamp and token binding
        const secureTokenData = {
            ...tokenData,
            stored_at: Date.now(),
            fingerprint: await this.generateDeviceFingerprint(),
            csrf_token: crypto.getRandomValues(new Uint8Array(16))
        };
        
        const encrypted = await this.encryptData(secureTokenData);
        sessionStorage.setItem(this.storageKey, JSON.stringify(encrypted));
    }
    
    async getTokens() {
        const encryptedData = sessionStorage.getItem(this.storageKey);
        if (!encryptedData) return null;
        
        try {
            const parsed = JSON.parse(encryptedData);
            const decrypted = await this.decryptData(parsed);
            
            // Verify token binding
            const currentFingerprint = await this.generateDeviceFingerprint();
            if (decrypted.fingerprint !== currentFingerprint) {
                throw new Error('Token binding validation failed');
            }
            
            // Check if token is too old (max 1 hour)
            if (Date.now() - decrypted.stored_at > 3600000) {
                this.clearTokens();
                return null;
            }
            
            return decrypted;
        } catch (error) {
            console.error('Token decryption failed:', error.message);
            this.clearTokens();
            return null;
        }
    }
    
    clearTokens() {
        sessionStorage.removeItem(this.storageKey);
        sessionStorage.removeItem('enc_key');
    }
    
    async generateDeviceFingerprint() {
        const components = [
            navigator.userAgent,
            navigator.language,
            screen.width + 'x' + screen.height,
            new Date().getTimezoneOffset().toString(),
            navigator.hardwareConcurrency || 'unknown'
        ];
        
        const fingerprint = components.join('|');
        const encoder = new TextEncoder();
        const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(fingerprint));
        
        return this.arrayBufferToBase64(hashBuffer);
    }
    
    arrayBufferToBase64(buffer) {
        const binary = String.fromCharCode(...new Uint8Array(buffer));
        return btoa(binary);
    }
    
    base64ToArrayBuffer(base64) {
        const binary = atob(base64);
        const bytes = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            bytes[i] = binary.charCodeAt(i);
        }
        return bytes.buffer;
    }
}

// Enhanced OAuth service with security monitoring
class SecureOAuthService {
    constructor() {
        this.tokenStorage = new SecureTokenStorage();
        this.securityMonitor = new SecurityMonitor();
        this.clientId = 'your_client_id';
    }
    
    async makeSecureAPICall(url, options = {}) {
        const tokenData = await this.tokenStorage.getTokens();
        if (!tokenData) {
            throw new Error('No valid authentication token available');
        }
        
        // Monitor API call patterns
        this.securityMonitor.recordAPICall(url, Date.now());
        
        try {
            const response = await fetch(url, {
                ...options,
                headers: {
                    'Authorization': `Bearer ${tokenData.access_token}`,
                    'Content-Type': 'application/json',
                    // Add CSRF token to headers
                    'X-CSRF-Token': btoa(String.fromCharCode(...tokenData.csrf_token)),
                    ...options.headers
                }
            });
            
            if (response.status === 401) {
                // Token expired, attempt refresh
                await this.refreshTokens();
                // Retry the request once
                return this.makeSecureAPICall(url, { ...options, _retry: true });
            }
            
            return response;
            
        } catch (error) {
            this.securityMonitor.recordSecurityEvent('api_call_failed', {
                url: url.replace(/access_token=[^&]+/g, 'access_token=***'),
                error: error.message
            });
            throw error;
        }
    }
    
    async refreshTokens() {
        const tokenData = await this.tokenStorage.getTokens();
        if (!tokenData?.refresh_token) {
            throw new Error('No refresh token available');
        }
        
        // Use backend endpoint for token refresh (recommended)
        const response = await fetch('/api/auth/refresh', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                refresh_token: tokenData.refresh_token,
                client_id: this.clientId
            })
        });
        
        if (!response.ok) {
            this.tokenStorage.clearTokens();
            throw new Error('Token refresh failed');
        }
        
        const newTokenData = await response.json();
        await this.tokenStorage.storeTokens(newTokenData);
    }
    
    async logout() {
        const tokenData = await this.tokenStorage.getTokens();
        
        // Revoke tokens on server
        if (tokenData) {
            try {
                await fetch('/api/auth/revoke', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        access_token: tokenData.access_token,
                        refresh_token: tokenData.refresh_token
                    })
                });
            } catch (error) {
                console.error('Token revocation failed:', error);
            }
        }
        
        this.tokenStorage.clearTokens();
        this.securityMonitor.recordSecurityEvent('user_logout', {});
    }
}

// Security monitoring for suspicious patterns
class SecurityMonitor {
    constructor() {
        this.events = [];
        this.apiCalls = [];
        this.maxEvents = 100;
    }
    
    recordAPICall(url, timestamp) {
        this.apiCalls.push({ url: this.sanitizeUrl(url), timestamp });
        
        // Keep only recent calls
        const oneHourAgo = Date.now() - 3600000;
        this.apiCalls = this.apiCalls.filter(call => call.timestamp > oneHourAgo);
        
        // Check for suspicious patterns
        this.detectAnomalies();
    }
    
    recordSecurityEvent(eventType, data) {
        const event = {
            type: eventType,
            timestamp: Date.now(),
            data: this.sanitizeData(data)
        };
        
        this.events.push(event);
        if (this.events.length > this.maxEvents) {
            this.events.shift();
        }
        
        // Report critical events
        if (this.isCriticalEvent(eventType)) {
            this.reportSecurityEvent(event);
        }
    }
    
    detectAnomalies() {
        const recentCalls = this.apiCalls.filter(call => 
            Date.now() - call.timestamp < 60000 // Last minute
        );
        
        // High frequency detection
        if (recentCalls.length > 50) {
            this.recordSecurityEvent('high_api_frequency', {
                count: recentCalls.length,
                timeframe: '1_minute'
            });
        }
        
        // Pattern analysis
        const uniqueUrls = new Set(recentCalls.map(call => call.url));
        if (uniqueUrls.size === 1 && recentCalls.length > 20) {
            this.recordSecurityEvent('repeated_api_calls', {
                url: Array.from(uniqueUrls)[0],
                count: recentCalls.length
            });
        }
    }
    
    sanitizeUrl(url) {
        return url.replace(/[?&](access_token|token|key|secret)=[^&]*/gi, '$1=***');
    }
    
    sanitizeData(data) {
        const sanitized = { ...data };
        const sensitiveKeys = ['token', 'password', 'secret', 'key', 'auth'];
        
        for (const key in sanitized) {
            if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
                sanitized[key] = '***';
            }
        }
        
        return sanitized;
    }
    
    isCriticalEvent(eventType) {
        const criticalEvents = [
            'high_api_frequency',
            'repeated_api_calls',
            'token_binding_failed',
            'decryption_failed'
        ];
        return criticalEvents.includes(eventType);
    }
    
    async reportSecurityEvent(event) {
        try {
            await fetch('/api/security/report', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    event,
                    user_agent: navigator.userAgent,
                    timestamp: Date.now()
                })
            });
        } catch (error) {
            console.error('Failed to report security event:', error);
        }
    }
}

// Content Security Policy implementation (add to HTML)
/*
<meta http-equiv="Content-Security-Policy" content="
    default-src 'self';
    script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    connect-src 'self' https://api.example.com https://oauth.provider.com;
    frame-src 'none';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
">
*/

Detect This Vulnerability in Your Code

Sourcery automatically identifies oauth access tokens exposed in client-side javascript and many other security issues in your codebase.