Repository navigation
fix(auth): refuse aal1 tokens from users with a verified MFA factor - #177
Conversation
A password or a recovery link alone gave an aal1 token that could read every query and run R1 and R2 commands; only R3 asked for the second factor. With AUTH_SESSION_CHECK=db the per-request session check now takes the token's assurance level and reads whether the user has a verified factor in auth.mfa_factors, through a definer owned by crove_auth_reader. An aal1 token of such a user gets 401 MFA_REQUIRED on every request. The auth-grants step grants exactly auth.mfa_factors (user_id, status) and a SELECT policy; auth_grants_ready requires them whenever the table exists, so a missing grant fails closed with 503. The four-argument session_check is dropped. The SPA no longer signs the user out on MFA_REQUIRED and shows the server's message until the MFA screens land. ADR-0028 and the plan record the rule and the requirement on the Cloud events producer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: auth-grants logs a warning when Supabase Auth has no auth.mfa_factors, the migration runner test covers the new factor reader, and ADR-0028 states that identity_resolve and AUTH_SESSION_CHECK=off do not enforce the rule and that the SPA challenge screen is still pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rebase Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 47 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements a security enhancement (issue #126) requiring users with a verified multi-factor authentication (MFA) factor to use an aal2 token for all requests, rather than just R3 operations. To support this, the database session_check function has been updated to accept the token's assurance level, and the auth-grants step now configures permissions for auth.mfa_factors. The changes also include client-side handling of the MFA_REQUIRED error, comprehensive documentation updates in ADR-0028 and the project plan, and a new invariant test suite. The reviewer suggested updating an error message in the migration script to accurately reflect that mfa_factors is now checked alongside other auth tables.
| log('WARNING: auth.mfa_factors does not exist, so the session check cannot require MFA for users who enrolled it; upgrade Supabase Auth and run auth-grants again'); | ||
| } | ||
| const { rows } = await client.query<{ ok: boolean }>('SELECT crove_kernel.auth_grants_ready() AS ok'); | ||
| if (!rows[0]?.ok) throw new Error('the session check still cannot read auth.sessions and auth.users after the grant'); |
There was a problem hiding this comment.
The error message is slightly outdated and misleading because auth_grants_ready() now also checks permissions on auth.mfa_factors if it exists. Updating the error message to include mfa_factors will make debugging easier if the grant fails on that table.
| if (!rows[0]?.ok) throw new Error('the session check still cannot read auth.sessions and auth.users after the grant'); | |
| if (!rows[0]?.ok) throw new Error('the session check still cannot read auth tables (sessions, users, or mfa_factors) after the grant'); |
Problem
A user with a verified TOTP factor could still use an
aal1access token (password only, or a recovery link) to call/apidirectly: every query and file, and R1 and R2 commands (upload, edit, reject, session revoke). Only R3 asked foraal2(P11).Design
AUTH_SESSION_CHECK=db).crove_kernel.session_checknow takes the token's assurance level (p_aal, 5 arguments; the 4-argument version is dropped so no caller can skip the check). After the upstream session check passes, anaal1token whose user has a verified factor raises28000 MFA_REQUIRED. The kernel answers it as 401MFA_REQUIRED. The check runs in the request transaction before any write, so reads, R1 and R2 commands, uploads, downloads and org-less calls (me/get,org/create) are all refused, and nothing is written.crove_kernel.auth_user_has_verified_factor(uuid)is a SECURITY DEFINER owned bycrove_auth_reader, likeauth_session_valid. It has no EXECUTE forcrove_appor PUBLIC and pinssearch_path. Any verified factor counts, of any type.auth.mfa_factors (user_id, status)plus a SELECT policy (Supabase Auth enables RLS on the table). The factor secrets are never granted.auth_grants_ready()requires that grant and policy whenever the table exists. A missing grant or policy therefore fails closed with 503AUTH_NOT_READY, as on the existing path.auth-grantslogs a warning. Rationale: Supabase Auth createsauth.mfa_factorsin a migration that runs before the one addingauth.sessions.not_after, whichauth-grantsalready waits for. I checked this against theauth.schema_migrationsorder of the pinnedsupabase/gotrue:v2.197.0. A Supabase Auth without the table has no MFA at all.mfa_factors.UNAUTHENTICATED, and the SPA signs it out. Only a live session that has not passed MFA getsMFA_REQUIRED.isUnauthenticated()no longer treatsMFA_REQUIREDas a sign-out, and queries do not retry it./mfaroute onmainyet (PR feat: SPA auth flows: PKCE callbacks, password recovery, TOTP MFA and step-up #101, issue Auth flows in the SPA: password recovery, MFA enrollment and callbacks #32), so for now the user sees the server's Vietnamese message ("Tài khoản này đã bật xác thực hai lớp (MFA): hãy nhập mã xác thực để tiếp tục")./mfaonisMfaRequired.jwt, since feat: Cloud sessions use the Supabase default with live checks for R3 #149).aal1access token does not say whether its user has a verified factor, and Crove cannot read a remote Supabase Auth. ADR-0028 section 3 now records the requirement: the identity provider must state the fact in the token it signs (for example, a claim from a custom access token hook) or not issue such tokens.jwtasks for the second factor only on R3, and gate T1 keeps this item open for Cloud.identity_resolvestill maps anaal1token to its account. It writes only the email and name that the token itself carries, and it grants no access.AUTH_SESSION_CHECK=off(local URLs only) does not enforce the rule.Migration
20261016180000_crove_kernel_mfa_required.sql:SET search_path = pg_catalog, pg_tempon every function.20261015090000, and REVOKE/GRANT are restated.mainand every pending PR at push time: feat(finance): journal reversal and a property test of the posting path (T3) #164 uses 150000, Inbound: tell the owner how many messages to a retired address were dropped #139 160000, Upload pipeline per plan 7.6: staging key, presigned URLs, generation in file references #109 170000.Tests
New invariant file
apps/kernel/test/invariant/mfa-required.db.test.ts. It runs through the HTTP API on PostgreSQL 17, against a mirror ofauth.users,auth.sessionsandauth.mfa_factorswith the enum types and RLS of the pinned GoTrue. It checks:aal1token of a user with a verified factor is refused with 401MFA_REQUIREDon a read query (inbox/list,files/recent), an R1 command (sessions/revoke), an upload (kernel.file.ingest) andme/get. No file and no revoke is written.aal2token of the same kind of user passes all of them.aal1access.aal1token on the next request, and deleting the factor restores access.UNAUTHENTICATED, notMFA_REQUIRED.(user_id, status)and neversecret. The factor reader's owner, definer, search_path and EXECUTE are asserted, and the 4-argumentsession_checkis gone.AUTH_NOT_READY, and re-runningauth-grantsrestores it.Also:
packages/db/test/invariant/migrate.db.test.tscovers the new reader as a non-superuser runner.28000message mapping.apps/kernel/test/session-check-stale.db.test.ts(from fix(auth): session revocation follow-ups from #90 #166) now expects the 5-argument check.Mutation check: when
sessions.tswas made to always sendaal2, the refusal tests failed.Verification
pnpm lintandpnpm typecheck: clean.pnpm exec vitest run: 293 passed.pnpm exec vitest run --config vitest.invariant.config.ts: 15 files, 251 passed.vitest --project db: 289 passed, 5 skipped.pnpm docs:check: OK.main's copy of the invariant suite andvitest.invariant.config.tsagainst this PR's code, asInvariant suite (from base)does: 14 files, 244 passed.postgres:17container. On the shared dev cluster, other sessions' parallel test databases caused unrelatedtuple concurrently updatederrors inALTER ROLE.Docs
main), External-tenant gate rows T1 and T2, and the T1 row in the Vietnamese page.Closes #126
🤖 Generated with Claude Code