From 83d6ed454146dd21e3fb447fcbdfd20093f319c8 Mon Sep 17 00:00:00 2001 From: Jordan Dalby Date: Sun, 24 Nov 2024 22:21:24 +0000 Subject: [PATCH] Improved OIDC error reporting to make it easier to identify issues --- client/src/components/auth/LoginPage.tsx | 12 ++-- .../src/components/auth/oidc/OIDCCallback.tsx | 15 +++-- client/src/contexts/ToastContext.tsx | 14 +++-- client/src/utils/oidcErrorHandler.ts | 55 +++++++++++++++++++ server/src/app.js | 2 +- server/src/routes/oidcRoutes.js | 23 +++++++- 6 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 client/src/utils/oidcErrorHandler.ts diff --git a/client/src/components/auth/LoginPage.tsx b/client/src/components/auth/LoginPage.tsx index 1432de9..35cf317 100644 --- a/client/src/components/auth/LoginPage.tsx +++ b/client/src/components/auth/LoginPage.tsx @@ -7,6 +7,7 @@ import { useToast } from '../../hooks/useToast'; import { ROUTES } from '../../constants/routes'; import { OIDCConfig } from '../../types/auth'; import { apiClient } from '../../utils/api/apiClient'; +import { handleOIDCError } from '../../utils/oidcErrorHandler'; export const LoginPage: React.FC = () => { const [username, setUsername] = useState(''); @@ -18,12 +19,13 @@ export const LoginPage: React.FC = () => { useEffect(() => { const params = new URLSearchParams(window.location.search); - if (params.get('error') === 'auth_failed') { - addToast('Authentication failed. Please try again.', 'error'); - } else if (params.get('error') === 'registration_disabled') { - addToast('Registration is disabled on this ByteStash instance.', 'error'); + const error = params.get('error'); + const message = params.get('message'); + + if (error) { + handleOIDCError(error, addToast, oidcConfig?.displayName, message || undefined); } - }, []); + }, [addToast, oidcConfig]); useEffect(() => { const fetchOIDCConfig = async () => { diff --git a/client/src/components/auth/oidc/OIDCCallback.tsx b/client/src/components/auth/oidc/OIDCCallback.tsx index 00c5686..5c3fa88 100644 --- a/client/src/components/auth/oidc/OIDCCallback.tsx +++ b/client/src/components/auth/oidc/OIDCCallback.tsx @@ -3,24 +3,31 @@ import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../../hooks/useAuth'; import { Loader2 } from 'lucide-react'; import { PageContainer } from '../../common/layout/PageContainer'; +import { useToast } from '../../../hooks/useToast'; +import { handleOIDCError } from '../../../utils/oidcErrorHandler'; export const OIDCCallback: React.FC = () => { const navigate = useNavigate(); const { login } = useAuth(); + const { addToast } = useToast(); useEffect(() => { const params = new URLSearchParams(window.location.search); const token = params.get('token'); + const error = params.get('error'); + const message = params.get('message'); if (token) { - // Store the token and redirect login(token, null); navigate('/', { replace: true }); + } else if (error) { + handleOIDCError(error, addToast, undefined, message || undefined); + navigate('/login', { replace: true }); } else { - // Handle error case - navigate('/login?error=auth_failed', { replace: true }); + handleOIDCError('auth_failed', addToast); + navigate('/login', { replace: true }); } - }, [login, navigate]); + }, [login, navigate, addToast]); return ( diff --git a/client/src/contexts/ToastContext.tsx b/client/src/contexts/ToastContext.tsx index 9217547..90eae32 100644 --- a/client/src/contexts/ToastContext.tsx +++ b/client/src/contexts/ToastContext.tsx @@ -7,11 +7,11 @@ export interface Toast { id: number; message: string; type: ToastType; - duration: number; + duration: number | null; } -interface ToastContextType { - addToast: (message: string, type?: ToastType, duration?: number) => void; +export interface ToastContextType { + addToast: (message: string, type?: ToastType, duration?: number | null) => void; removeToast: (id: number) => void; } @@ -65,7 +65,7 @@ const ToastComponent: React.FC = ({ clearInterval(interval); return 0; } - return prev - (100 / (duration / 100)); + return prev - (100 / ((duration || 0) / 100)); }); }, 100); @@ -106,11 +106,13 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ childre const addToast = useCallback(( message: string, type: ToastType = 'info', - duration = 3000 + duration: number | null = 3000 ) => { const id = Date.now(); setToasts(prev => [...prev, { id, message, type, duration }]); - setTimeout(() => removeToast(id), duration); + if (duration !== null) { + setTimeout(() => removeToast(id), duration); + } }, [removeToast]); return ( diff --git a/client/src/utils/oidcErrorHandler.ts b/client/src/utils/oidcErrorHandler.ts new file mode 100644 index 0000000..f8adfac --- /dev/null +++ b/client/src/utils/oidcErrorHandler.ts @@ -0,0 +1,55 @@ +import { ToastType } from '../contexts/ToastContext'; + +interface OIDCErrorConfig { + message: string; + type: ToastType; + duration?: number | null; +} + +const OIDC_ERROR_CONFIGS: Record = { + auth_failed: { + message: "Authentication failed. This could be due to a cancelled login attempt or an expired session. Please try again.", + type: 'error', + duration: 8000 + }, + registration_disabled: { + message: "New account registration is currently disabled on this ByteStash instance. Please contact your administrator.", + type: 'error', + duration: null + }, + provider_error: { + message: "The identity provider encountered an error or is unavailable. Please try again later or contact your administrator.", + type: 'error', + duration: 8000 + }, + config_error: { + message: "There was an error with the SSO configuration. Please contact your administrator.", + type: 'error', + duration: null + }, + default: { + message: "An unexpected error occurred during authentication. Please try again.", + type: 'error', + duration: 8000 + } +}; + +export const handleOIDCError = ( + error: string, + addToast: (message: string, type: ToastType, duration?: number | null) => void, + providerName?: string, + additionalMessage?: string +) => { + const config = OIDC_ERROR_CONFIGS[error] || OIDC_ERROR_CONFIGS.default; + let message = config.message; + + if (providerName) { + message = message.replace('identity provider', providerName); + } + + if (additionalMessage) { + message = `${message}\n\nError details: ${additionalMessage}`; + } + + addToast(message, config.type, config.duration); +}; \ No newline at end of file diff --git a/server/src/app.js b/server/src/app.js index c5a294d..31f5dfe 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1,5 +1,5 @@ import express from 'express'; -import { initializeDatabase } from './config/database.js'; +import { initializeDatabase, shutdownDatabase } from './config/database.js'; import snippetRoutes from './routes/snippetRoutes.js'; import authRoutes from './routes/authRoutes.js'; import shareRoutes from './routes/shareRoutes.js'; diff --git a/server/src/routes/oidcRoutes.js b/server/src/routes/oidcRoutes.js index 7d2ddc7..9ec483c 100644 --- a/server/src/routes/oidcRoutes.js +++ b/server/src/routes/oidcRoutes.js @@ -45,7 +45,7 @@ router.get('/auth', async (req, res) => { try { const oidc = await OIDCConfig.getInstance(); if (!oidc.isEnabled()) { - return res.status(404).json({ error: 'OIDC not enabled' }); + return res.redirect('/login?error=config_error'); } const baseUrl = getBaseUrl(req); @@ -58,7 +58,8 @@ router.get('/auth', async (req, res) => { res.redirect(authUrl); } catch (error) { Logger.error('OIDC auth error:', error); - res.redirect('/login?error=auth_failed'); + const errorMessage = encodeURIComponent(error.message || 'Unknown error'); + res.redirect(`/login?error=provider_error&message=${errorMessage}`); } }); @@ -118,7 +119,23 @@ router.get('/callback', async (req, res) => { res.redirect(`/auth/callback?token=${token}`); } catch (error) { Logger.error('OIDC callback error:', error); - res.redirect('/login?error=auth_failed'); + let errorType = 'auth_failed'; + let errorDetails = ''; + + if (error.message?.includes('state parameter')) { + errorType = 'auth_failed'; + errorDetails = 'Your authentication session has expired'; + } else if (error.message?.includes('accounts disabled')) { + errorType = 'registration_disabled'; + } else if (error.message?.includes('OIDC configuration')) { + errorType = 'config_error'; + } else if (error.response?.status === 401 || error.response?.status === 403) { + errorType = 'provider_error'; + errorDetails = 'Authorization denied by identity provider'; + } + + const messageParam = errorDetails ? `&message=${encodeURIComponent(errorDetails)}` : ''; + res.redirect(`/login?error=${errorType}${messageParam}`); } });