mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 22:58:52 +00:00
fix(auth/ux): cause-aware OIDC + session error surfacing (HIGH-7 + HIGH-8 closure)
Server (HIGH-7): the OIDC callback failure path now 302-redirects to /login?error=oidc_failed&reason=<category> instead of emitting a blank 400. `category` is the existing audit `failure_category` value; classifyOIDCFailure was extended with three new sentinel paths (email_domain_not_allowed, email_missing_but_required, pkce_invalid) so CRIT-5 + PKCE failures get distinguishable GUI rendering. Audit-log observability is unchanged — the same failure_category is written to the auth.oidc_login_failed audit row; the 302 is purely a UX leg layered on top. Server (HIGH-8): SessionMiddleware now stashes a cause classification on the request context when Validate returns an error, mapping the sentinels via classifySessionError (errors.Is-based, so wrapped sentinels still classify) to the stable wire-strings idle_timeout / absolute_timeout / back_channel_revoked / invalid_token. The 401 emit point in bearerSkipIfAuthenticated reads the stashed cause and emits WWW-Authenticate: Bearer realm="certctl", error="invalid_token", error_description=<cause> per RFC 6750 §3. GUI (HIGH-7): LoginPage reads ?error= + ?reason= from the URL via react-router useSearchParams and renders an operator-friendly amber-bordered banner above the form; OIDC_FAILURE_REASON_TEXT maps all 16 known categories with a defensive 'unspecified' fallback for forward-compat with future server-side categories. GUI (HIGH-8): api/client fetchJSON parses the WWW-Authenticate cause via parseWWWAuthenticateCause and attaches it to the 'certctl:auth-required' CustomEvent detail; AuthProvider redirects to /login?session_expired=<cause> on cause-aware 401s; LoginPage renders a blue-bordered session-cause banner. invalid_token stays on the current page (no hard redirect for opaque failures). Misc cleanup: ErrorState now accepts the title/message/data-testid form added by CRIT-4 BreakglassPage (was erroring tsc on master). Regression matrix: - internal/api/handler/oidc_redirect_categories_test.go pins all 16 failure categories to the 302 + reason= location + audit-row leg - internal/auth/session/www_authenticate_test.go pins the 4 stable cause categories on classifySessionError (incl. errors.Is wrapped sentinels) + the WWW-Authenticate emission across all 4 categories + the no-session-context fallback case - internal/api/handler/auth_session_oidc_test.go: 4 pre-existing TestLoginCallback_*Returns400 tests updated to assert 302 + reason= location (the wire shape changed from 400 to 302, but the audit observability and behaviour-equivalent failure-classification are preserved) - web/src/pages/LoginPage.test.tsx: 6 new cases pinning the failure banner, session-cause banner, unknown-reason fallback, and forward-compat 'unspecified' category Spec: cowork/auth-bundles-fixes-2026-05-10/08-high-7-8-error-surfacing.md Closes: HIGH-7, HIGH-8 of cowork/auth-bundles-audit-2026-05-10.md
This commit is contained in:
@@ -66,14 +66,35 @@ export default function AuthProvider({ children }: { children: ReactNode }) {
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// Listen for 401 events from the API client
|
||||
// Listen for 401 events from the API client.
|
||||
//
|
||||
// Audit 2026-05-10 HIGH-8 — the API client now attaches a cause
|
||||
// category to the event detail (parsed from the WWW-Authenticate
|
||||
// header). When a cause is recognised, redirect to
|
||||
// /login?session_expired=<cause> so the LoginPage renders OIDC-aware
|
||||
// re-login wording instead of the generic "session expired" + API-key
|
||||
// copy. Cookie-mode (OIDC) and Bearer-mode (API-key) callers share
|
||||
// the same wire shape; the LoginPage banner is purely UX.
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<{ cause?: string }>).detail;
|
||||
const cause = detail?.cause || '';
|
||||
setAuthenticated(false);
|
||||
setApiKey(null);
|
||||
setUser('');
|
||||
setAdmin(false);
|
||||
// Generic copy; the LoginPage will overlay a cause-specific
|
||||
// banner when ?session_expired=<cause> is present.
|
||||
setError('Session expired. Please re-enter your API key.');
|
||||
// Forward the cause to the LoginPage. window.location is used
|
||||
// (not React Router's navigate) because this listener fires
|
||||
// outside any route component's render and we want a hard
|
||||
// navigation that clears any stale state.
|
||||
if (cause && cause !== 'invalid_token' &&
|
||||
window.location.pathname !== '/login') {
|
||||
const params = new URLSearchParams({ session_expired: cause });
|
||||
window.location.href = '/login?' + params.toString();
|
||||
}
|
||||
};
|
||||
window.addEventListener('certctl:auth-required', handler);
|
||||
return () => window.removeEventListener('certctl:auth-required', handler);
|
||||
|
||||
Reference in New Issue
Block a user