开源的用户管理解决方案,自带前端组件和管理后台。
Go to file
BilalG1 c69a27017b
fix team invitation email check + verification code TOCTOU (#1365)
## Summary

Two authorization fixes in the backend. Both are pre-existing in `dev`
and were found during a security audit of `apps/backend/src`.

### 1. Team invitation accept — email not validated


[`team-invitations/accept/verification-code-handler.tsx`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx)
destructured the invited email as `{}` and only used `data.team_id` +
the accepting `user`. Any signed-in user in the tenancy who possessed
the 45-char code could join the team as themselves — the invitation was
not actually bound to the email it was addressed to.

**Attack scenarios that work without this fix**
- Forwarded invitation email (shared inbox, assistant inbox,
auto-forward rules).
- Screenshot of the invitation link pasted into Slack / Notion.
- Insider with server-access reading the email outbox (`GET
/api/latest/emails/outbox` returns rendered `html` +
`variables.teamInvitationLink`).
- Stale invite still sitting in spam after the invitee forwarded it
elsewhere.

**Fix.** The accept handler now requires that the accepting user owns
the invited email as a *verified* contact channel on their account.
Matches the invariant already used by the "list invitations for me"
endpoint
([`team-invitations/crud.tsx:41-66`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/app/api/latest/team-invitations/crud.tsx#L41-L66)).
Rejections return a new `TEAM_INVITATION_EMAIL_MISMATCH` (403) error.

### 2. Verification-code handler TOCTOU


[`route-handlers/verification-code-handler.tsx`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/route-handlers/verification-code-handler.tsx)
had a classic read-then-write TOCTOU:

```ts
const verificationCode = await prisma.verificationCode.findUnique(...);
if (verificationCode.usedAt) throw new KnownErrors.VerificationCodeAlreadyUsed();
// ... validation ...
await prisma.verificationCode.update({ data: { usedAt: new Date() } });  // unconditional
return await options.handler(...);
```

Five concurrent requests with the same code all pass the `if (usedAt)`
gate, all mark the code used, all run the post-handler. For OTP sign-in
the handler calls `createAuthTokens` which writes a fresh
`projectUserRefreshToken` row per call — so **one OTP → N refresh
tokens**. `auth/sessions/current` only revokes by `id: refreshTokenId`
and there is no bulk-revoke for passwordless users (only password change
in
[`users/crud.tsx:1210`](https://github.com/stack-auth/stack-auth/blob/dev/apps/backend/src/app/api/latest/users/crud.tsx#L1210)
does `deleteMany`). A phished OTP therefore becomes a
session-persistence primitive.

**Fix.** Replace the unconditional `update` with a conditional
`updateMany({ where: { …, usedAt: null } })` executed before
`options.handler`; if `count === 0` the race was already lost and we
throw `VERIFICATION_CODE_ALREADY_USED` (409). This also benefits MFA
sign-in and passkey sign-in, which share the same handler.

## Changes

| File | Change |
|---|---|
| `team-invitations/accept/verification-code-handler.tsx` | Require
verified contact channel matching `method.email` |
| `route-handlers/verification-code-handler.tsx` | Atomic `updateMany`
claim gated on `usedAt: null` |
| `stack-shared/src/known-errors.tsx` | New
`TeamInvitationEmailMismatch` (403) |
| `e2e/.../team-invitations.test.ts` | Two new tests (mismatch + happy
path) |
| `e2e/.../auth/otp/sign-in.test.ts` | One new test: 5 parallel
redemptions of one OTP → 1× 200 + 4× 409 |

## Test plan

- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts` —
27/27 pass
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/auth/otp/sign-in.test.ts` —
12/12 (+ 4 pre-existing `it.todo`)
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/auth/password` — 33/33 (+ 7
pre-existing todos)
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/contact-channels` — 24/24
- [x] `pnpm test run
apps/e2e/tests/backend/endpoints/api/v1/auth/passkey
apps/e2e/tests/backend/endpoints/api/v1/auth/mfa` — 16/16
- [x] `pnpm --filter @stackframe/backend typecheck` — clean
- [x] `pnpm --filter @stackframe/backend lint` + `pnpm --filter
@stackframe/stack-shared lint` — clean

## Notes

- The broader "plaintext credentials in DB + Sentry logs every header"
finding from the same audit is **not** in this PR — a scrubber for
`Sentry.setContext` request headers + unit tests is prepared on a local
stash and will go out as a separate PR.
- The team-invitation fix does not require any config change; fresh
signups via the OTP / password flows that set `primary_email_verified:
true` during creation already land the user with a verified channel
matching the invited email, so the happy path is unaffected.

### Follow-up review (Codex)

Addressed in follow-up commit `954cddb`:
- **Finding 1 (High)**: mismatched invite acceptance was consuming the
invitation before rejecting. Moved the email-ownership check into the
pre-claim `options.validate` hook so a wrong-email attempt leaves
`usedAt` untouched and the real recipient can still redeem. New test
asserts this end-to-end.
- **Finding 3 (Medium)**: invitation stored `body.email` raw but contact
channels are stored via `normalizeEmail`, so case-varied invites (e.g.
`Alice@Example.com`) wouldn't match a `alice@example.com` channel.
`send-code` now normalizes on storage and `accept` normalizes on compare
for back-compat with already-issued invites. New test covers the
mixed-case path.
- **Finding 2 (partial)**: added `expiresAt > now` to the atomic claim
predicate for the boundary case where a code expires between the read
and the claim. The reviewer's broader point about the `attemptCount`
rate-limit check being non-atomic with its own increment **pre-dates
this PR** (it reads the in-memory `verificationCode.attemptCount` from
line 150, not a fresh read) and exists independently of the `usedAt`
TOCTOU I'm fixing here. Tracking that as a separate follow-up so this PR
stays scoped to the two originally-flagged issues.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Invite acceptance now requires the invitee’s verified, normalized
(case‑insensitive) email; mismatches return HTTP 403
(TEAM_INVITATION_EMAIL_MISMATCH).
* Client APIs now surface the new email-mismatch error alongside
verification errors.

* **Bug Fixes**
* OTP verification codes are now guarded against parallel double‑redeem
so only one request succeeds.

* **Tests**
* Added E2E tests for invitation email validation, non‑consuming
rejection, case‑insensitive matching, and OTP concurrency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-04 17:13:03 -07:00
.agents/skills/pr-visual-writeup Add pr-visual-writeup skill to .agents/skills (#1354) 2026-04-20 11:54:55 -07:00
.changeset Disable changesets changelogs 2026-01-12 15:21:56 -08:00
.claude [codex] Fix preview dummy payments customer types (#1398) 2026-05-01 09:44:30 -07:00
.cursor Update pre-push.md 2026-04-12 21:52:33 -07:00
.devcontainer Customizable ports (#962) 2025-10-20 15:24:47 -07:00
.github Fast-start local emulator via RAM snapshot + live secret rotation (#1340) 2026-04-20 14:24:49 -07:00
.vscode Payments bulldozer txn rework (#1315) 2026-04-17 22:11:21 +00:00
apps fix team invitation email check + verification code TOCTOU (#1365) 2026-05-04 17:13:03 -07:00
claude Add useCliAuthConfirmation hook and customizable cliAuthConfirm URL target (#1388) 2026-04-28 15:29:46 -07:00
configs [Fix] Infinite Loop on handler/sign-in due to useStackApp not being able to find the StackProvider given context (#1248) 2026-03-12 22:28:47 -07:00
docker fix(emulator): move mock OAuth off 8114 to avoid pnpm dev conflict (#1385) 2026-04-27 09:39:34 -07:00
docs chore: update package versions 2026-05-04 15:33:33 -07:00
docs-mintlify chore: update package versions 2026-05-04 15:33:33 -07:00
examples chore: update package versions 2026-05-04 15:33:33 -07:00
packages fix team invitation email check + verification code TOCTOU (#1365) 2026-05-04 17:13:03 -07:00
patches Fix MS OAuth (#457) 2025-02-21 19:39:22 +01:00
scripts Fix memory leak 2026-04-18 22:21:05 -07:00
sdks chore: update package versions 2026-05-04 15:33:33 -07:00
.dockerignore emu with a q stuff (#1266) 2026-04-04 00:33:52 +00:00
.gitignore Fast-start local emulator via RAM snapshot + live secret rotation (#1340) 2026-04-20 14:24:49 -07:00
.gitmodules private files n sm build shit (#1276) 2026-03-23 12:31:36 -07:00
AGENTS.md Payments bulldozer txn rework (#1315) 2026-04-17 22:11:21 +00:00
CHANGELOG.md CHANGELOG.md Update with Images 2026-02-02 11:27:09 -06:00
CLAUDE.md session replays (#1187) 2026-02-16 14:15:17 -08:00
CONTRIBUTING.md Config sources (#1083) 2026-01-21 18:08:35 -08:00
LICENSE Fix user hooks 2025-06-22 19:32:52 -07:00
package.json Fix memory leak 2026-04-18 22:21:05 -07:00
pnpm-lock.yaml Move internal MCP server to backend, use Mintlify MCP for docs tools (#1389) 2026-04-29 09:45:52 -07:00
pnpm-workspace.yaml Replace npx with pnpm exec (#1300) 2026-04-08 17:08:55 -07:00
README.md LLM MCP Flow (#1321) 2026-04-15 17:57:08 +00:00
turbo.json Fix build 2026-02-27 00:48:07 -08:00
vitest.shared.ts Fix tests 2026-02-17 19:57:08 -08:00
vitest.workspace.ts Hosted components (#1229) 2026-03-10 11:29:05 -07:00

Stack Logo

Ask DeepWiki

📘 Docs | ☁️ Hosted Version | Demo | 🎮 Discord

Stack Auth: The open-source auth platform

Stack Auth is a managed user authentication solution. It is developer-friendly and fully open-source (licensed under MIT and AGPL).

Stack Auth gets you started in just five minutes, after which you'll be ready to use all of its features as you grow your project. Our managed service is completely optional and you can export your user data and self-host, for free, at any time.

We support Next.js, React, and JavaScript frontends, along with any backend that can use our REST API. Check out our setup guide to get started.

Stack Auth Setup

Table of contents

How is this different from X?

Ask yourself about X:

  • Is X open-source?
  • Is X developer-friendly, well-documented, and lets you get started in minutes?
  • Besides authentication, does X also do authorization and user management (see feature list below)?

If you answered "no" to any of these questions, then that's how Stack Auth is different from X.

Features

To get notified first when we add new features, please subscribe to our newsletter.

<SignIn/> and <SignUp/>

Authentication components that support OAuth, password credentials, and magic links, with shared development keys to make setup faster. All components support dark/light modes.
Sign-in component

Idiomatic Next.js APIs

We build on server components, React hooks, and route handlers.
Dark/light mode

User dashboard

Dashboard to filter, analyze, and edit users. Replaces the first internal tool you would have to build.
User dashboard

Account settings

Lets users update their profile, verify their e-mail, or change their password. No setup required.
Account settings component

Multi-tenancy & teams

Manage B2B customers with an organization structure that makes sense and scales to millions.
Selected team switcher component

Role-based access control

Define an arbitrary permission graph and assign it to users. Organizations can create org-specific roles.
RBAC

OAuth Connections

Beyond login, Stack Auth can also manage access tokens for third-party APIs, such as Outlook and Google Calendar. It handles refreshing tokens and controlling scope, making access tokens accessible via a single function call.
OAuth tokens

Passkeys

Support for passwordless authentication using passkeys, allowing users to sign in securely with biometrics or security keys across all their devices.
OAuth tokens

Impersonation

Impersonate users for debugging and support, logging into their account as if you were them.
Webhooks

Webhooks

Get notified when users use your product, built on Svix.
Webhooks

Automatic emails

Send customizable emails on triggers such as sign-up, password reset, and email verification, editable with a WYSIWYG editor.
Email templates

User session & JWT handling

Stack Auth manages refresh and access tokens, JWTs, and cookies, resulting in the best performance at no implementation cost.
User button

M2M authentication

Use short-lived access tokens to authenticate your machines to other machines.
M2M authentication

📦 Installation & Setup

To install Stack Auth in your Next.js project (for React, JavaScript, or other frameworks, see our complete documentation):

  1. Run Stack Auth's installation wizard with the following command:

    npx @stackframe/stack-cli@latest init
    
  2. Then, create an account on the Stack Auth dashboard, create a new project with an API key, and copy its environment variables into the .env.local file of your Next.js project:

    NEXT_PUBLIC_STACK_PROJECT_ID=<your-project-id>
    NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=<your-publishable-client-key>
    STACK_SECRET_SERVER_KEY=<your-secret-server-key>
    
  3. That's it! You can run your app with npm run dev and go to http://localhost:3000/handler/signup to see the sign-up page. You can also check out the account settings page at http://localhost:3000/handler/account-settings.

Check out the documentation for a more detailed guide.

🌱 Some community projects built with Stack Auth

Have your own? Happy to feature it if you create a PR or message us on Discord.

Templates

Examples

🏗 Development & Contribution

This is for you if you want to contribute to the Stack Auth project or run the Stack Auth dashboard locally.

Important: Please read the contribution guidelines carefully and join our Discord if you'd like to help.

Requirements

  • Node v20
  • pnpm v9
  • Docker

Setup

Note: 24GB+ of RAM is recommended for a smooth development experience.

In a new terminal:

pnpm install

# Build the packages and generate code. We only need to do this once, as `pnpm dev` will do this from now on
pnpm build:packages
pnpm codegen

# Start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema
# Make sure you have Docker (or OrbStack) installed and running
pnpm restart-deps

# Start the dev server
pnpm dev

# In a different terminal, run tests in watch mode
pnpm test # useful: --no-watch (disables watch mode) and --bail 1 (stops after the first failure) 

You can now open the dev launchpad at http://localhost:8100. From there, you can navigate to the dashboard at http://localhost:8101, API on port 8102, demo on port 8103, docs on port 8104, Inbucket (e-mails) on port 8105, and Prisma Studio on port 8106. See the dev launchpad for a list of all running services.

Your IDE may show an error on all @stackframe/XYZ imports. To fix this, simply restart the TypeScript language server; for example, in VSCode you can open the command palette (Ctrl+Shift+P) and run Developer: Reload Window or TypeScript: Restart TS server.

Pre-populated .env files for the setup below are available and used by default in .env.development in each of the packages. However, if you're creating a production build (eg. with pnpm run build), you must supply the environment variables manually (see below).

Useful commands

# NOTE:
# Please see the dev launchpad (default: http://localhost:8100) for a list of all running services.

# Installation commands
pnpm install: Installs dependencies

# Types & linting commands
pnpm typecheck: Runs the TypeScript type checker. May require a build or dev server to run first.
pnpm lint: Runs the ESLint linter. Optionally, pass `--fix` to fix some of the linting errors. May require a build or dev server to run first.

# Build commands
pnpm build: Builds all projects, including apps, packages, examples, and docs. Also runs code-generation tasks. Before you can run this, you will have to copy all `.env.development` files in the folders to `.env.production.local` or set the environment variables manually.
pnpm build:packages: Builds all the npm packages.
pnpm codegen: Runs all the code-generation tasks, eg. Prisma client and OpenAPI docs generation.

# Development commands
pnpm dev: Runs the development servers of the main projects, excluding most examples. On the first run, requires the packages to be built and codegen to be run. After that, it will watch for file changes (including those in code-generation files). If you have to restart the development server for anything, that is a bug that you can report.
pnpm dev:full: Runs the development servers for all projects, including examples.
pnpm dev:basic: Runs the development servers only for the necessary services (backend and dashboard). Not recommended for most users, upgrade your machine instead.

# Environment commands
pnpm start-deps: Starts the Docker dependencies (DB, Inbucket, etc.) as Docker containers, and initializes them with the seed script & migrations. Note: The started dependencies will be visible on the dev launchpad (port 8100 by default).
pnpm stop-deps: Stops the Docker dependencies (DB, Inbucket, etc.) and deletes the data on them.
pnpm restart-deps: Stops and starts the dependencies.

# Database commands
pnpm db:migration-gen: Currently not used. Please generate Prisma migrations manually (or with AI).
pnpm db:reset: Resets the database to the initial state. Run automatically by `pnpm start-deps`.
pnpm db:init: Initializes the database with the seed script & migrations. Run automatically by `pnpm db:reset`.
pnpm db:seed: Re-seeds the database with the seed script. Run automatically by `pnpm db:init`.
pnpm db:migrate: Runs the migrations. Run automatically by `pnpm db:init`.

# Testing commands
pnpm test <file-filters>: Runs the tests. Pass `--bail 1` to make the test only run until the first failure. Pass `--no-watch` to run the tests once instead of in watch mode.

# Various commands
pnpm explain-query: Paste a SQL query to get an explanation of the query plan, helping you debug performance issues.
pnpm verify-data-integrity: Verify the integrity of the data in the database by running a bunch of integrity checks. This should never fail at any point in time (unless you messed with the DB manually).

Note: When working with AI, you should keep a terminal tab with the dev server open so the AI can run queries against it.

❤ Contributors