Skip to content

feat(auth): invites, forgot password, Google/Microsoft/GitHub setup, sign-in hardening - #442

Open
huyplb wants to merge 1 commit into
mainfrom
feat/auth-pages
Open

huyplb wants to merge 1 commit into
mainfrom
feat/auth-pages

Conversation

@huyplb

@huyplb huyplb commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What changes

Pages

  • First launch: Create your account (the install's administrator; claims the old local account so data stays).
  • Sign-in: Forgot password?, Have an invite or reset code?, Google / Microsoft / GitHub buttons (when configured), Caps Lock warning.
  • New-password fields check the server's own rules as you type (passwordProblem in @foxschema/shared).

Accounts

  • Add user with no password sends an invite: a one-time code, and the person chooses their own password. Unaccepted invites are marked Invited; each row can resend an invite or send a reset code.
  • Forgot password: a 30-minute, single-use code. It goes by email, or, with no email set up, to the server log and foxschema reset-password [email]. Redeeming ends every other session. The reply is identical whether or not the account exists, and the code is sent after the response, so timing doesn't reveal it either.

Admin → Access control → Sign-in (new tab)

  • Google, Microsoft (tenant) and GitHub client ID and secret, with the redirect URL to register.
  • The SMTP relay, with Hostinger / Gmail / Microsoft 365 presets and a Send test email button.
  • The public URL.

Secrets are write-only and encrypted with the install key. Env vars (SSO_*, SMTP_*, APP_PUBLIC_URL) still win and show read-only. The SMTP client moved from workflow-engine to @foxschema/db/mail, with a re-export at its old path, so the server can send without depending on the engine.

Security fixes found along the way

Issue Fix
Microsoft SSO account takeover ("nOAuth"): with common, the email claim is whatever any tenant's admin typed Accept only personal accounts, xms_edov-verified domains, or the configured single tenant
Google SSO ignored email_verified; GitHub could use an unverified public email Required / verified primary only
No PKCE; state compared with !== PKCE S256; constant-time compare
trustProxy: true let any client set X-Forwarded-For and skip every per-address limit Trust only loopback/private peers (FOX_TRUST_PROXY to override)
Brute force across many IPs Per-email lockout (5 failures / 15 min), same for unknown emails; unknown email costs the same scrypt time
Raw session tokens in the metadata DB Stored as SHA-256; migration 20 (append-only) clears old sessions, so everyone signs in once more
Reset links built from the Host header would let a requester choose where a link points Links use the configured public URL only, else the email carries the code alone; the code sits in the URL fragment so it never reaches a server log

Password policy for new passwords: 10+ characters, not a common password, and not containing the email name. Existing passwords keep working.

Verification

  • apps/web / cli / e2e tsc clean. lint and lint:security clean. build passes.
  • npx vitest run: 432 files, 5119 passed.
  • New tests:
    • auth.recovery.test.ts: reset, invite, lockout, hashes-only storage.
    • sso.service.test.ts: per-provider email trust and PKCE. A mutation check confirmed the Microsoft and Google guards are each caught.
    • sign-in-settings.service.test.ts.
    • recovery.routes.test.ts, on a real listener: identical forgot replies, 429 + Retry-After, invite once-only, 403 for viewers, secrets never returned.
    • password-policy.test.ts.
    • Web: AuthPage (sign-up rules, forgot, emailed invite link, wrong code), AdminAccessPanel (invite, resend), SignInSettingsPanel.
    • The HTTP contract table goes from 84 to 95 routes.
  • New e2e auth-recovery.test.ts, in a real browser: invite link → sign up → forgot password → reset code → old password refused → sign in with the new one. 4/4.
  • Existing e2e: smoke 3/3, access 6/6, schema history 6/6, SQL Editor 51/51.
  • Checked by hand in the browser: sign-in page, the forgot reply (same for an unknown email), and the wrong-code error.

🤖 Generated with Claude Code


Note

High Risk
Touches authentication, session storage, SSO/email configuration, and proxy trust; upgrading invalidates all sessions and changes sign-in abuse controls.

Overview
Adds end-to-end account recovery and invites without open registration: admins can invite users (empty starting password → one-time code), users get forgot password and redeem invite/reset code flows in the UI, and foxschema reset-password [email] prints a reset code when SMTP is not configured.

Backend grows one-time auth_codes (hashed), public /api/auth/password/* routes, admin POST /api/admin/users/:id/code, and /api/admin/sign-in for public URL, OAuth providers, and SMTP. Codes and session cookies are stored hashed (migration 20 clears old sessions). Per-email lockout after failed sign-ins, stricter new password rules (10+ chars via @foxschema/shared), and FOX_TRUST_PROXY defaults so rate limits are not bypassed via X-Forwarded-For. Email delivery uses a new @foxschema/db/mail SMTP client.

Admin UI adds Sign-in settings (SSO redirect URLs, mail presets, test email), IssuedCodeNotice when codes are logged instead of emailed, and Invited / resend-invite / send-reset on user rows. E2e adds auth-recovery.test.ts and optional unsigned Playwright sessions for sign-in pages.

Reviewed by Cursor Bugbot for commit 002a0c7. Bugbot is set up for automated code reviews on this repo. Configure here.

…sign-in hardening

Pages: first launch is "Create your account"; sign-in adds Forgot password?
and "Have an invite or reset code?". New passwords are checked as you type
against the server's rules (shared package), with a Caps Lock warning.

Accounts: Add user without a password sends a one-time invite code and the
person chooses their own password; admins can resend an invite or send a
reset code from a user's row. Forgot password issues a 30-minute single-use
code by email, or, with no email configured, to the server log and via
`foxschema reset-password`. Redeeming ends every other session.

Admin -> Access control -> Sign-in configures Google, Microsoft and GitHub
(with the redirect URL to register), the SMTP relay (Hostinger / Gmail /
Microsoft 365 presets, test email) and the public URL. Env vars still win.
The SMTP client moved to @foxschema/db/mail so the server can use it.

Security:
- SSO trusted unverified emails. Microsoft via `common` accepted any
  tenant's typed email (nOAuth account takeover); now only personal
  accounts, xms_edov-verified domains or the configured tenant. Google
  requires email_verified; GitHub uses only the verified primary address.
  PKCE added; state compared in constant time.
- X-Forwarded-For was trusted from any peer, so rate limits could be
  skipped; now only loopback/private proxies (FOX_TRUST_PROXY).
- Per-email lockout after 5 failures (also for emails with no account);
  unknown emails cost the same scrypt time.
- Session tokens and codes stored as SHA-256 (migration 20 clears
  sessions). Reset links use the configured public URL only, never Host,
  and carry the code in the URL fragment.
- Password policy: 10+ chars, not common, not the email name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_29121fd1-d5e2-44e8-b313-608b98f65b15)

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.

1 participant