feat(auth): add generic OIDC login with mandatory groups
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { beginOidcLogin, logout } from "./auth";
|
||||
import { server } from "../test/msw";
|
||||
import { clearAuthState, setAuthState } from "../auth/authState";
|
||||
|
||||
const user = {
|
||||
issuer: "local", subject: "user-a", roles: ["user"] as const,
|
||||
permissions: ["session.use"], isAdmin: false, csrfToken: "a".repeat(43), session: null,
|
||||
};
|
||||
|
||||
function deferred() {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((onResolve) => { resolve = onResolve; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearAuthState();
|
||||
setAuthState(user);
|
||||
});
|
||||
|
||||
test("OIDC navigation waits for a successful pending logout response", async () => {
|
||||
const gate = deferred();
|
||||
let logoutStarted!: () => void;
|
||||
const started = new Promise<void>((resolve) => { logoutStarted = resolve; });
|
||||
server.use(http.post("/api/auth/logout", async () => {
|
||||
logoutStarted();
|
||||
await gate.promise;
|
||||
return new HttpResponse(null, { status: 204 });
|
||||
}));
|
||||
const logoutPromise = logout();
|
||||
await started;
|
||||
const navigate = vi.fn();
|
||||
const oidc = beginOidcLogin(navigate);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
gate.resolve();
|
||||
await Promise.all([logoutPromise, oidc]);
|
||||
expect(navigate).toHaveBeenCalledOnce();
|
||||
expect(navigate).toHaveBeenCalledWith("/api/auth/oidc/login");
|
||||
});
|
||||
|
||||
test("OIDC navigation waits for a failed pending logout response before continuing", async () => {
|
||||
const gate = deferred();
|
||||
let logoutStarted!: () => void;
|
||||
const started = new Promise<void>((resolve) => { logoutStarted = resolve; });
|
||||
server.use(http.post("/api/auth/logout", async () => {
|
||||
logoutStarted();
|
||||
await gate.promise;
|
||||
return HttpResponse.json({ code: "auth_unavailable" }, { status: 503 });
|
||||
}));
|
||||
const logoutPromise = logout().catch(() => undefined);
|
||||
await started;
|
||||
const navigate = vi.fn();
|
||||
const oidc = beginOidcLogin(navigate);
|
||||
|
||||
await Promise.resolve();
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
gate.resolve();
|
||||
await Promise.all([logoutPromise, oidc]);
|
||||
expect(navigate).toHaveBeenCalledWith("/api/auth/oidc/login");
|
||||
});
|
||||
@@ -6,6 +6,7 @@ const authModes = new Set<AuthPublicConfig["mode"]>(["local", "oidc", "upstream"
|
||||
const roles = new Set<AuthRole>(["user", "admin"]);
|
||||
const sessionMethods = new Set<AuthSessionInfo["method"]>(["local", "oidc", "upstream"]);
|
||||
let pendingLogoutResponse: Promise<void> | null = null;
|
||||
const oidcLoginPath = "/api/auth/oidc/login";
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
@@ -86,6 +87,23 @@ export async function loginLocal(username: string, password: string, remember: b
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* OIDC creates its browser session through a top-level same-origin navigation. A previous logout
|
||||
* response may still carry a clearing Set-Cookie, so it must settle before this navigation begins.
|
||||
*/
|
||||
export async function beginOidcLogin(navigate: (path: string) => void = (path) => window.location.assign(path)): Promise<void> {
|
||||
const pending = pendingLogoutResponse;
|
||||
if (pending) {
|
||||
try {
|
||||
await pending;
|
||||
} catch {
|
||||
// A failed logout must release the coordinator; the current browser cookie remains the
|
||||
// backend's authority during the following OIDC handshake.
|
||||
}
|
||||
}
|
||||
navigate(oidcLoginPath);
|
||||
}
|
||||
|
||||
export async function logout(): Promise<boolean> {
|
||||
const logoutGeneration = getAuthGeneration();
|
||||
if (pendingLogoutResponse) {
|
||||
|
||||
Reference in New Issue
Block a user