(null);
const [oidcLoading, setOidcLoading] = useState(false);
- const autoRedirected = useRef(false);
- useEffect(() => {
- api.GET('/auth/oidc/config')
- .then(({ data }) => {
- if (data?.authorizationEndpoint && data?.clientId) {
- setOidc({
- clientId: data.clientId,
- authorizationEndpoint: data.authorizationEndpoint,
- resource: data.resource ?? undefined,
- additionalScopes: data.additionalScopes ?? undefined,
- });
- if (data.endSessionEndpoint) {
- localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint);
- }
- }
- })
- .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 scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(oidc.additionalScopes || [])];
- const params = new URLSearchParams({
- response_type: 'code',
- client_id: oidc.clientId,
- redirect_uri: redirectUri,
- scope: scopes.join(' '),
- prompt: 'none',
- });
- if (oidc.resource) params.set('resource', oidc.resource);
- window.location.href = `${oidc.authorizationEndpoint}?${params}`;
- }
- }, [oidc, forceLocal]);
+ const { data: caps, isError: capsFailed, isLoading: capsLoading } = useAuthCapabilities();
if (isAuthenticated) return ;
+ if (capsLoading) return null;
+
+ const oidcEnabled = caps?.oidc?.enabled === true;
+ const adminRecoveryOnly = caps?.localAccounts?.adminRecoveryOnly === true;
+ const providerName = caps?.oidc?.providerName || 'Single Sign-On';
+
+ // Render decisions
+ const showSsoPrimary = oidcEnabled && adminRecoveryOnly && !forceLocal;
+ const showLocalForm = !oidcEnabled || forceLocal || !adminRecoveryOnly || capsFailed;
+ const showAdminRecoveryBanner = oidcEnabled && adminRecoveryOnly && forceLocal;
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
login(username, password);
};
- const handleOidcLogin = () => {
- if (!oidc) return;
+ const handleOidcLogin = async () => {
setOidcLoading(true);
+ const { data } = await api.GET('/auth/oidc/config');
+ if (!data?.authorizationEndpoint || !data?.clientId) {
+ setOidcLoading(false);
+ useAuthStore.setState({ error: 'OIDC configuration unavailable. Try the local form via /login?local.' });
+ return;
+ }
+ if (data.endSessionEndpoint) {
+ localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint);
+ }
const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`;
- const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(oidc.additionalScopes || [])];
+ const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(data.additionalScopes || [])];
const params = new URLSearchParams({
response_type: 'code',
- client_id: oidc.clientId,
+ client_id: data.clientId,
redirect_uri: redirectUri,
scope: scopes.join(' '),
});
- if (oidc.resource) params.set('resource', oidc.resource);
- window.location.href = `${oidc.authorizationEndpoint}?${params}`;
+ if (data.resource) params.set('resource', data.resource);
+ // Note: NO prompt=none. Per RFC 9700 §4.4, that's silent re-auth only;
+ // for first-time login it returns login_required and traps users on a local form.
+ window.location.href = `${data.authorizationEndpoint}?${params}`;
};
return (
@@ -125,68 +101,81 @@ export function LoginPage() {
{subtitle}
+ {capsFailed && (
+
+
Sign-in options couldn't load. Refresh or use the form below.
+
+ )}
+
+ {showAdminRecoveryBanner && (
+
+
+ Admin recovery login. Use SSO for normal sign-in.
+
+
← Back to SSO
+
+ )}
+
{error && (
)}
- {oidc && (
- <>
-
-
-
-
- >
+ {showSsoPrimary && (
+
+
+
+ Admin recovery →
+
+
)}
-
+
+
+ )}