* feat(auth): OIDC logout-to-IdP — phase 3 (#798) RP-initiated logout behind a new oidc_logout_from_idp setting: logging out of PicPeak also ends the IdP session. The SSO callback stores the raw ID token in an HttpOnly cookie (also the marker that the session came in via SSO — local-password sessions never bounce to the IdP); /logout builds the end_session URL from discovery metadata with id_token_hint + post_logout_redirect_uri + client_id and returns it as ssoLogoutUrl for the frontend to navigate to. Any failure (no end_session_endpoint, IdP unreachable, feature off) degrades to the plain local logout. Settings surface exposes the toggle plus the computed post-logout redirect URI to register at the IdP. Session timeouts deliberately stay local-only. 6 integration tests over the mock IdP; live-verified against Keycloak 26 (logout ends the Keycloak session, no confirmation prompt). * fix(auth): harden the SSO logout marker cookie (#798 phase 3) Codex review round 1: - Derive the oidc_id_token cookie options from the shared cookie policy (COOKIE_SAMESITE / COOKIE_DOMAIN / secure resolution) — hardcoded Lax meant split-origin deployments running on SameSite=None never sent the marker to the cross-site /logout XHR, silently disabling logout-to-IdP. - Oversized ID tokens (>3.9KB) now store a bare 'sso' marker instead of no cookie, so the claimed client_id-only end-session fallback actually happens; /logout only passes the value as id_token_hint when it is a real JWT. - establishAdminSession clears any stale marker on every fresh login — sessions can die without /logout (deactivation, expiry, restore), and a surviving marker would bounce a later local-password session to the IdP. The SSO callback re-sets the marker for its own session. Tests: oversized-token marker + hint-less end-session URL, stale-marker cleared on local login; helper updated for the clear+set cookie pair. * fix(auth): validate the logout hint against the current OIDC config (#798 phase 3) Codex review round 2: an ID token stored at login can outlive an issuer/client config change; sending it to the newly configured IdP as id_token_hint strands the user on the IdP's error page (providers validate iss/aud on the hint). buildEndSessionUrl now decodes the hint (no verification — routing only): different issuer → skip the round-trip entirely (the session belongs to another IdP); same issuer but changed client → keep the round-trip, drop the unusable hint. Two tests pin both paths. --------- Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
co-authored by
Paul Nothaft
parent
c5dc790e28
commit
219d07b04a
@@ -107,6 +107,7 @@ export const SsoTab: React.FC = () => {
|
||||
// (rightly) rejects an explicit true while SSO is off, and the promise
|
||||
// is that disabling SSO restores password login.
|
||||
oidc_disable_local_login: form.oidc_enabled ? form.oidc_disable_local_login : false,
|
||||
oidc_logout_from_idp: form.oidc_enabled ? form.oidc_logout_from_idp : false,
|
||||
};
|
||||
if (newSecret.trim()) payload.oidc_client_secret = newSecret.trim();
|
||||
saveMutation.mutate(payload);
|
||||
@@ -424,6 +425,35 @@ export const SsoTab: React.FC = () => {
|
||||
{t('settings.sso.policy.breakGlassHint', 'Locked out because the IdP is down or misconfigured? Set the environment variable OIDC_BREAK_GLASS=true on the backend and restart — password login comes back immediately.')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Logout-to-IdP (#798 phase 3) */}
|
||||
<label className="flex items-start gap-3 pt-1 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-0.5 rounded border-neutral-300 dark:border-neutral-600 text-accent focus:ring-primary-500"
|
||||
checked={form.oidc_logout_from_idp}
|
||||
onChange={(e) => set('oidc_logout_from_idp', e.target.checked)}
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{t('settings.sso.policy.logoutFromIdp', 'Also sign out of the identity provider')}
|
||||
</span>
|
||||
<span className="block text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{t('settings.sso.policy.logoutFromIdpHint', 'Logging out of PicPeak also ends the IdP session (RP-initiated logout). Only applies to sessions that signed in via SSO; without this, logging out of PicPeak leaves the IdP session alive and the next SSO click signs straight back in.')}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{form.oidc_logout_from_idp && form.post_logout_redirect_uri && (
|
||||
<div className="rounded-lg bg-neutral-50 dark:bg-neutral-800/60 p-3">
|
||||
<p className="text-xs font-medium text-neutral-600 dark:text-neutral-300 mb-1">
|
||||
{t('settings.sso.policy.postLogoutRedirectUri', 'Post-logout redirect URI (register this on your IdP client, e.g. Keycloak "Valid post logout redirect URIs")')}
|
||||
</p>
|
||||
<code className="block text-xs text-neutral-800 dark:text-neutral-200 break-all">
|
||||
{form.post_logout_redirect_uri}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user