Repository navigation
fix(magic-link): refuse a cross-origin sign-in before the link is spent - #59
Merged
kamilmysliwiec merged 1 commit intoOct 22, 2026
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
Hi! 馃憢 This is the follow-up I offered in #42.
MagicLinkService.consume()answers a request from another origin with a 403, but it has already spent the link.On master (85fd249),
consume()uses the link withconsumeMagicLink()and settles the browser cookie (lib/magic-link/magic-link.service.ts:196-199). Only then does it callSignInService.signIn()(:224), which refuses the request withForbiddenException('Cross-origin sign-in refused')(lib/session/sign-in.service.ts:47and:195). When the user opens the same link again from the app, it does not sign in: the link is spent, and the refused response also took it out of the browser's cookie.With
bindToBrowseron, someone else can't burn the link, because they don't have the browser's cookie. The case I have in mind is an app whose SPA origin is missing fromsession.trustedOrigins: the first click fails every time, and the link is gone before anyone fixes the configuration.I ran the repro on 0.1.1:
As in #42, the refusal itself is correct. Only its timing is wrong.
What is the new behavior?
consume()refuses a request from another origin first, with the sameSignInService.refuseCrossOrigin()call ascreate()andPasswordResetService.reset(), on the request of the current call.signIn()doesn't check the origin in that case either.Does this PR introduce a breaking change?
A request from another origin now gets the 403 before the other checks. Before, a token that is not valid got
null, and a link opened without the browser's cookie got the 401not_this_browserwith amagic-link-refusedevent. Same-origin requests give the same answer as before.Other information
The new specs are in
tests/magic-links.spec.ts. They cover these cases:consumeMagicLink()is not called, the cookie is not changed, nomagic-link-refusedevent is emitted, and the same link signs in afterwards from the app's own origin.The old tests only checked the status of the refusal and that the cookie was cleared. They never opened the same link again, so they did not see that it was spent.
The existing login CSRF test in
tests/authentication.e2e.spec.tspinned the old order: a 401 without the cookie, and a 403 that cleared the cookie. It now expects a 403 in both cases with the cookie kept, and checks that the same link then signs in, on Express and Fastify.The docs say that
consume()"burns the token, callsresolveUser(), and signs the user in". I can add a line in docs.nestjs.com that a request from another origin is refused before the link is used. 馃檪