diff --git a/src/web-server/routes/auth-routes.ts b/src/web-server/routes/auth-routes.ts index 3a7efb3b..70b2564f 100644 --- a/src/web-server/routes/auth-routes.ts +++ b/src/web-server/routes/auth-routes.ts @@ -7,7 +7,7 @@ import { Router, type Request, type Response } from 'express'; import bcrypt from 'bcrypt'; import crypto from 'crypto'; import { getDashboardAuthConfig } from '../../config/unified-config-loader'; -import { loginRateLimiter } from '../middleware/auth-middleware'; +import { isLoopbackRemoteAddress, loginRateLimiter } from '../middleware/auth-middleware'; /** * Timing-safe string comparison to prevent timing attacks. @@ -94,9 +94,15 @@ router.post('/logout', (req: Request, res: Response) => { */ router.get('/check', (req: Request, res: Response) => { const authConfig = getDashboardAuthConfig(); + const isLocal = isLoopbackRemoteAddress(req.socket.remoteAddress); + + // When auth is not configured and access is remote, the dashboard API + // endpoints return 403. Signal auth-required so the UI can show the + // login/setup page instead of a silently broken dashboard. + const effectiveAuthRequired = authConfig.enabled || !isLocal; res.json({ - authRequired: authConfig.enabled, + authRequired: effectiveAuthRequired, authenticated: req.session?.authenticated ?? false, username: req.session?.username ?? null, }); diff --git a/ui/src/contexts/auth-context.tsx b/ui/src/contexts/auth-context.tsx index f43993a2..d0e39ee7 100644 --- a/ui/src/contexts/auth-context.tsx +++ b/ui/src/contexts/auth-context.tsx @@ -47,8 +47,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { setUsername(res.username); }) .catch(() => { - // If auth check fails (e.g., 403 from remote access without auth configured), - // treat as auth required so the login page appears instead of a broken dashboard. + // If auth check fails (network error, server down, CORS issue), + // fail closed: require auth instead of granting access. + // Prevents silently broken dashboard when server is unreachable. setAuthRequired(true); setIsAuthenticated(false); })