mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:21:29 +00:00
auth-bundle-2 Phase 8: GUI auth surface (OIDC providers + group mappings + sessions + LoginPage IdP buttons + AuthState refactor + logout wiring)
Closes Phase 8 of cowork/auth-bundle-2-prompt.md. Every Bundle 2 endpoint
now has a permission-gated, data-testid-instrumented React surface.
Frontend changes
================
api/client.ts (Category H — AuthState refactor):
* fetchJSON now sends `credentials: 'include'` on every request so the
HttpOnly session cookie + the JS-readable CSRF cookie ride along with
Bearer-mode requests transparently. Mode is determined per call by
what cookies are present, NOT by a state-machine — the same client
works for Bearer-only deploys, session-only deploys, and the mixed
upgrade path described in cowork/auth-bundles-index.md Category H.
* readCSRFCookie() + isStateChangingMethod() helpers auto-attach
`X-CSRF-Token` to POST/PUT/PATCH/DELETE when the CSRF cookie exists.
Bearer-only callers ride through unchanged (no CSRF cookie → no
header → backend's CSRF middleware skips).
* AuthInfoResponse extended with optional `oidc_providers?:
AuthInfoOIDCProvider[]` matching the Phase 6 server extension.
* New API helpers (1:1 with Phase 5 / 7.5 endpoints):
- listOIDCProviders / createOIDCProvider / updateOIDCProvider /
deleteOIDCProvider / refreshOIDCProvider
- listGroupMappings / addGroupMapping / removeGroupMapping
- listSessions(actorID?, actorType?) / revokeSession / logout
- breakglassLogin / breakglassSetPassword / breakglassUnlock /
breakglassRemove
Permission gates fire server-side; the GUI predicates are UX only.
pages/auth/OIDCProvidersPage.tsx (NEW):
* Lists configured OIDC providers, gated on `auth.oidc.list`.
* Empty state + error state + loading state.
* Embedded Configure-Provider modal with form fields for name,
issuer_url, client_id, client_secret, redirect_uri,
groups_claim_path/format, fetch_userinfo, scopes. Modal hidden
unless caller has `auth.oidc.create`.
* Unsaved-changes confirmation on cancel.
pages/auth/OIDCProviderDetailPage.tsx (NEW):
* Provider config dl + edit/delete/refresh action buttons.
* Edit and refresh require `auth.oidc.edit`. Delete requires
`auth.oidc.delete`.
* Type-confirm-name delete dialog. Surfaces server's 409 Conflict
("ErrOIDCProviderInUse") inline so the operator knows to revoke
the provider's active sessions first.
* Refresh discovery cache button → POST .../refresh → server re-runs
RefreshKeys with the IdP-downgrade-attack defense from Phase 3.
* Group→role mappings link.
pages/auth/GroupMappingsPage.tsx (NEW):
* Per-provider group-claim → role-id mapping CRUD.
* Empty state explains the fail-closed semantics from Phase 3
(no mappings ⇒ no users authenticate via this provider).
* Inline add form (group_name input + role_id select populated from
`authListRoles`); add/remove gated on `auth.oidc.edit`.
pages/auth/SessionsPage.tsx (NEW):
* Default "My sessions" view available to anyone holding
`auth.session.list`.
* "All actors (admin)" toggle exposed only when caller holds
`auth.session.list.all`; renders an actor_id filter input that
threads ?actor_id= through the GET.
* Self-pill marker on the caller's own rows.
* Revoke button is shown when (a) the row is the caller's own session
(handler-side own-bypass) OR (b) caller holds `auth.session.revoke`.
* Confirms via window.confirm; surfaces revocation errors inline.
pages/LoginPage.tsx (MODIFIED):
* Fetches /v1/auth/info on mount; if `oidc_providers[]` is non-empty,
renders one "Sign in with X" button per provider linking to the
provider's `login_url` (the server-side handler in Phase 5 builds
this URL with state + nonce + PKCE verifier sealed in the pre-login
cookie; the GUI never touches those values).
* The API-key form remains as a fallback for Bearer-mode deploys and
the Phase 7.5 break-glass path.
* All interactive elements carry data-testid:
login-oidc-providers / login-oidc-button-{id} / login-api-key-form /
login-api-key-input / login-api-key-submit.
components/AuthProvider.tsx (MODIFIED):
* logout() now also fires POST /auth/logout via the api/client helper
before clearing local state. The endpoint is auth-exempt; the
catch-and-swallow keeps the local logout flow working even if the
cookie is already invalid (idempotent server-side as well).
components/Layout.tsx (MODIFIED):
* Two new nav entries under the Auth section: "OIDC Providers" + "Sessions".
main.tsx (MODIFIED):
* Four new routes:
- /auth/oidc/providers
- /auth/oidc/providers/:id
- /auth/oidc/providers/:id/mappings
- /auth/sessions
Vitest coverage
===============
Five new test files, 28 new test cases. Pattern matches Bundle 1
Phase 10's Vitest scaffold (vi.mock api/client, render with
QueryClient + MemoryRouter, authMe-driven permission shaping,
data-testid selectors).
* OIDCProvidersPage.test.tsx (5 tests): ErrorState w/o auth.oidc.list,
empty state, list + create button render, hide-create-button
without auth.oidc.create, submit-creates-via-API.
* OIDCProviderDetailPage.test.tsx (5 tests): ErrorState w/o list,
full-perms render, hide edit/refresh/delete with only list,
refresh button calls API, delete confirm-button stays disabled
until typed text matches provider name.
* GroupMappingsPage.test.tsx (5 tests): ErrorState w/o list, empty
fail-closed warning, mapping rows render, hide-form without
auth.oidc.edit, submit-add-form-calls-API.
* SessionsPage.test.tsx (6 tests): ErrorState w/o list, own sessions
+ self-pill, hide All-actors toggle without list.all, show
toggle with list.all, hide revoke on other-actor sessions without
auth.session.revoke, click-revoke calls API after window.confirm.
* LoginPage.test.tsx (extended +2 tests): renders OIDC buttons when
/auth/info reports providers; omits the OIDC block when none.
Verification
============
* `npx tsc --noEmit` — 0 errors.
* Vitest run across api/components/hooks/utils/auth/pages = 475 tests,
all green.
* `npm run build` — green (980 KB bundle, no surprises vs Phase 7).
* No backend (Go) changes in this commit; Phase 5-7.5 surfaces
consumed unchanged.
Not in this commit (deferred)
=============================
* "Test login flow" button on the provider detail page (prompt §Phase 8
optional row). Requires a server-side test=true flag on the OIDC
login handler — out of scope for the GUI commit.
* `web/src/__tests__/e2e/` Keycloak-via-testcontainers harness for the
15 comprehensive flow checks. Tracked under Phase 10 of
cowork/auth-bundle-2-prompt.md.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { getAuthInfo, checkAuth, setApiKey } from '../api/client';
|
||||
import { getAuthInfo, checkAuth, setApiKey, logout as apiLogout } from '../api/client';
|
||||
|
||||
interface AuthState {
|
||||
loading: boolean;
|
||||
@@ -96,6 +96,11 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
// Bundle 2 Phase 8 — fire POST /auth/logout so the server can revoke the
|
||||
// session row + clear the HttpOnly session cookie. The API logout helper
|
||||
// sends `credentials: 'include'`. Errors are swallowed (the user's intent
|
||||
// is still to be logged out locally; e.g. cookie already expired).
|
||||
void apiLogout().catch(() => undefined);
|
||||
setApiKey(null);
|
||||
setAuthenticated(false);
|
||||
setUser('');
|
||||
|
||||
@@ -27,6 +27,9 @@ const nav = [
|
||||
{ to: '/est', label: 'EST Admin', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' },
|
||||
{ to: '/audit', label: 'Audit Trail', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
// Bundle 1 Phase 10 — RBAC management (Roles / Keys / Settings).
|
||||
// Bundle 2 Phase 8 — OIDC + Sessions.
|
||||
{ to: '/auth/oidc/providers', label: 'OIDC Providers', icon: 'M12 11c0 3.517-1.009 6.799-2.753 9.571m-3.44-2.04l.054-.09A13.916 13.916 0 008 11a4 4 0 118 0c0 1.017-.07 2.019-.203 3m-2.118 6.844A21.88 21.88 0 0015.171 17m3.839 1.132c.645-2.266.99-4.659.99-7.132A8 8 0 008 4.07M3 15.364c.64-1.319 1-2.8 1-4.364 0-1.457.39-2.823 1.07-4' },
|
||||
{ to: '/auth/sessions', label: 'Sessions', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
{ to: '/auth/roles', label: 'Roles', icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z' },
|
||||
{ to: '/auth/keys', label: 'API Keys', icon: 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z' },
|
||||
{ to: '/auth/approvals', label: 'Approvals', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
|
||||
Reference in New Issue
Block a user