A working magic link is not a finished magic link
A magic-link flow can look complete the moment you log in with it once. You enter an email, a link arrives, you click it, and you land signed in. That demo proves the happy path runs. It does not prove the link expires, that it can only be used once, that the issue endpoint resists abuse, or that the redirect target is validated. Each of those is a separate control, and magic link security depends on all of them being present, not just the click working.
Separate what you can observe from what is only possible. You can observe whether a link still signs you in an hour later, whether clicking it twice works twice, and whether the post-login redirect honors a target supplied in the URL. You cannot tell from the outside how the token is generated, stored, or compared. So the checks below stay on behavior you can reproduce against your own app, and the limits section is honest about what that behavior does and does not prove.
The four conditions worth confirming before launch are short: a magic-link token should have a tight time-to-live, should be invalidated after a single successful use, the endpoint that issues links should be rate limited, and any redirect-after-login parameter should be validated against an allowlist. These map to known weakness classes, including improper authentication, insufficient session expiration, and open redirect (CWE/OWASP taxonomy) without making any claim about whether your specific app is exploitable.
Why AI-built apps ship the happy path only
When you prompt an AI tool to add passwordless login, it generates the flow that makes the screen work: a form that takes an email, a step that sends a link, and a callback route that consumes a token and creates a session. That code runs and the demo succeeds, which is exactly what makes the gaps invisible. The thing that signs you in is wired up correctly. The things that constrain it are the parts a human normally adds during review, and that review step is what fast generation skips.
A few specific defaults tend to slip through in generated auth code:
None of these break the demo. They only matter once the URL is public and someone other than you interacts with the flow. That is why they survive into launch.
- Token lifetime is long, unset, or left at a permissive library default, so a link captured from an inbox or a log stays valid far longer than it should.
- The token is not marked consumed after first use, so a link that lands in browser history, a referrer header, or a forwarded email can be replayed.
- The issue endpoint has no rate limit, because rate limiting is server-side plumbing the prompt did not mention, so nothing stops repeated requests for the same or many addresses.
- The post-login redirect trusts a next or redirect parameter from the URL without checking it, which is an open-redirect setup waiting for a crafted link.
Self-checks you can run on your own app
Run these against your own deployment, signed in as yourself. They reproduce real behavior without attacking anyone.
Token expiry: request a magic link, then wait past the lifetime you believe you set and click it. The safe result is that an expired link refuses to sign you in and asks for a fresh one. The suspicious result is that a link from an hour or a day ago still works. If you do not know the configured lifetime, that uncertainty is itself a finding to resolve in code.
Single-use: request a link and click it to sign in, then open the same link again in a fresh browser session or a private window. The safe result is that the second use fails because the token was consumed. The suspicious result is a second successful login from the same link, which means the token is replayable.
Issue-endpoint rate limit: submit the email form repeatedly in quick succession and watch the Network tab in DevTools. The safe result is that after a few attempts the server responds with HTTP 429 and ideally a Retry-After header, and stops sending more mail. The suspicious result is that every request returns 200 and a new email goes out each time, which lets one address be flooded and lets an attacker probe many addresses cheaply.
Redirect validation: append a redirect or next parameter pointing at an external domain you control to the login or callback URL, then complete the flow. The safe result is that the app ignores the external target and lands you on an internal page. The suspicious result is that you end up redirected off-site, which confirms an open redirect that can be used to dress up a malicious link as your domain.
These checks confirm observable behavior, not implementation correctness. A link that appears single-use in the browser could still be replayable through a direct API call, and a 429 in the UI does not prove the limit is enforced per-identifier on the server. Treat a clean self-check as one signal and confirm the logic in code.
Fix the four controls, and know what each fix leaves open
Each control is a focused change, and each has a boundary worth stating.
A note on the difference that runs through all of this: these controls govern authentication, proving the holder of the link controls the email address. They do not govern authorization, deciding what that authenticated user may then read or do. A correctly expiring, single-use magic link can still drop a user onto data they should not see if your access rules are wrong. Keep the two concerns separate when you review.
- Set a short token time-to-live, on the order of minutes, not hours, and make the value explicit in config rather than relying on a library default. Short expiry shrinks the window for a leaked link. It does not stop a link from being used inside that window, so it pairs with single-use, not replaces it.
- Mark the token consumed on first successful exchange and reject any later use, ideally inside the same transaction that creates the session so two near-simultaneous clicks cannot both succeed. Single-use stops replay. It does not stop interception of a still-valid first click, which is what transport security and short TTL address.
- Rate limit the issue endpoint server-side, keyed by both target email and source, returning 429 with Retry-After when the limit trips. This reduces mail flooding and address probing. It is an authentication-edge control and does not authorize anything; it limits how often the door can be knocked on, not who gets through.
- Validate the redirect target against an allowlist of internal paths and reject or ignore anything else. This closes the open redirect. It does not address whether the user is allowed to see the page they land on, which is an authorization question.
Where VibeCodeGuard fits, and what it cannot prove
VibeCodeGuard's Launch Check works from the public surface. It can flag when the auth endpoints show no rate-limit signals, by probing for 429 and Retry-After responses, and it confirms whether expected security headers are present on the surface a visitor reaches. Those are the parts of magic link security an outside scan can actually observe, and the scan reports them with severity and fix direction so a missing rate-limit signal shows up before you share the URL.
What the scan does not do is read your token logic. Whether tokens truly expire, whether they are single-use, and whether the redirect parameter is validated are decisions that live in code, and confirming them needs manual review of the issue and callback handlers. A clean Launch Check tells you the public surface looked right at scan time; it does not certify the auth flow, and it is not a penetration test, an exploit verification, or an authorization review.
Next step: run the four self-checks above against your own deployment, fix any control that fails in the issue and callback handlers, then run a focused Launch Check against the public URL before sharing it so a missing rate-limit signal surfaces in your scan and not in someone else's probe.
A passing scan does not prove your magic links expire or resist replay. The scan sees rate-limit and header signals on the public surface; it does not exercise your token lifecycle or read your code. Treat a missing rate-limit signal as a launch blocker, and treat a clean result as one input to your own review, not a guarantee.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.