Skip to content

AUTH_ALLOWED_EMAILS: named sign-in exceptions, so the domain rule cannot become a lock-out - #36

Merged
philosophercode merged 1 commit into
mainfrom
v5/auth-allowlist
Sep 24, 2026
Merged

philosophercode merged 1 commit into
mainfrom
v5/auth-allowlist

Conversation

@philosophercode

Copy link
Copy Markdown
Owner

Stacked on #35.

The bug this closes

The super-admin floor exists so nobody can be locked out permanently. But isSuperAdminFloor calls isAllowedEmail first:

export function isSuperAdminFloor(email: string | null | undefined): boolean {
  const normalized = normalizeEmail(email);
  if (!normalized || !isAllowedEmail(normalized)) return false;   // <-- domain rule
  return superAdminEmails().includes(normalized);
}

So a floor entry on any other domain was silently ignored. You would set AUTH_SUPER_ADMIN_EMAILS to a personal address, redeploy, sign in, and be bounced to /auth/rejected with no indication why — the env var looked configured and did nothing. That is the worst possible shape for a safety net, and it fails exactly the accounts that need it: the people holding super_admin are the ones whose institutional address is temporary.

The change

AUTH_ALLOWED_EMAILS — a comma-separated list of full addresses admitted whatever domain they are on. isAllowedEmail now passes an address on the configured domain or one named in the list.

Named exceptions, never a second open domain. One entry admits one person, not a provider. Adding one is a deliberate act by somebody who can already deploy. Empty by default, and empty leaves the domain rule byte-identical to before.

One consequence, stated plainly

Setting this drops the Google hd hint, and that is not optional. hd is not only a hint to the account picker — Better Auth verifies the claim on the returned id token, and a personal Google account carries no hd at all. Left on, it would refuse every named exception before this app's own check ever ran, and the allowlist would look configured and do nothing. Again.

So socialProviders.google.hd is omitted whenever the list is non-empty. The account picker gets wider. The two server-side enforcement points in §3.4 do not move, and they are the control — hd never was.

Tests

The one that matters is the third: naming one Gmail address must not admit a second one.

  • an off-domain named address is admitted, case-insensitively
  • the institutional domain keeps working alongside it
  • naming steinbergisaac@gmail.com does not admit someone.else@gmail.com
  • an empty or unset list behaves exactly as the domain rule did

Verification

With every credential unset: typecheck clean, 97 files / 1362 tests, spec:coverage 74 items / 0 undocumented. The coverage gate caught the undocumented env var before I did, which is the gate working.

Bootstrap plan

  • AUTH_SUPER_ADMIN_EMAILS=ies22@cornell.edu — the floor at launch, works with no code change
  • AUTH_ALLOWED_EMAILS=steinbergisaac@gmail.com — so access survives the Cornell account

🤖 Generated with Claude Code

https://claude.ai/code/session_01AjvfabH9CRceoC9GfjjSvE

The super-admin floor exists so nobody can be locked out permanently, but
isSuperAdminFloor calls isAllowedEmail first, so a floor entry on any other
domain was silently ignored: the environment variable looked set and did
nothing. That is the worst shape a safety net can have, and it bites exactly
the accounts that need it — the people holding super_admin are the ones whose
institutional address is temporary.

isAllowedEmail now admits an address on the configured domain or one named in
AUTH_ALLOWED_EMAILS. Named exceptions, never a second open domain: one entry
admits one person, and adding one is a deliberate act by somebody who can
already deploy. Empty by default, where it changes nothing.

Setting it drops the Google hd hint, and that is not optional. Better Auth
verifies the hd claim on the returned id token and a personal account carries
none, so leaving it on would refuse every named exception before this app's
own check ran. The picker widens; the two server-side checks do not move.

Verified with the credentials unset: 97 files / 1362 tests, typecheck clean,
spec:coverage 74 items / 0 undocumented. The new tests assert the part that
matters — naming one gmail address does not admit a second one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjvfabH9CRceoC9GfjjSvE
Copilot AI lite review requested due to automatic review settings September 22, 2026 21:34
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
makerlab-tools Ready Ready Preview Sep 22, 2026 9:36pm UTC
makerlab-tools-v5 Ready Ready Preview Sep 22, 2026 9:36pm UTC

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03e6861e3f

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread v5/src/lib/auth/config.ts
export function createAuth(db: Db) {
const secret = process.env.AUTH_SECRET || "";
const domain = allowedEmailDomain();
const namedExceptions = allowedEmails().length > 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the allowlist to the auth-instance fingerprint

When AUTH_ALLOWED_EMAILS changes after getAuth() has memoized an instance, the new value changes namedExceptions and therefore whether Better Auth receives hd, but envFingerprint() does not include this variable. The cached instance can consequently retain the institutional hd check after an off-domain exception is enabled, causing Google to reject that named account before isAllowedEmail() runs. Include AUTH_ALLOWED_EMAILS in the fingerprint so configuration changes rebuild the instance as intended.

Useful? React with 👍 / 👎.

Comment thread v5/.env.example
Comment on lines +149 to +155
# Addresses allowed in BY NAME, whatever domain they are on — comma-separated.
# Named exceptions, never a second open domain: for a maintainer or a director
# whose institutional account is temporary. Setting any value here drops the
# Google `hd` hint, because Better Auth verifies that claim and a personal
# account carries none. Empty (the Cornell Tech deployment's setting) leaves
# the domain rule exactly as it was.
AUTH_ALLOWED_EMAILS=

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the canonical auth guidance for the new allowlist

Document this variable in the scoped v5 guidance and the Stage 3 deployment instructions: the former still says AUTH_SUPER_ADMIN_EMAILS is the one remaining environment list, while the latter omits AUTH_ALLOWED_EMAILS entirely. An operator following those canonical instructions cannot configure the off-domain floor that this change is intended to support, leaving the lock-out recovery procedure incomplete.

AGENTS.md reference: v5/AGENTS.md:L109-L114

Useful? React with 👍 / 👎.

@philosophercode
philosophercode changed the base branch from v5/data-platform-phase-3-4 to main September 24, 2026 00:56
@philosophercode
philosophercode merged commit ca3ab0c into main Sep 24, 2026
3 checks passed

This branch was successfully deployed

2 active deployments
Preview – makerlab-tools — 03e6861e Deployed Sep 22, 2026 by vercel[bot]
Preview – makerlab-tools-v5 — 03e6861e Deployed Sep 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants