Added Cooldown logic for OIDC auto redirect for failed login attempts

This commit is contained in:
Danilo Pejakovic 2025-02-27 10:58:52 +01:00
parent caaadf2fdb
commit bfc7179f16
3 changed files with 104 additions and 9 deletions

View file

@ -22,6 +22,13 @@ const oauthHandler = async (req, res) => {
return;
}
await setAuthTokens(req.user._id, res);
// On successful login, let's clear any openid redirect flags
res.cookie('successful_login', 'true', {
maxAge: 1000, // very short-lived, just for client-side detection
httpOnly: false // client needs to read this
});
res.redirect(domains.client);
} catch (err) {
logger.error('Error in setting authentication tokens:', err);
@ -31,7 +38,9 @@ const oauthHandler = async (req, res) => {
router.get('/error', (req, res) => {
// A single error message is pushed by passport when authentication fails.
logger.error('Error in OAuth authentication:', { message: req.session.messages.pop() });
res.redirect(`${domains.client}/login`);
// Redirect to login page with auth_failed parameter to prevent infinite redirect loops
res.redirect(`${domains.client}/login?auth_failed=true`);
});
/**

View file

@ -3,7 +3,7 @@ import { useEffect, useRef } from 'react';
import { useAuthContext } from '~/hooks/AuthContext';
import type { TLoginLayoutContext } from '~/common';
import { ErrorMessage } from '~/components/Auth/ErrorMessage';
import { getLoginError } from '~/utils';
import { getLoginError, shouldRedirectToOpenID, clearOpenIDRedirectFlag, getCookie } from '~/utils';
import { useLocalize } from '~/hooks';
import LoginForm from './LoginForm';
@ -18,22 +18,40 @@ function Login() {
// When enabled, users will be automatically redirected to the OpenID provider
// without seeing the login form at all
useEffect(() => {
// Simple check if redirect is needed and not yet attempted
// Check for URL parameters that indicate a failed auth attempt
const urlParams = new URLSearchParams(window.location.search);
const authFailed = urlParams.get('auth_failed') === 'true';
// Use the utility function to determine if we should redirect
if (
!redirectAttemptedRef.current &&
startupConfig?.openidLoginEnabled &&
startupConfig?.openidAutoRedirect &&
startupConfig?.serverDomain
shouldRedirectToOpenID({
redirectAttempted: redirectAttemptedRef.current,
openidLoginEnabled: startupConfig?.openidLoginEnabled,
openidAutoRedirect: startupConfig?.openidAutoRedirect,
serverDomain: startupConfig?.serverDomain,
authFailed
})
) {
// Mark that we've attempted to redirect
// Mark that we've attempted to redirect in this component instance
redirectAttemptedRef.current = true;
// Log and redirect
console.log('Auto-redirecting to OpenID provider...');
window.location.href = `${startupConfig.serverDomain}/oauth/openid`;
window.location.href = `${startupConfig?.serverDomain}/oauth/openid`;
}
}, [startupConfig]);
// Clear the redirect flag after successful login (when the cookie is present)
useEffect(() => {
const successfulLogin = getCookie('successful_login');
if (successfulLogin) {
// Clear the redirect flag in localStorage
clearOpenIDRedirectFlag();
// Clear the cookie since we've processed it
document.cookie = 'successful_login=; Max-Age=0; path=/;';
}
}, []);
return (
<>

View file

@ -1,5 +1,10 @@
import { LocalStorageKeys, TConversation } from 'librechat-data-provider';
// Key for tracking OpenID redirect attempts
export const OPENID_REDIRECT_KEY = 'openid_redirect_attempted';
// Cooldown period in milliseconds (5 minutes)
export const OPENID_REDIRECT_COOLDOWN = 5 * 60 * 1000;
export function getLocalStorageItems() {
const items = {
lastSelectedModel: localStorage.getItem(LocalStorageKeys.LAST_MODEL) ?? '',
@ -24,6 +29,69 @@ export function getLocalStorageItems() {
};
}
/**
* Handles the OpenID redirect logic to prevent infinite redirect loops
* @param conditions Object containing conditions that must be met for redirect
* @returns Boolean indicating whether to proceed with the redirect
*/
export function shouldRedirectToOpenID({
redirectAttempted,
openidLoginEnabled,
openidAutoRedirect,
serverDomain,
authFailed = false,
}: {
redirectAttempted: boolean;
openidLoginEnabled?: boolean;
openidAutoRedirect?: boolean;
serverDomain?: string;
authFailed?: boolean;
}): boolean {
// Get timestamp of last redirect attempt from localStorage
const lastRedirectAttempt = localStorage.getItem(OPENID_REDIRECT_KEY);
const currentTime = Date.now();
// Only redirect if all conditions are met
if (
!redirectAttempted &&
openidLoginEnabled &&
openidAutoRedirect &&
serverDomain &&
!authFailed &&
(!lastRedirectAttempt || currentTime - parseInt(lastRedirectAttempt, 10) > OPENID_REDIRECT_COOLDOWN)
) {
// Store the current timestamp in localStorage
localStorage.setItem(OPENID_REDIRECT_KEY, currentTime.toString());
return true;
}
return false;
}
/**
* Clears the OpenID redirect tracking flag
*/
export function clearOpenIDRedirectFlag(): void {
localStorage.removeItem(OPENID_REDIRECT_KEY);
}
/**
* Gets a cookie value by name
* @param name The name of the cookie
* @returns The cookie value or null if not found
*/
export function getCookie(name: string): string | null {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const part = parts.pop();
if (part) {
return part.split(';').shift() || null;
}
}
return null;
}
export function clearLocalStorage(skipFirst?: boolean) {
const keys = Object.keys(localStorage);
keys.forEach((key) => {