fix(ui): proper OIDC logout — server revoke + top-level redirect
Previous logout fired fetch(end_session, {mode:'no-cors'}), which is a
no-op for OIDC: cross-origin fetch never clears the IdP's session cookie.
Result: subsequent SSO clicks silently re-authenticated the prior user.
New flow:
1. Best-effort POST /auth/logout to bump token_revoked_before.
2. Clear localStorage + Zustand state.
3. Set sessionStorage 'cameleer:signed_out=1' so /login renders a
confirmation splash (mirrors cameleer-saas pattern).
4. window.location.replace(end_session_endpoint?id_token_hint=...
&post_logout_redirect_uri=...&client_id=...) — top-level navigation,
the only form that actually clears the IdP session cookie.
client_id is now persisted at OIDC initiation alongside
end_session_endpoint and id_token, so logout has all three params
without an extra round-trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,9 @@ export function LoginPage() {
|
||||
if (data.endSessionEndpoint) {
|
||||
localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint);
|
||||
}
|
||||
if (data.clientId) {
|
||||
localStorage.setItem('cameleer-oidc-client-id', data.clientId);
|
||||
}
|
||||
const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`;
|
||||
const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(data.additionalScopes || [])];
|
||||
const params = new URLSearchParams({
|
||||
|
||||
@@ -13,7 +13,7 @@ interface AuthState {
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
loginWithOidcCode: (code: string, redirectUri: string) => Promise<void>;
|
||||
refresh: () => Promise<boolean>;
|
||||
logout: () => void;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
function parseRolesFromJwt(token: string): string[] {
|
||||
@@ -140,12 +140,26 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
logout: async () => {
|
||||
const accessToken = get().accessToken;
|
||||
const endSessionEndpoint = localStorage.getItem('cameleer-oidc-end-session');
|
||||
const idToken = localStorage.getItem('cameleer-oidc-id-token');
|
||||
const clientId = localStorage.getItem('cameleer-oidc-client-id');
|
||||
|
||||
// Best-effort server-side revocation. Don't fail logout if it errors —
|
||||
// the SPA-side cleanup below is authoritative for the SPA.
|
||||
if (accessToken) {
|
||||
try {
|
||||
await api.POST('/auth/logout', {});
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
clearTokens();
|
||||
localStorage.removeItem('cameleer-oidc-end-session');
|
||||
localStorage.removeItem('cameleer-oidc-id-token');
|
||||
localStorage.removeItem('cameleer-oidc-client-id');
|
||||
set({
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
@@ -154,17 +168,24 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
isAuthenticated: false,
|
||||
error: null,
|
||||
});
|
||||
const loginUrl = `${config.basePath}login`;
|
||||
|
||||
// Tell the upcoming /login render that this is a post-logout landing,
|
||||
// not a fresh visit. Mirrors cameleer-saas ui/src/auth/useAuth.ts.
|
||||
sessionStorage.setItem('cameleer:signed_out', '1');
|
||||
|
||||
const localLoginUrl = `${config.basePath}login`;
|
||||
|
||||
if (endSessionEndpoint && idToken) {
|
||||
// OIDC RP-Initiated Logout 1.0: top-level navigation, NOT fetch.
|
||||
// Cross-origin fetch never clears the IdP's session cookie.
|
||||
const params = new URLSearchParams({
|
||||
id_token_hint: idToken,
|
||||
post_logout_redirect_uri: `${window.location.origin}${config.basePath}login`,
|
||||
});
|
||||
fetch(`${endSessionEndpoint}?${params}`, { mode: 'no-cors' }).finally(() => {
|
||||
window.location.href = loginUrl;
|
||||
});
|
||||
if (clientId) params.set('client_id', clientId);
|
||||
window.location.replace(`${endSessionEndpoint}?${params}`);
|
||||
} else {
|
||||
window.location.href = loginUrl;
|
||||
window.location.replace(localLoginUrl);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user