Enhance OAuth security and state validation
- Implemented stricter OAuth security measures, including CSRF protection via state parameter validation and redirect_uri checks. - Updated OAuth models to support timezone-aware datetime comparisons, replacing deprecated `utcnow`. - Enhanced logging for malformed Basic auth headers during token, introspect, and revoke requests. - Added allowlist validation for OAuth provider domains to prevent open redirect attacks. - Improved nonce validation for OpenID Connect tokens, ensuring token integrity during Google provider flows. - Updated E2E and unit tests to cover new security features and expanded OAuth state handling scenarios.
This commit is contained in:
@@ -153,6 +153,7 @@
|
||||
"authFailed": "Authentication Failed",
|
||||
"providerError": "The authentication provider returned an error",
|
||||
"missingParams": "Missing authentication parameters",
|
||||
"stateMismatch": "Invalid OAuth state. Please try again.",
|
||||
"unexpectedError": "An unexpected error occurred during authentication",
|
||||
"backToLogin": "Back to Login"
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"authFailed": "Autenticazione Fallita",
|
||||
"providerError": "Il provider di autenticazione ha restituito un errore",
|
||||
"missingParams": "Parametri di autenticazione mancanti",
|
||||
"stateMismatch": "Stato OAuth non valido. Riprova.",
|
||||
"unexpectedError": "Si è verificato un errore durante l'autenticazione",
|
||||
"backToLogin": "Torna al Login"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,18 @@ export default function OAuthCallbackPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
// SECURITY: Validate state parameter against stored value (CSRF protection)
|
||||
// This prevents cross-site request forgery attacks
|
||||
const storedState = sessionStorage.getItem('oauth_state');
|
||||
if (!storedState || storedState !== state) {
|
||||
// Clean up stored state on mismatch
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
sessionStorage.removeItem('oauth_mode');
|
||||
sessionStorage.removeItem('oauth_provider');
|
||||
setError(t('stateMismatch') || 'Invalid OAuth state. Please try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
hasProcessed.current = true;
|
||||
|
||||
// Process the OAuth callback
|
||||
|
||||
@@ -56,6 +56,44 @@ export function useOAuthProviders() {
|
||||
// OAuth Flow Mutations
|
||||
// ============================================================================
|
||||
|
||||
// Allowed OAuth provider domains for security validation
|
||||
const ALLOWED_OAUTH_DOMAINS = [
|
||||
'accounts.google.com',
|
||||
'github.com',
|
||||
'www.facebook.com', // For future Facebook support
|
||||
'login.microsoftonline.com', // For future Microsoft support
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate OAuth authorization URL
|
||||
* SECURITY: Prevents open redirect attacks by only allowing known OAuth provider domains
|
||||
*/
|
||||
function isValidOAuthUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// Only allow HTTPS for OAuth (security requirement)
|
||||
if (parsed.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
// Check if domain is in allowlist
|
||||
return ALLOWED_OAUTH_DOMAINS.includes(parsed.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract state parameter from OAuth authorization URL
|
||||
*/
|
||||
function extractStateFromUrl(url: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.searchParams.get('state');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start OAuth login/registration flow
|
||||
* Redirects user to the OAuth provider
|
||||
@@ -77,12 +115,27 @@ export function useOAuthStart() {
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
// Store mode in sessionStorage for callback handling
|
||||
sessionStorage.setItem('oauth_mode', mode);
|
||||
sessionStorage.setItem('oauth_provider', provider);
|
||||
|
||||
// Response is { [key: string]: unknown }, so cast authorization_url
|
||||
const authUrl = (response.data as { authorization_url: string }).authorization_url;
|
||||
|
||||
// SECURITY: Validate the authorization URL before redirecting
|
||||
// This prevents open redirect attacks if the backend is compromised
|
||||
if (!isValidOAuthUrl(authUrl)) {
|
||||
throw new Error('Invalid OAuth authorization URL');
|
||||
}
|
||||
|
||||
// SECURITY: Extract and store the state parameter for CSRF validation
|
||||
// The callback page will verify this matches the state in the response
|
||||
const state = extractStateFromUrl(authUrl);
|
||||
if (!state) {
|
||||
throw new Error('Missing state parameter in authorization URL');
|
||||
}
|
||||
|
||||
// Store mode, provider, and state in sessionStorage for callback handling
|
||||
sessionStorage.setItem('oauth_mode', mode);
|
||||
sessionStorage.setItem('oauth_provider', provider);
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
|
||||
// Redirect to OAuth provider
|
||||
window.location.href = authUrl;
|
||||
}
|
||||
@@ -151,14 +204,16 @@ export function useOAuthCallback() {
|
||||
queryClient.invalidateQueries({ queryKey: ['user'] });
|
||||
}
|
||||
|
||||
// Clean up session storage
|
||||
// Clean up session storage (including state for security)
|
||||
sessionStorage.removeItem('oauth_mode');
|
||||
sessionStorage.removeItem('oauth_provider');
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
},
|
||||
onError: () => {
|
||||
// Clean up session storage on error too
|
||||
sessionStorage.removeItem('oauth_mode');
|
||||
sessionStorage.removeItem('oauth_provider');
|
||||
sessionStorage.removeItem('oauth_state');
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -199,12 +254,25 @@ export function useOAuthLink() {
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
// Store mode in sessionStorage for callback handling
|
||||
sessionStorage.setItem('oauth_mode', 'link');
|
||||
sessionStorage.setItem('oauth_provider', provider);
|
||||
|
||||
// Response is { [key: string]: unknown }, so cast authorization_url
|
||||
const authUrl = (response.data as { authorization_url: string }).authorization_url;
|
||||
|
||||
// SECURITY: Validate the authorization URL before redirecting
|
||||
if (!isValidOAuthUrl(authUrl)) {
|
||||
throw new Error('Invalid OAuth authorization URL');
|
||||
}
|
||||
|
||||
// SECURITY: Extract and store the state parameter for CSRF validation
|
||||
const state = extractStateFromUrl(authUrl);
|
||||
if (!state) {
|
||||
throw new Error('Missing state parameter in authorization URL');
|
||||
}
|
||||
|
||||
// Store mode, provider, and state in sessionStorage for callback handling
|
||||
sessionStorage.setItem('oauth_mode', 'link');
|
||||
sessionStorage.setItem('oauth_provider', provider);
|
||||
sessionStorage.setItem('oauth_state', state);
|
||||
|
||||
// Redirect to OAuth provider
|
||||
window.location.href = authUrl;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user