Skip to content

Tech-debt batch 14: auth — OAuth email proof, verified invites, effective-permission admin guards (#622 #623 #624) - #647

Merged
antosubash merged 6 commits into
mainfrom
fix/debt-batch-14
Oct 24, 2026
Merged

antosubash merged 6 commits into
mainfrom
fix/debt-batch-14

Conversation

@antosubash

Copy link
Copy Markdown
Owner

Tech-debt batch 14 — authentication/authorization fixes from the 2026-10-24 scan.

  • users: OIDC sign-in links to an existing account by an unverified email #622 OAuth account takeover by unverified email. The callback linked any provider identity to an existing account by email and marked new accounts verified. Now only a provider's proof counts: OIDC email_verified: true, Google People API metadata.verified on the primary email, a verified GitHub address from /user/emails; Microsoft (Entra) email claims never count ("nOAuth"). An unverified account — SSO-provisioned ones included, pending invites excepted — never absorbs a later OAuth login (closes a pre-hijacking path raised in review).
  • tenants: invitations can be accepted by an account whose email is unverified #623 Tenant invitations accepted by an unverified address. Acceptance now requires a verified email, read fresh through the user directory (new DirectoryUser.email_verified, None = provider doesn't track it → Keycloak's email_verified claim), so verifying after sign-in works without re-signing in. Distinct invitation_email_unverified error (en/de).
  • users: users.manage can grant admin; no last-admin guard on disable/demote #624 Delegated admin escalation. users/admin/safety.py: outside a full admin session, a role can be granted and an account managed only when its effective permissions (roles + direct grants, token scopes applied) are within the actor's. Delegates and admins' scoped tokens can't grant admin, can't touch admin/superuser accounts, can't grant roles mapped to permissions they lack. Nobody disables/demotes themselves or the last active admin (row-locked on Postgres).

Behaviour changes (CHANGELOG): providers without email proof can't sign in to an existing account by email alone (400 OAUTH_USER_ALREADY_EXISTS); with require_verification off, self-registered accounts must verify before accepting an invite.

Verification

  • Red/green per issue (e.g. users: users.manage can grant admin; no last-admin guard on disable/demote #624: 28 of 36 new tests fail on the old service; pre-hijack test fails without the guard).
  • make lint ✓ · make test-py (8 workers) ✓ 6082 passed · make test-js ✓ 770 · Postgres (admin guards, effective permissions, tenants, OAuth) ✓.
  • /ship: review pass 1 (reworked users: users.manage can grant admin; no last-admin guard on disable/demote #624 to effective permissions after an automated security finding; demo PAT test retargeted to an account within the token's scopes) → pass 2 clean → QA: delegated helpdesk admin refused on self-grant/admin edits/stronger grants, self-disable and last-admin guards (UI + API); unverified invitee refused then accepted in the same session once verified; OIDC against a real Keycloak: unverified-email identity refused (no link), verified identity linked to the existing account → round-2 review clean.

Follow-ups (P3, filed separately): admin UI toasts drop the server's detail; OIDC refusal renders raw JSON.

Closes #622
Closes #623
Closes #624

https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4

…s email-verification proof (#622)

OIDC requires email_verified=true, Google the People API metadata.verified of the
primary email, GitHub a verified entry in /user/emails; Microsoft (Entra) email
claims are never proof. Existing subject links keep working.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…h from the user directory (#623)

Adds DirectoryUser.email_verified (None = provider doesn't track it); the users
directory fills it from is_verified, Keycloak principals carry the token's
email_verified claim as the fallback.

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
…thority; keep a last admin (#624)

A delegate with users.manage grants only roles it holds and cannot manage admins,
superusers or stronger accounts; an admin's scoped token manages ordinary accounts
but never grants admin. Nobody disables or demotes themselves or the last active
admin (serialized with FOR UPDATE on Postgres).

Claude-Session: https://claude.ai/code/session_01M9neheZZEe3sVpDi2S3zT4
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Deploying simple-module-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7d474fe
Status: ✅  Deploy successful!
Preview URL: https://41e5b8be.simple-module-python.pages.dev
Branch Preview URL: https://fix-debt-batch-14.simple-module-python.pages.dev

View logs

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-24T12:58:12.511157Z 7d474fe PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@antosubash
antosubash merged commit 42c1afc into main Oct 24, 2026
17 checks passed
@antosubash
antosubash deleted the fix/debt-batch-14 branch October 24, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant