fix(auth): preserve intentional logout across browser return
Keep a bounded per-tab logout intent and broadcast it to already open tabs so back navigation after a normal logout is not mistaken for renewal failure. Preserve real refresh and session expiry signals, and keep IdP navigation reachable when local cleanup fails.
This commit is contained in:
@@ -3,12 +3,17 @@ import type { ReactNode } from "react";
|
||||
|
||||
import { useAccessStore } from "@/store/accessStore";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import {
|
||||
clearIntentionalLogout,
|
||||
markIntentionalLogout,
|
||||
} from "@/lib/logoutIntent";
|
||||
import { App } from "./RefineContext";
|
||||
|
||||
let mockSessionState: {
|
||||
data: {
|
||||
accessToken: string;
|
||||
user: { id: string; name: string };
|
||||
error?: "RefreshAccessTokenError" | "SessionExpired";
|
||||
} | null;
|
||||
status: "authenticated" | "unauthenticated";
|
||||
} = {
|
||||
@@ -70,6 +75,7 @@ describe("RefineContext access authentication", () => {
|
||||
const originalRequest = global.Request;
|
||||
|
||||
beforeEach(() => {
|
||||
clearIntentionalLogout();
|
||||
mockPathname = "/network-simulation";
|
||||
mockProjectProviderBlocks = false;
|
||||
mockSessionState = {
|
||||
@@ -210,4 +216,63 @@ describe("RefineContext access authentication", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat returning to a protected route after logout as session expiry", async () => {
|
||||
markIntentionalLogout();
|
||||
mockSessionState = { data: null, status: "unauthenticated" };
|
||||
useAuthStore.setState({
|
||||
accessToken: null,
|
||||
sessionExpired: true,
|
||||
sessionExpiryReason: "refresh_failed",
|
||||
});
|
||||
|
||||
render(
|
||||
<App>
|
||||
<div>应用内容</div>
|
||||
</App>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useAuthStore.getState()).toMatchObject({
|
||||
sessionExpired: false,
|
||||
sessionExpiryReason: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("does not hide a real refresh failure after an intentional logout", async () => {
|
||||
markIntentionalLogout();
|
||||
mockSessionState = {
|
||||
data: {
|
||||
accessToken: "expired-access-token",
|
||||
user: { id: "user-1", name: "Test User" },
|
||||
error: "RefreshAccessTokenError",
|
||||
},
|
||||
status: "authenticated",
|
||||
};
|
||||
|
||||
const { rerender } = render(
|
||||
<App>
|
||||
<div>应用内容</div>
|
||||
</App>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useAuthStore.getState()).toMatchObject({
|
||||
sessionExpired: true,
|
||||
sessionExpiryReason: "refresh_failed",
|
||||
});
|
||||
});
|
||||
|
||||
mockSessionState = { data: null, status: "unauthenticated" };
|
||||
rerender(
|
||||
<App>
|
||||
<div>应用内容</div>
|
||||
</App>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useAuthStore.getState().sessionExpired).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,10 @@ import { useAccessStore } from "@/store/accessStore";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { apiFetch } from "@/lib/apiFetch";
|
||||
import { completeLogout, reportLogoutAudit } from "@/lib/logoutFlow";
|
||||
import {
|
||||
clearIntentionalLogout,
|
||||
isIntentionalLogout,
|
||||
} from "@/lib/logoutIntent";
|
||||
import { clearSessionRecoveryDrafts } from "@/lib/sessionRecoveryDraft";
|
||||
import { permissionCodes, resourcePermissions } from "@/lib/permissions";
|
||||
import { config } from "@config/config";
|
||||
@@ -88,15 +92,17 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.error === "SessionExpired") {
|
||||
clearIntentionalLogout();
|
||||
markSessionExpired("session_max_age");
|
||||
return;
|
||||
}
|
||||
if (data?.error === "RefreshAccessTokenError") {
|
||||
clearIntentionalLogout();
|
||||
markSessionExpired("refresh_failed");
|
||||
return;
|
||||
}
|
||||
if (status === "unauthenticated") {
|
||||
if (to === "/login") {
|
||||
if (to === "/login" || isIntentionalLogout()) {
|
||||
clearSessionExpired();
|
||||
return;
|
||||
}
|
||||
@@ -104,6 +110,7 @@ export const App = (props: React.PropsWithChildren<AppProps>) => {
|
||||
return;
|
||||
}
|
||||
if (status === "authenticated") {
|
||||
clearIntentionalLogout();
|
||||
clearSessionExpired();
|
||||
}
|
||||
}, [clearSessionExpired, data?.error, markSessionExpired, status, to]);
|
||||
|
||||
Reference in New Issue
Block a user