Skip to content

fix(auth): allow one magic-link request per Turnstile verification - #7769

Closed
iscekic wants to merge 1 commit into
mainfrom
igor/magic-link-turnstile-single-use
Closed

iscekic wants to merge 1 commit into
mainfrom
igor/magic-link-turnstile-single-use

Conversation

@iscekic

@iscekic iscekic commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

/api/auth/verify-turnstile sets a turnstile_jwt cookie that is valid for one hour. The OAuth sign-in callback deletes that cookie when it uses it. The magic-link route only verified it, so one Turnstile solve allowed any number of magic-link requests from the same IP until the cookie expired. A script could solve Turnstile once and then request magic links for many addresses.

This change deletes the cookie in /api/auth/magic-link after a successful verification. Each magic-link request now needs its own Turnstile solve.

User impact

None expected. useSignInFlow creates a new Turnstile widget attempt before every submission (email submit, Back, retry, provider click), and sendMagicLink has no other caller. The SSO discovery request still reads the cookie before the magic-link request, so the order of the existing flow is unchanged.

Tests

  • New test accepts only one magic-link request per Turnstile verification: it uses the real verifyTurnstileJWT with one signed cookie. The first request returns 200 and the second returns 401, and only one email is sent.
  • Without the route change, the new test fails and the other 17 tests pass. With the change, all 18 pass.
  • oxlint and oxfmt pass on the changed files.

Related: #7766 (signup step-up for risky signups).

The magic-link route verified the Turnstile cookie but never consumed it, so one verification covered every magic-link request from that IP until the cookie expired. The route now deletes the cookie after a successful verification, as the OAuth sign-in callback already does. The sign-in form solves a new Turnstile for each attempt, so users see no change.
@iscekic iscekic self-assigned this Oct 16, 2026
@kilo-code-bot

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

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The magic-link route now deletes the turnstile_jwt cookie after a successful verification, but because the JWT is verified statelessly and stays valid for an hour, this only enforces single-use for clients that honor Set-Cookie and not for a script that replays the captured token.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/app/api/auth/magic-link/route.ts 40 Client-side cookie deletion does not prevent a script from replaying the still-valid Turnstile JWT.
Files Reviewed (2 files)
  • apps/web/src/app/api/auth/magic-link/route.ts - 1 issue
  • apps/web/src/app/api/auth/magic-link/route.test.ts - no issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash · Input: 58.9K · Output: 11.9K · Cached: 864.3K

Review guidance: REVIEW.md from base branch main

@iscekic iscekic closed this Oct 16, 2026
@iscekic

iscekic commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Closing. Clearing the cookie does not stop scripted reuse of the Turnstile token (see the review thread), and Bouncer's per-IP signup rules already limit how many email signups one verification can produce.

@iscekic
iscekic deleted the igor/magic-link-turnstile-single-use branch October 16, 2026 16:27
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