mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-26 21:28:50 +01:00
Added Cooldown logic for OIDC auto redirect for failed login attempts
This commit is contained in:
parent
caaadf2fdb
commit
bfc7179f16
3 changed files with 104 additions and 9 deletions
|
|
@ -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`);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue