[{"data":1,"prerenderedAt":365},["ShallowReactive",2],{"blog-diagrams/magic-link-out-of-band":3},{"id":4,"title":5,"body":6,"category":333,"date":334,"dateModified":334,"description":335,"draft":336,"extension":337,"faq":338,"featured":336,"headerVariant":348,"image":349,"keywords":351,"meta":352,"navigation":176,"ogDescription":353,"ogTitle":354,"path":355,"readTime":356,"schemaOrg":357,"schemaType":358,"seo":359,"sitemap":360,"stem":361,"tags":362,"twitterCard":363,"__hash__":364},"blog/blog/diagrams/magic-link-out-of-band.md","Magic Links: The Out of Band Hop (Diagram)",{"type":7,"value":8,"toc":327},"minimark",[9,13,16,25,30,33,40,46,52,62,68,77,81,84,90,96,102,106,259,293,311,323],[10,11,12],"p",{},"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.",[10,14,15],{},"That detour is the whole security model, and it is the part a numbered list of implementation steps flattens.",[17,18],"diagram",{"alt":19,"caption":20,"height":21,"href":22,"src":23,"width":24},"A sequence diagram of a magic link login. Your server stores only the SHA-256 hash of the token and emails the raw token, so for the lifetime of the link the only usable copy sits in a mailbox that any device can read, and the token becomes worthless only once it comes back and your server marks it used.","Your database stores a hash. The inbox holds the real thing.",1262,"/blog/how-to/magic-links","magic-link-out-of-band",2168,[26,27,29],"h2",{"id":28},"reading-the-diagram","Reading the diagram",[10,31,32],{},"Four parties, and the question to hold onto is: at each moment, who is holding something that can log in?",[10,34,35,39],{},[36,37,38],"strong",{},"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.",[10,41,42,45],{},[36,43,44],{},"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.",[10,47,48,51],{},[36,49,50],{},"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.",[10,53,54,57,58,61],{},[36,55,56],{},"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 ",[36,59,60],{},"at that moment"," rather than after issuing the session. Get that ordering wrong and two requests arriving together can both redeem the same link.",[10,63,64,67],{},[36,65,66],{},"Step 8"," issues the session, and the link in the inbox is now inert.",[69,70,71],"warning-box",{},[10,72,73,76],{},[36,74,75],{},"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.",[26,78,80],{"id":79},"what-this-changes-about-how-you-build-it","What this changes about how you build it",[10,82,83],{},"Three things follow from the shape above, and they are the ones that get missed.",[10,85,86,89],{},[36,87,88],{},"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.",[10,91,92,95],{},[36,93,94],{},"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.",[10,97,98,101],{},[36,99,100],{},"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.",[26,103,105],{"id":104},"the-source","The source",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-mermaid shiki shiki-themes github-dark","---\ntitle: \"Your login token lives in the inbox\"\n---\nsequenceDiagram\n    autonumber\n    participant S as Your server\n    participant D as Your database\n    participant E as Email, outside\u003Cbr/>your control\n    participant B as Whatever device\u003Cbr/>opens the link\n\n    S->>D: store the SHA-256 hash only.\u003Cbr/>the raw token is never written down\n    S->>E: the raw token, inside a link\n\n    rect rgb(254, 242, 242)\n        E->>B: for 15 minutes anything that can read\u003Cbr/>this mailbox can open the link\n    end\n\n    B->>S: the raw token comes back\n    S->>D: hash it again and compare\n    D-->>S: match, and not used yet\n    S->>D: mark it used, right now\n    S-->>B: session issued\n\n    Note over S,B: your database never held the live token.\u003Cbr/>the inbox did.\n","mermaid","",[114,115,116,124,130,135,141,147,153,159,165,171,178,184,190,195,201,207,213,218,224,230,236,242,248,253],"code",{"__ignoreMap":112},[117,118,121],"span",{"class":119,"line":120},"line",1,[117,122,123],{},"---\n",[117,125,127],{"class":119,"line":126},2,[117,128,129],{},"title: \"Your login token lives in the inbox\"\n",[117,131,133],{"class":119,"line":132},3,[117,134,123],{},[117,136,138],{"class":119,"line":137},4,[117,139,140],{},"sequenceDiagram\n",[117,142,144],{"class":119,"line":143},5,[117,145,146],{},"    autonumber\n",[117,148,150],{"class":119,"line":149},6,[117,151,152],{},"    participant S as Your server\n",[117,154,156],{"class":119,"line":155},7,[117,157,158],{},"    participant D as Your database\n",[117,160,162],{"class":119,"line":161},8,[117,163,164],{},"    participant E as Email, outside\u003Cbr/>your control\n",[117,166,168],{"class":119,"line":167},9,[117,169,170],{},"    participant B as Whatever device\u003Cbr/>opens the link\n",[117,172,174],{"class":119,"line":173},10,[117,175,177],{"emptyLinePlaceholder":176},true,"\n",[117,179,181],{"class":119,"line":180},11,[117,182,183],{},"    S->>D: store the SHA-256 hash only.\u003Cbr/>the raw token is never written down\n",[117,185,187],{"class":119,"line":186},12,[117,188,189],{},"    S->>E: the raw token, inside a link\n",[117,191,193],{"class":119,"line":192},13,[117,194,177],{"emptyLinePlaceholder":176},[117,196,198],{"class":119,"line":197},14,[117,199,200],{},"    rect rgb(254, 242, 242)\n",[117,202,204],{"class":119,"line":203},15,[117,205,206],{},"        E->>B: for 15 minutes anything that can read\u003Cbr/>this mailbox can open the link\n",[117,208,210],{"class":119,"line":209},16,[117,211,212],{},"    end\n",[117,214,216],{"class":119,"line":215},17,[117,217,177],{"emptyLinePlaceholder":176},[117,219,221],{"class":119,"line":220},18,[117,222,223],{},"    B->>S: the raw token comes back\n",[117,225,227],{"class":119,"line":226},19,[117,228,229],{},"    S->>D: hash it again and compare\n",[117,231,233],{"class":119,"line":232},20,[117,234,235],{},"    D-->>S: match, and not used yet\n",[117,237,239],{"class":119,"line":238},21,[117,240,241],{},"    S->>D: mark it used, right now\n",[117,243,245],{"class":119,"line":244},22,[117,246,247],{},"    S-->>B: session issued\n",[117,249,251],{"class":119,"line":250},23,[117,252,177],{"emptyLinePlaceholder":176},[117,254,256],{"class":119,"line":255},24,[117,257,258],{},"    Note over S,B: your database never held the live token.\u003Cbr/>the inbox did.\n",[260,261,262,269,275,281,287],"faq-section",{},[263,264,266],"faq-item",{"question":265},"Why store a hash of the magic link token instead of the token itself?",[10,267,268],{},"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.",[263,270,272],{"question":271},"Does a magic link mean my app is only as secure as the user's email?",[10,273,274],{},"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.",[263,276,278],{"question":277},"How short should the expiry be?",[10,279,280],{},"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.",[263,282,284],{"question":283},"Why does the token have to be single use?",[10,285,286],{},"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.",[263,288,290],{"question":289},"Is it safe that the link might open on a different device?",[10,291,292],{},"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.",[294,295,296,301,306],"related-articles",{},[297,298],"related-card",{"description":299,"href":22,"title":300},"The full build: schema, token generation, hashing, expiry, and the redemption endpoint","How to Implement Magic Link Authentication",[297,302],{"description":303,"href":304,"title":305},"What happens to the session this flow issues, and where revocation stops working","/blog/diagrams/session-lifecycle-and-the-jwt-gap","Session Lifecycle and the JWT Gap",[297,307],{"description":308,"href":309,"title":310},"The same custody question asked of an OAuth login instead of an emailed one","/blog/diagrams/pkce-who-holds-the-secret","PKCE: Who Holds the Secret at Each Hop",[312,313,316,320],"cta-box",{"href":314,"label":315},"/","Start Free Scan",[26,317,319],{"id":318},"check-what-your-login-flow-exposes","Check What Your Login Flow Exposes",[10,321,322],{},"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.",[324,325,326],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":112,"searchDepth":126,"depth":126,"links":328},[329,330,331,332],{"id":28,"depth":126,"text":29},{"id":79,"depth":126,"text":80},{"id":104,"depth":126,"text":105},{"id":318,"depth":126,"text":319},"diagrams","2026-08-14","A sequence diagram of magic link login, showing that the only live copy of the token sits in an inbox you do not control, and what the single-use burn on the way back is actually for.",false,"md",[339,340,342,344,346],{"question":265,"answer":268},{"question":271,"answer":341},"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 for a password reset, and why magic links are a poor fit on their own for accounts holding money or other people's data.",{"question":277,"answer":343},"Fifteen minutes is the common default and it is a reasonable place to start. The number is a trade between the window an attacker gets and the number of users who wander off mid-login and come back to a dead link. Shorter is safer, but if you push it under about five minutes you will start getting support requests from people whose mail provider delayed delivery.",{"question":283,"answer":345},"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 means the copy sitting in the inbox is inert from then on, so a mailbox compromised next month cannot be used to log in with an old email.",{"question":289,"answer":347},"It is normal, and it is the reason the flow works at all 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.","blue",{"src":350,"alt":19},"https://checkyourvibe.dev/diagrams/magic-link-out-of-band.png","magic link diagram, magic link security, passwordless login flow, magic link single use token, out of band authentication, magic link token expiry",{"ogImage":350},"Your database stores a hash. The inbox holds the real thing. That gap is the whole security model of a magic link.",null,"/blog/diagrams/magic-link-out-of-band","4 min read","[object Object]","Article",{"title":5,"description":335},{"loc":355},"blog/diagrams/magic-link-out-of-band",[],"summary_large_image","YhZBwUlosACebYdjnL7_xXJb9Fo8j3n0TusLQTlMQGM",1787602613791]