A magic link removes the password, and people usually describe that as removing the attack surface. It moves it. The login secret leaves your server, travels through an email system you do not run, and sits in a mailbox until someone opens it.
That detour is the whole security model, and it is the part a numbered list of implementation steps flattens.

Reading the diagram
Four parties, and the question to hold onto is: at each moment, who is holding something that can log in?
Step 1. Your server generates a random token and writes only its SHA-256 hash to the database. This is easy to skip and it is the difference between a database leak being embarrassing and a database leak being a set of working logins. A hash cannot be turned back into a link.
Step 2. The raw token goes out inside a link, over email. At this instant your own database no longer holds anything usable. The only live copy of the login just left the building.
Step 3 is the red band, and it is not an attack step. It is the ordinary state of the system for the next fifteen minutes. Anything that can read that mailbox can open that link: the user, a phone they left on a table, an old session in a webmail client, a mail rule forwarding to a shared address, an assistant with delegated access. Your app cannot distinguish any of them from the person who asked to log in.
Steps 4 through 7 are the return trip, and this is where the token stops being dangerous. The raw token comes back, your server hashes it and compares against the stored hash, confirms it has not been used, and marks it used at that moment rather than after issuing the session. Get that ordering wrong and two requests arriving together can both redeem the same link.
Step 8 issues the session, and the link in the inbox is now inert.
The window is the design, not a bug to engineer away. Every knob you have controls how long step 3 lasts and how many times it can pay out. Short expiry shrinks the window. Single-use closes it the moment the link is redeemed. Neither one lets you skip the question of whether email is a good enough gate for what the account can do.
What this changes about how you build it
Three things follow from the shape above, and they are the ones that get missed.
Rate limit the request endpoint, not just the redemption. Anyone who can type an email address into your form can cause mail to be sent to it. Without a limit that is a spam cannon pointed at your users and a fast way to get your sending domain flagged.
Do not leak whether the account exists. The response to "send me a link" should look identical whether or not there is a user with that address. Otherwise the login form is an account-enumeration endpoint, and confirming that someone has an account somewhere is worth more to an attacker than it sounds.
Match the method to what the account holds. A magic link is a good fit for a reading account, a dashboard, a newsletter, an internal tool. For an account that can move money or read other people's data, email alone is a thin gate, and the honest answer is a second factor rather than a shorter expiry.
The source
---
title: "Your login token lives in the inbox"
---
sequenceDiagram
autonumber
participant S as Your server
participant D as Your database
participant E as Email, outside<br/>your control
participant B as Whatever device<br/>opens the link
S->>D: store the SHA-256 hash only.<br/>the raw token is never written down
S->>E: the raw token, inside a link
rect rgb(254, 242, 242)
E->>B: for 15 minutes anything that can read<br/>this mailbox can open the link
end
B->>S: the raw token comes back
S->>D: hash it again and compare
D-->>S: match, and not used yet
S->>D: mark it used, right now
S-->>B: session issued
Note over S,B: your database never held the live token.<br/>the inbox did.
Why store a hash of the magic link token instead of the token itself?
So that reading your database does not hand someone a working login. If you store the raw token, anyone with a database dump, a leaked backup, or read access through an unrelated bug can paste it into the URL and become that user. Storing the SHA-256 hash means the database holds something that cannot be turned back into a link, and the only usable copy is the one already in the user's inbox.
Does a magic link mean my app is only as secure as the user's email?
For the lifetime of that link, yes. Anything that can read the mailbox can open the link, and the app cannot tell the difference. That is why short expiry and single-use both matter more here than they would elsewhere, and why magic links are a poor fit on their own for accounts holding money or other people's data.
How short should the expiry be?
Fifteen minutes is the common default and a reasonable place to start. The number trades the window an attacker gets against the number of users who wander off mid-login and come back to a dead link. Shorter is safer, but push it under about five minutes and you will start getting support requests from people whose mail provider delayed delivery.
Why does the token have to be single use?
Because the link does not vanish once it works. It stays in the mailbox, in any mail backup, and in whatever search index the mail client keeps. Marking it used at the moment of redemption makes the copy sitting in the inbox inert, so a mailbox compromised next month cannot be used to log in with an old email.
Is it safe that the link might open on a different device?
It is normal, and it is why the flow works when someone requests a link on a laptop and reads mail on a phone. It does mean the session gets issued to whatever device opened the link rather than the one that asked, so do not treat opening a magic link as proof the original requester is present. If a step needs that assurance, ask for it separately.
Check What Your Login Flow Exposes
Our scanner looks at your deployed app for the things this diagram implies: unthrottled auth endpoints, responses that reveal whether an account exists, and tokens that survive longer than they should.