diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx index a1c522bc..9077bfbf 100644 --- a/ui/src/auth/LoginPage.tsx +++ b/ui/src/auth/LoginPage.tsx @@ -1,5 +1,5 @@ -import { type FormEvent, useEffect, useMemo, useState } from 'react'; -import { Navigate } from 'react-router'; +import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react'; +import { Navigate, useSearchParams } from 'react-router'; import { useAuthStore } from './auth-store'; import { api } from '../api/client'; import { config } from '../config'; @@ -41,11 +41,14 @@ const SUBTITLES = [ export function LoginPage() { const { isAuthenticated, login, loading, error } = useAuthStore(); + const [searchParams] = useSearchParams(); + const forceLocal = searchParams.has('local'); const subtitle = useMemo(() => SUBTITLES[Math.floor(Math.random() * SUBTITLES.length)], []); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [oidc, setOidc] = useState(null); const [oidcLoading, setOidcLoading] = useState(false); + const autoRedirected = useRef(false); useEffect(() => { api.GET('/auth/oidc/config') @@ -60,6 +63,22 @@ export function LoginPage() { .catch(() => {}); }, []); + // Auto-redirect to OIDC provider for SSO (skip if ?local is in URL) + useEffect(() => { + if (oidc && !forceLocal && !autoRedirected.current) { + autoRedirected.current = true; + const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`; + const params = new URLSearchParams({ + response_type: 'code', + client_id: oidc.clientId, + redirect_uri: redirectUri, + scope: 'openid email profile', + prompt: 'none', + }); + window.location.href = `${oidc.authorizationEndpoint}?${params}`; + } + }, [oidc, forceLocal]); + if (isAuthenticated) return ; const handleSubmit = (e: FormEvent) => { diff --git a/ui/src/auth/OidcCallback.tsx b/ui/src/auth/OidcCallback.tsx index d98d146d..c1158d07 100644 --- a/ui/src/auth/OidcCallback.tsx +++ b/ui/src/auth/OidcCallback.tsx @@ -18,6 +18,11 @@ export function OidcCallback() { const errorParam = params.get('error'); if (errorParam) { + // prompt=none SSO attempt failed (no active session) — fall back to login form + if (errorParam === 'login_required' || errorParam === 'interaction_required') { + window.location.replace(`${config.basePath}login?local`); + return; + } useAuthStore.setState({ error: params.get('error_description') || errorParam, loading: false,