Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/server/worker/auth/otp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// otp.ts:Email OTP(6 位,10min)+ WhatsApp/SMS OTP(6 位,5min)passwordless 认证 handler。
// OTP 存 HMAC-SHA256 哈希(verificationTokens.codeHash),验证后立即删(01 章 4)
// OTP 存 HMAC-SHA256 哈希(verificationTokens.codeHash),验证后以 consumedAt CAS 标记消费
// 限流:同一邮箱/手机 1/min + 5/hour(RateLimitStore DO,anti-abuse rule)。
// 最多 5 次错误后 token 作废(01 章 4:Email OTP 5 次错误后作废)。
// Phone OTP 国家白名单默认 US/CA,租户可扩展(01 章 4)。
Expand Down
6 changes: 3 additions & 3 deletions docs/api-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Authentication: public auth endpoints or a cookie session. The public product se
| POST | `/auth/verify-email` | PASS | `{ ok: true }` |
| POST | `/auth/resend-verification` | PASS | enumeration-resistant success response |
| POST | `/auth/magic-link/send` | PASS | enumeration-resistant success response |
| GET | `/auth/magic-link/verify` | PASS | Mutation-free compatibility redirect from a legacy query-string token to the Hosted UI `/magic-link#token=...` confirmation page; a missing or unresolvable credential redirects to the tokenless branded error state instead of rendering API JSON; never consumes the token or writes a session |
| POST | `/auth/magic-link/verify` | PASS | Explicit user-confirmed token consumption; on success writes the session cookie and returns `{ redirectUrl }` |
| GET | `/auth/magic-link/verify` | PASS | Mutation-free compatibility redirect from a legacy query-string token to the Hosted UI `/magic-link#token=...` confirmation page; a missing or unresolvable credential redirects to the tokenless branded error state instead of rendering API JSON; never consumes the token or writes a session |
| POST | `/auth/magic-link/verify` | PASS | Explicit user-confirmed token consumption; on success writes the session cookie and returns `{ redirectUrl }` |
| POST | `/auth/otp/email/send` | PASS | enumeration-resistant success response |
| POST | `/auth/otp/email/verify` | PASS | `{ redirectUrl? }`; on success writes the session cookie |
| POST | `/auth/otp/whatsapp/send` | PASS | When the provider is ready, writes the OTP token and enqueues it to `WHATSAPP_QUEUE`; when the provider is not configured, returns the enumeration-resistant response and writes a policy denial audit event |
Expand Down Expand Up @@ -75,7 +75,7 @@ Hosted Auth policy rules:
- The Magic Link send endpoint distinguishes `login` from `user_creation` by whether the email already exists. An existing user MUST satisfy `magicLink.allowLogin`, a new user MUST satisfy `magicLink.allowUserCreation`; a policy denial still returns the enumeration-resistant success response and sends no mail.
- The Magic Link JWT carries `action = login | user_creation`; after consuming the token the verify endpoint re-validates policy against that action and marks the primary email as verified.
- The Email OTP send endpoint distinguishes `login` from `user_creation` by whether the email already exists. An existing user MUST satisfy `emailOtp.allowLogin`, a new user MUST satisfy `emailOtp.allowUserCreation`; a policy denial still returns the enumeration-resistant success response and sends no code.
- On successful Email OTP verification the one-time token is deleted, the primary email is marked verified, and a session is issued.
- On successful Email OTP verification the one-time token is atomically marked consumed, the primary email is marked verified, and a session is issued.
- When `forceSso = true` and enterprise SSO has `enabled + allowLogin + domainDiscovery`, the Hosted UI shows only the enterprise SSO email discovery form.
- `POST /sso/hrd` queries a verified org domain only when enterprise SSO has `enabled + allowLogin + domainDiscovery`, and it applies both the Hosted Auth global `allowedEmailDomains` / `blockedEmailDomains` and the enterprise SSO `allowedEmailDomains` / `blockedEmailDomains`; when enterprise SSO or domain discovery is denied by policy it returns `{ connectionId: null }` and writes `auth.policy_denied`.
- OIDC RP JIT and SAML JIT apply both the Hosted Auth global domain policy and the enterprise SSO domain policy. When either policy denies, no user is created, no existing user is synced, and no membership is written.
Expand Down
26 changes: 13 additions & 13 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -837,19 +837,19 @@ work. Those need the per-feature smokes below.

Smokes that involve a real provider need real credentials or a code that was really received. Always pass that kind of input through a **file variable** rather than a plain environment variable, so it never lands in shell history, the process environment or command logs.

| Command | Covers | Required input |
| ----------------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `pnpm smoke:production:auth` | Email OTP real cookie plus `/v1/me` | `XID_PRODUCTION_EMAIL` |
| `pnpm smoke:production:browser` | Headless Chrome checks on DOM, console and navigation | `XID_PRODUCTION_EMAIL` |
| `pnpm smoke:production:magic-link-send` | Magic Link send and audit path | `XID_PRODUCTION_EMAIL` |
| `pnpm smoke:production:magic-link` | Magic Link sign-in from a real click | `XID_PRODUCTION_MAGIC_LINK_URL_FILE` |
| `pnpm smoke:production:whatsapp-otp-send` | WhatsApp OTP send side | `XID_PRODUCTION_PHONE_OTP_PHONE_FILE` (SKIP when no provider is configured) |
| `pnpm smoke:production:sms-otp-send` | SMS OTP send side | Same as above |
| `pnpm smoke:production:whatsapp-otp` | Full WhatsApp OTP verification | `XID_PRODUCTION_PHONE_OTP_ORGANIZATION_ID` plus phone file plus code file |
| `pnpm smoke:production:sms-otp` | Full SMS OTP verification | Same as above |
| `pnpm smoke:production:social-oauth` | Social OAuth real callback | `XID_PRODUCTION_SOCIAL_OAUTH_CALLBACK_URL_FILE` |
| `pnpm smoke:production:enterprise-sso` | Enterprise SSO real IdP callback | `..._CALLBACK_URL_FILE` for OIDC; `..._SAML_RESPONSE_FILE` plus `..._CONNECTION_ID` for SAML |
| `pnpm smoke:production:mfa-sms` | MFA SMS step-up | `XID_PRODUCTION_MFA_SMS_COOKIE_FILE` plus `XID_PRODUCTION_MFA_SMS_CODE_FILE` |
| Command | Covers | Required input |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `pnpm smoke:production:auth` | Email OTP real cookie plus `/v1/me` | `XID_PRODUCTION_EMAIL` |
| `pnpm smoke:production:browser` | Chrome checks on DOM, console and navigation; set `XID_PRODUCTION_BROWSER_HEADED=1` when production Turnstile rejects headless automation. On macOS, headed Chrome launches in the background and closes through CDP. | `XID_PRODUCTION_EMAIL` |
| `pnpm smoke:production:magic-link-send` | Magic Link send and audit path | `XID_PRODUCTION_EMAIL` |
| `pnpm smoke:production:magic-link` | Magic Link sign-in from a real click | `XID_PRODUCTION_MAGIC_LINK_URL_FILE` |
| `pnpm smoke:production:whatsapp-otp-send` | WhatsApp OTP send side | `XID_PRODUCTION_PHONE_OTP_PHONE_FILE` (SKIP when no provider is configured) |
| `pnpm smoke:production:sms-otp-send` | SMS OTP send side | Same as above |
| `pnpm smoke:production:whatsapp-otp` | Full WhatsApp OTP verification | `XID_PRODUCTION_PHONE_OTP_ORGANIZATION_ID` plus phone file plus code file |
| `pnpm smoke:production:sms-otp` | Full SMS OTP verification | Same as above |
| `pnpm smoke:production:social-oauth` | Social OAuth real callback | `XID_PRODUCTION_SOCIAL_OAUTH_CALLBACK_URL_FILE` |
| `pnpm smoke:production:enterprise-sso` | Enterprise SSO real IdP callback | `..._CALLBACK_URL_FILE` for OIDC; `..._SAML_RESPONSE_FILE` plus `..._CONNECTION_ID` for SAML |
| `pnpm smoke:production:mfa-sms` | MFA SMS step-up | `XID_PRODUCTION_MFA_SMS_COOKIE_FILE` plus `XID_PRODUCTION_MFA_SMS_CODE_FILE` |

The correct order for a full Magic Link smoke is: run `magic-link-send` first, take the link from **this** run out of the real inbox into a temporary file, then run `magic-link`. Otherwise you end up with mismatched evidence: a new email was sent, but an older link was consumed.

Expand Down
Loading