// 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 (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
};
// 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 (
<div>
<h2>Please log in to view your profile</h2>
</div>
);
}
if (loading) {
return <div>Loading profile...</div>;
}
if (error) {
return (
<div>
<h2>Error</h2>
<p>{error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
return (
<div>
<h2>User Profile</h2>
{profile ? (
<div>
<p>Name: {profile.name}</p>
<p>Email: {profile.email}</p>
<p>Member since: {new Date(profile.created_at).toLocaleDateString()}</p>
{/* No debug functionality in secure implementation */}
</div>
) : (
<p>No profile data available.</p>
)}
</div>
);
};
// App.js - Main application with secure routing
const App = () => {
return (
<AuthProvider>
<Router>
<div className="App">
<nav>
<AuthenticatedNav />
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/callback" element={<CallbackHandler />} />
<Route
path="/profile"
element={
<ProtectedRoute>
<SecureUserProfile />
</ProtectedRoute>
}
/>
</Routes>
</div>
</Router>
</AuthProvider>
);
};
// ProtectedRoute.js - Route protection
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/" replace />;
}
return children;
};
// CallbackHandler.js - OAuth callback handling
const CallbackHandler = () => {
const { isLoading } = useAuth();
if (isLoading) {
return (
<div>
<h2>Processing login...</h2>
<p>Please wait while we complete your authentication.</p>
</div>
);
}
// Redirect to profile after successful authentication
return <Navigate to="/profile" replace />;
};
export default App;