fix(auth): prevent logout audit from blocking redirect
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { completeLogout, reportLogoutAudit } from "./logoutFlow";
|
||||
|
||||
describe("reportLogoutAudit", () => {
|
||||
it("starts an authenticated keepalive request", async () => {
|
||||
const fetcher = jest.fn(async () => ({ ok: true }) as Response);
|
||||
|
||||
await reportLogoutAudit({
|
||||
endpoint: "https://server.example/api/v1/audit-events",
|
||||
accessToken: "access-token",
|
||||
fetcher,
|
||||
});
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetcher.mock.calls[0];
|
||||
expect(url).toBe("https://server.example/api/v1/audit-events");
|
||||
expect(init).toMatchObject({
|
||||
method: "POST",
|
||||
body: JSON.stringify({ event: "logout" }),
|
||||
keepalive: true,
|
||||
});
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe(
|
||||
"Bearer access-token",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("completeLogout", () => {
|
||||
it("navigates immediately without waiting for audit reporting", async () => {
|
||||
const reportAudit = jest.fn(
|
||||
() => new Promise<unknown>(() => undefined),
|
||||
);
|
||||
const clearLocalState = jest.fn();
|
||||
const navigate = jest.fn();
|
||||
|
||||
const result = completeLogout({
|
||||
reportAudit,
|
||||
clearLocalState,
|
||||
navigate,
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(reportAudit).toHaveBeenCalledTimes(1);
|
||||
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
||||
await expect(result).resolves.toEqual({ success: true });
|
||||
});
|
||||
|
||||
it("completes logout when audit reporting fails", async () => {
|
||||
const reportAudit = jest.fn(() => Promise.reject(new Error("offline")));
|
||||
const clearLocalState = jest.fn();
|
||||
const navigate = jest.fn();
|
||||
|
||||
await expect(
|
||||
completeLogout({ reportAudit, clearLocalState, navigate }),
|
||||
).resolves.toEqual({ success: true });
|
||||
|
||||
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
||||
});
|
||||
|
||||
it("completes logout when audit reporting throws synchronously", async () => {
|
||||
const clearLocalState = jest.fn();
|
||||
const navigate = jest.fn();
|
||||
|
||||
await expect(
|
||||
completeLogout({
|
||||
reportAudit: () => {
|
||||
throw new Error("invalid request");
|
||||
},
|
||||
clearLocalState,
|
||||
navigate,
|
||||
}),
|
||||
).resolves.toEqual({ success: true });
|
||||
|
||||
expect(clearLocalState).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith("/api/auth/keycloak-logout");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
const KEYCLOAK_LOGOUT_PATH = "/api/auth/keycloak-logout";
|
||||
|
||||
type CompleteLogoutOptions = {
|
||||
reportAudit: () => Promise<unknown>;
|
||||
clearLocalState: () => void;
|
||||
navigate: (path: string) => void;
|
||||
};
|
||||
|
||||
type LogoutAuditOptions = {
|
||||
endpoint: string;
|
||||
accessToken?: string | null;
|
||||
fetcher?: typeof fetch;
|
||||
};
|
||||
|
||||
export const reportLogoutAudit = ({
|
||||
endpoint,
|
||||
accessToken,
|
||||
fetcher = fetch,
|
||||
}: LogoutAuditOptions) => {
|
||||
const headers = new Headers({ "Content-Type": "application/json" });
|
||||
if (accessToken) {
|
||||
headers.set("Authorization", `Bearer ${accessToken}`);
|
||||
}
|
||||
|
||||
return fetcher(endpoint, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ event: "logout" }),
|
||||
keepalive: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const completeLogout = async ({
|
||||
reportAudit,
|
||||
clearLocalState,
|
||||
navigate,
|
||||
}: CompleteLogoutOptions) => {
|
||||
try {
|
||||
void reportAudit().catch(() => undefined);
|
||||
} catch {
|
||||
// Synchronous reporting errors must not block logout either.
|
||||
}
|
||||
|
||||
clearLocalState();
|
||||
navigate(KEYCLOAK_LOGOUT_PATH);
|
||||
return { success: true as const };
|
||||
};
|
||||
Reference in New Issue
Block a user