feat(auth): add generic OIDC login with mandatory groups

This commit is contained in:
2026-08-17 05:44:10 +02:00
parent 202822f3ba
commit 4fe51cbeb1
20 changed files with 1144 additions and 40 deletions
+2 -4
View File
@@ -76,14 +76,12 @@ describe("LoginPage", () => {
expect(screen.getByLabelText(/password/i)).toHaveValue("");
});
test("shows OIDC only when public configuration enables it and uses same-origin navigation", () => {
test("shows OIDC only when public configuration enables it", () => {
const { rerender } = render(<LoginPage config={localConfig} onAuthenticated={vi.fn()} />);
expect(screen.queryByRole("link", { name: /single sign-on/i })).not.toBeInTheDocument();
rerender(<LoginPage config={oidcConfig} onAuthenticated={vi.fn()} />);
expect(screen.getByRole("link", { name: /single sign-on/i })).toHaveAttribute(
"href", "/api/auth/oidc/login",
);
expect(screen.getByRole("button", { name: /single sign-on/i })).toBeEnabled();
});
test("does not dispatch local login until an in-flight logout response settles", async () => {
+12 -1
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import type { FormEvent } from "react";
import { AlertTriangle, ArrowRight, LockKeyhole } from "lucide-react";
import { ApiError } from "../api/client";
import { loginLocal } from "../api/auth";
import { beginOidcLogin, loginLocal } from "../api/auth";
import type { AuthenticatedUser, AuthPublicConfig } from "../api/types";
import { Button } from "../components/ui/button";
@@ -24,6 +24,7 @@ function loginError(error: unknown): { message: string; retry: boolean } {
export function LoginPage({ config, onAuthenticated, onRetry }: LoginPageProps) {
const localLogin = config.mode === "local" && config.localLogin;
const oidcLogin = config.mode === "oidc" && config.oidcLogin;
const formRef = useRef<HTMLFormElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
const mountedRef = useRef(true);
@@ -65,6 +66,10 @@ export function LoginPage({ config, onAuthenticated, onRetry }: LoginPageProps)
}
}
function startOidcLogin() {
void beginOidcLogin();
}
return (
<main className="min-h-screen bg-background px-5 py-8 text-foreground sm:px-8 sm:py-12">
<div className="mx-auto grid min-h-[calc(100vh-4rem)] max-w-5xl items-center gap-12 lg:grid-cols-[minmax(0,1fr)_26rem]">
@@ -138,6 +143,12 @@ export function LoginPage({ config, onAuthenticated, onRetry }: LoginPageProps)
</Button>
</form>
)}
{oidcLogin && (
<Button type="button" size="lg" className="w-full" onClick={startOidcLogin}>
Continue with single sign-on
<ArrowRight aria-hidden="true" />
</Button>
)}
{config.oidcLogin && (
<a