From 6e0a8f47725e2b56739f54ccbdf9b94d63021102 Mon Sep 17 00:00:00 2001 From: Chris Kehayias Date: Sat, 10 Oct 2026 14:58:59 -0400 Subject: [PATCH] feat(no-access): add a sign-out form for users with no MP security role Sign-in is not role-gated, so a role-less user has a session but, with the UserMenu gone, no way to end it short of the 12 h lifetime. /no-access now carries the same `handleSignOut` form as /session-error. Walked against a production build with the enforced CSP: MP logout, back to /signin, MP login prompt, zero violations. Docs updated. Co-Authored-By: Claude Opus 5.5 (1M context) --- .claude/references/auth/oauth-flow.md | 2 +- .claude/references/data-flow/call-graphs.md | 4 +-- .claude/references/security/README.md | 15 ++++++----- README.md | 2 +- src/app/(web)/no-access/page.test.tsx | 30 +++++++++++++++++---- src/app/(web)/no-access/page.tsx | 23 ++++++++++++---- src/components/shared-actions/README.md | 2 +- 7 files changed, 56 insertions(+), 22 deletions(-) diff --git a/.claude/references/auth/oauth-flow.md b/.claude/references/auth/oauth-flow.md index 26b6bd8..8becae6 100644 --- a/.claude/references/auth/oauth-flow.md +++ b/.claude/references/auth/oauth-flow.md @@ -17,7 +17,7 @@ id_token verification, profile mapping, and RP-initiated logout via `endsession` - `src/lib/env.ts` — `getMpBaseUrl()`, `getAuthBaseUrl()`, `getMpClient()` (validated env readers) - `src/app/signin/sign-in-content.tsx` — calls `authClient.signIn.social({ provider: "ministryplatform", callbackURL })` - `src/app/api/auth/[...all]/route.ts` — deny-by-default allowlist in front of `toNextJsHandler(auth)` -- `src/components/shared-actions/sign-out.ts` — `handleSignOut()` server action (the only caller is the `/session-error` form) +- `src/components/shared-actions/sign-out.ts` — `handleSignOut()` server action (called by the sign-out forms on `/no-access` and `/session-error`) - Tests: `src/auth.oidc-discovery.test.ts` (outages, lazy discovery), `src/auth.user-info.test.ts` (`getUserInfo` with signed tokens), `src/auth.id-token-sign-in.test.ts` (F12), `src/auth.shared-instance.test.ts`, `src/auth.test.ts` (config pins), `src/components/shared-actions/sign-out.test.ts`; mock MP in `src/test-utils/mock-oidc.ts` ## Key concepts diff --git a/.claude/references/data-flow/call-graphs.md b/.claude/references/data-flow/call-graphs.md index fd57a7a..bd0380a 100644 --- a/.claude/references/data-flow/call-graphs.md +++ b/.claude/references/data-flow/call-graphs.md @@ -188,8 +188,8 @@ last_verified: 2026-04-17 ## Flow 6: Sign-out (OIDC logout) -**Trigger:** user submits the "Sign out" form on `/session-error` — the only sign-out control in the app (the dropdown `UserMenu` was removed 2026-10-10). -**Entry point:** `src/app/session-error/page.tsx` — `
`. +**Trigger:** user submits a sign-out form — on `/no-access` (signed in, no MP security role) or `/session-error` (session without `userGuid`). These are the app's only sign-out controls (the dropdown `UserMenu` was removed 2026-10-10). +**Entry points:** `src/app/(web)/no-access/page.tsx` and `src/app/session-error/page.tsx` — ``. **Stack trace:** 1. `src/components/shared-actions/sign-out.ts` — `handleSignOut` (`'use server'`). diff --git a/.claude/references/security/README.md b/.claude/references/security/README.md index 5eebce0..d5e13ac 100644 --- a/.claude/references/security/README.md +++ b/.claude/references/security/README.md @@ -24,10 +24,10 @@ policy. Read this before changing anything under `src/lib/auth.ts`, Sign-in is deliberately **not** role-gated — no check in `getUserInfo`, `mapProfileToUser`, `customSession` or `AuthWrapper` — so a role-less user still gets a session and the app shell. They are redirected from `/tools/*` to -`/no-access`, which renders inside the shell. **There is no sign-out control on -`/no-access`** — the only one in the app is the `/session-error` form (the -dropdown `UserMenu` was removed 2026-10-10). Such a session ends at the 12 h -hard lifetime or when the browser drops the cookie. +`/no-access`, which renders inside the shell and carries a **Sign out** form +(`handleSignOut`) so they can end the session, e.g. to sign in as a different +MP user. That form and the one on `/session-error` are the app's only sign-out +controls (the dropdown `UserMenu` was removed 2026-10-10). ### Why authentication is not enough @@ -256,9 +256,10 @@ against a production build before calling it done. Last walk: **2026-10-10**, enforced policy, production build — sign-in, every tool, Radix select/popover/command and dialog (scroll-lock `