feat(ui): signed-out splash + prompt=login on OIDC redirect
Two defensive layers complementing the RP-Initiated Logout in 82e25933:
1. cameleer:signed_out sessionStorage flag (set in auth-store.logout,
read+cleared in LoginPage on mount) renders a 'You have been signed
out successfully' card with an explicit 'Sign in again' button.
Mirrors the cameleer-saas pattern.
2. prompt=login on the OIDC authorization redirect forces the IdP to
re-prompt for credentials even if its session cookie somehow
survived RP-Initiated Logout (proxy, race, misconfigured
post_logout_redirect_uri). OIDC Core 1.0 §3.1.2.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,11 +47,43 @@ export function LoginPage() {
|
|||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [oidcLoading, setOidcLoading] = useState(false);
|
const [oidcLoading, setOidcLoading] = useState(false);
|
||||||
|
|
||||||
|
// Mirrors cameleer-saas: when logout sets this flag, render a "Signed out"
|
||||||
|
// confirmation instead of the regular form. The flag is one-shot — read +
|
||||||
|
// cleared on mount.
|
||||||
|
const [signedOut] = useState(() => {
|
||||||
|
const flag = sessionStorage.getItem('cameleer:signed_out');
|
||||||
|
if (flag) sessionStorage.removeItem('cameleer:signed_out');
|
||||||
|
return !!flag;
|
||||||
|
});
|
||||||
|
|
||||||
const { data: caps, isError: capsFailed, isLoading: capsLoading } = useAuthCapabilities();
|
const { data: caps, isError: capsFailed, isLoading: capsLoading } = useAuthCapabilities();
|
||||||
|
|
||||||
if (isAuthenticated) return <Navigate to="/" replace />;
|
if (isAuthenticated) return <Navigate to="/" replace />;
|
||||||
if (capsLoading) return null;
|
if (capsLoading) return null;
|
||||||
|
|
||||||
|
if (signedOut) {
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<div className={styles.loginForm}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<img src={brandLogo} alt="" className={styles.logoImg} />
|
||||||
|
cameleer
|
||||||
|
</div>
|
||||||
|
<p className={styles.subtitle}>You have been signed out successfully.</p>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => { window.location.replace(`${config.basePath}login`); }}
|
||||||
|
className={styles.submitButton}
|
||||||
|
>
|
||||||
|
Sign in again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const oidcPrimary = caps?.oidc?.primary === true;
|
const oidcPrimary = caps?.oidc?.primary === true;
|
||||||
const adminRecoveryOnly = caps?.localAccounts?.adminRecoveryOnly === true;
|
const adminRecoveryOnly = caps?.localAccounts?.adminRecoveryOnly === true;
|
||||||
const providerName = caps?.oidc?.providerName || 'Single Sign-On';
|
const providerName = caps?.oidc?.providerName || 'Single Sign-On';
|
||||||
@@ -87,10 +119,13 @@ export function LoginPage() {
|
|||||||
client_id: data.clientId,
|
client_id: data.clientId,
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
scope: scopes.join(' '),
|
scope: scopes.join(' '),
|
||||||
|
// Defence-in-depth: even if RP-Initiated Logout did not fully clear
|
||||||
|
// the IdP session (proxy/cookie edge cases), prompt=login forces the
|
||||||
|
// IdP to re-prompt for credentials instead of silent re-auth.
|
||||||
|
// OIDC Core 1.0 §3.1.2.1.
|
||||||
|
prompt: 'login',
|
||||||
});
|
});
|
||||||
if (data.resource) params.set('resource', data.resource);
|
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}`;
|
window.location.href = `${data.authorizationEndpoint}?${params}`;
|
||||||
} catch {
|
} catch {
|
||||||
useAuthStore.setState({ error: 'OIDC configuration unavailable. Try the local form via /login?local.' });
|
useAuthStore.setState({ error: 'OIDC configuration unavailable. Try the local form via /login?local.' });
|
||||||
|
|||||||
Reference in New Issue
Block a user