Skip to content

feat(auth): step up risky signups to Google or Apple - #7766

Merged
iscekic merged 7 commits into
mainfrom
igor/signup-step-up
Oct 16, 2026
Merged

iscekic merged 7 commits into
mainfrom
igor/signup-step-up

Conversation

@iscekic

@iscekic iscekic commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Risky signups now get a step-up instead of a hard block: Bouncer's new signup_step_up verdict asks the user to finish with Google or Apple.

  • signupDecide sends optional method (the auth provider ID) and emailDomain (the registrable domain of the normalized email's host, via tldts, so subdomains share one counter). An invalid value is omitted whole, so Bouncer falls back to its method-less policy.
  • The response schema accepts { enforced: true, code: "signup_step_up", methods, flags } (methods: 1–8 strings of ^[a-z0-9-]{1,32}$), adds the signup:busy and signup:domain flags, and raises the flag cap to 8.
  • createOrUpdateUser maps a validated step-up to the new SIGNUP-STEP-UP error with the known methods (google, apple, in that order). It creates no Stripe customer, user, or auth-provider row.
  • Web: the sign-in redirect carries methods=<comma-separated>. The notification renders only Google and Apple from that untrusted query and never names a signal.
  • Native token route: HTTP 403 { error: "SIGNUP-STEP-UP", methods } on the email and Apple/Google paths. The email-code reservation is released.
  • Mobile: the toast names "Sign in with Apple" or "Sign in with Google" only when Bouncer allows that method and the screen shows that button. Otherwise it points to "More sign-in options". There are three new catalog keys, translated into all 86 locales.
  • SIGNUP-STEP-UP is added everywhere SIGNUP-RATE-LIMITED is handled: useSignInFlow, sso.ts, the expected-error list, and AuthErrorType. ENVIRONMENT.md documents the change.

Deploy order

Deploy after Kilo-Org/bouncer#20, with enforcement.signupStepUp=false. Old Bouncer builds never send the new branch. A new Bouncer never sends it to old Cloud builds, because those omit method.

Verification

Run on a Linux VM:

  • Typecheck passes for web-shared, web, and mobile. Lint passes for web and mobile.
  • Mobile check:unused, check:i18n, check:plurals, and format:check pass, and oxfmt reports no differences.
  • Web Jest passes 19 suites (512 tests): bouncer client, createOrUpdateUser, native routes, auth components, redirect URLs, and next-auth options. Mobile vitest src/lib/auth passes 26 files (556 tests).
  • The Android emulator was not run: the VM has no nested virtualization. The mobile mapping is covered by hook tests and the catalog check.
  • E2E against a local Kilo-Org/bouncer#20 with step-up on (see the E2E comment). I used the fallback route: a production next build kept failing on unrelated module-level environment requirements, one variable per build. A throwaway script ran the real createOrUpdateUser with NODE_ENV=production against local Postgres and the local Bouncer. An email signup from an IP past the burst limit returned SIGNUP-STEP-UP with ["google","apple"] and created no user, Stripe customer, or auth-provider row. An email signup from a quiet IP created the account; Bouncer logged domainKey: "domain:example.test" for the host quiet.example.test. The sign-in page screenshots for methods=google,apple and methods=apple come from next dev.

Send the signup method and email domain to signup-decide, accept the signup_step_up branch with its methods, add the signup:busy and signup:domain flags, and raise the flag cap to 8.
A validated signup_step_up verdict returns SIGNUP-STEP-UP and creates no Stripe customer, user, or auth provider. Web redirects to sign-in with the allowed methods; the native token route returns them with HTTP 403.
SIGNUP-STEP-UP names the Apple or Google button when Bouncer allows it and the screen shows it, and otherwise points to the browser sign-in options.
@iscekic iscekic self-assigned this Oct 16, 2026
Subdomains of one registrable domain now share Bouncer's domain counter. A public suffix such as my.id keeps the full host.
@kilo-code-bot

kilo-code-bot Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Reviewed the risky-signup step-up feature end to end — the Bouncer signup-decide client schema, Cloud settlement mapping, web redirect and notification, the native token route, mobile messaging, and the 87 locale catalogs — and found no defects on the changed lines; the untrusted methods query is parsed against a known-methods allowlist, and the step-up path creates no Stripe customer, user, or auth-provider row.

Files Reviewed (110 files)

Non-locale source and docs (23):

  • packages/web-shared/src/lib/auth/constants.ts — added SIGNUP-STEP-UP error, SIGNUP_STEP_UP_METHODS, and knownSignupStepUpMethods (order-preserving, de-duplicated allowlist)
  • packages/web-shared/src/lib/bouncer/client.ts — optional method/emailDomain request fields (dropped whole when invalid), signup:busy/signup:domain flags, flag cap 5→8, and the signup_step_up response branch
  • packages/web-shared/src/lib/bouncer/client.test.ts
  • apps/web/src/lib/user/index.ts — maps a validated step-up to SIGNUP-STEP-UP with known methods before any Stripe/user/provider creation; sends the registrable email domain via tldts
  • apps/web/src/lib/user/index.test.ts
  • apps/web/src/lib/user/next-auth-options.ts — threads methods into the failure redirect
  • apps/web/src/lib/user/sso.ts
  • apps/web/src/lib/auth/redirect-urls.ts — methods param build/parse against the allowlist
  • apps/web/src/lib/auth/redirect-urls.test.ts
  • apps/web/src/components/auth/AuthErrorNotification.tsx — renders only known provider names from untrusted query, never a signal
  • apps/web/src/components/auth/AuthErrorNotification.test.ts
  • apps/web/src/components/auth/SignInForm.tsx
  • apps/web/src/components/auth/auth-touch-targets.test.ts
  • apps/web/src/hooks/useSignInFlow.ts
  • apps/web/src/app/api/auth/native/token/route.ts — 403 { error, methods } on email and Apple/Google paths; reservation released
  • apps/web/src/app/api/auth/native/token/route.test.ts
  • apps/mobile/src/lib/auth/auth-error-messages.ts — signupStepUpMessage names Apple/Google only when allowed and shown
  • apps/mobile/src/lib/auth/auth-fetch.ts
  • apps/mobile/src/lib/auth/native-auth-contract.ts — methods parsed independently so a malformed list cannot hide the code
  • apps/mobile/src/lib/auth/native-auth-contract.test.ts
  • apps/mobile/src/lib/auth/use-native-auth.ts
  • apps/mobile/src/lib/auth/use-native-auth.test.ts
  • ENVIRONMENT.md

Locale catalogs (87): all apps/mobile/src/i18n/locales/*.json — 3 added keys each; validated as well-formed JSON, all three keys present, and referenced login.moreSignInOptions/login.signInWithApple/login.signInWithGoogle keys exist in every catalog.


Reviewed by deepseek-v4.1-flash · Input: 123.9K · Output: 25.7K · Cached: 3.4M

Review guidance: REVIEW.md from base branch main

@iscekic

iscekic commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

bot: E2E on a task VM against a local Kilo-Org/bouncer#20 (enforcement.enabled, signup, and signupStepUp on; local secrets only).

I used the fallback route: a production next build kept failing on unrelated module-level environment requirements during page-data collection, one variable per 10-minute build. Instead, a throwaway script ran the real createOrUpdateUser with NODE_ENV=production against the local Postgres and the local Bouncer. The sign-in page screenshots come from next dev, which renders the same notification.

Scenario Bouncer verdict Cloud result Rows
Email signup from 198.51.100.21, already past the burst limit signup_step_up, methods: ["google","apple"], stepUp: true SIGNUP-STEP-UP with ["google","apple"] no user, no Stripe customer, no auth provider
Email signup from a quiet IP (198.51.100.40), host quiet.example.test admitted, domainKey: "domain:example.test" created, isNew: true user, Stripe test customer, auth provider
?error=SIGNUP-STEP-UP&methods=google,apple ?error=SIGNUP-STEP-UP&methods=apple
google and apple apple only

The "Development Login" panel appears only because the screenshots come from next dev.

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