Magic links: how passwordless sign-in emails work
How magic link sign-in works, the token flow that keeps it safe, a sign-in email template in HTML and plain text, and how to send it with Boundry.
A magic link is a single-use sign-in link sent to a person's email address. They ask to sign in, your server emails them a link, and opening it signs them in with no password.
A magic link is the same thing as a password reset link that is always on offer: whoever can open the email can get into the account. So the security of the sign-in is the security of the mailbox, and of the token behind the link. Build the token flow carefully first, then write the email.
How a magic link works
- The person types their email address into a sign-in form.
- Your server creates a random token, stores a hash of it with the address and an expiry, and emails a link containing the token.
- The person opens the link on a page of yours. You check the token, mark it used, and start a session.
- The token never works again, and expires on its own if it is never used.
Because the link proves control of an inbox, a magic link also verifies the email address. Many products use the same flow for sign-up, so there is no separate confirmation step.
Magic links compared with passwords and codes
- Passwords: nothing to send, but you store and protect them, and people forget them, which brings back the reset email.
- Magic links: one tap on the device that has the email open. The sign-in depends on the email arriving quickly, and the reader has to leave your page to find it.
- One-time codes: the person types a short code from the email into the page they started on. This works well when they read email on a different device from the one they are signing in on, because a link opened on the phone signs in the phone.
Many products offer a link and a code in the same email, so the reader can pick whichever suits their device. If you do, both must be single-use and expire together.
The token flow that keeps it safe
The rules are the same as for password reset links, because the risk is the same. The OWASP Forgot Password Cheat Sheet is a good reference for the shared parts.
- Generate the token from a cryptographically secure random source, at least 128 bits. Store only a hash of it, with the address, the creation time and an expiry.
- Expire it quickly. Ten to fifteen minutes is common for sign-in, shorter than a reset link, because the person is waiting for it.
- Make it single-use. When a new link is requested for the same address, invalidate the earlier ones.
- Do not sign in on a plain GET. Some mail scanners and link previewers open every link in a message, and would use up the token before the person does. Have the link open a page with a 'Sign in' button, and check the token when that button posts.
- Always show the same message after the form is submitted, such as 'If that address can sign in, we have sent a link'. Do not reveal which addresses have accounts.
- Send the link only to the address that was entered and check that the address matches the account. Never let the request choose a different destination.
- Optionally tie the link to the browser that asked for it, with a cookie set when the form was submitted. A link opened elsewhere then asks for a code instead of signing in. This stops a forwarded or intercepted link from working, at the cost of breaking the 'request on desktop, open on phone' habit.
- Rate limit requests per address and per IP so the form cannot be used to flood someone's inbox.
- If your link includes a redirect to the page the person wanted, only accept paths on your own site.
- Serve the sign-in page with a Referrer-Policy of no-referrer so the token is not passed on in the Referer header.
Magic link email template
A sign-in email is short. Name the action, give the link, say how long it lasts and tell the wrong person to ignore it. Replace Acme, the domain and the expiry with your own, and send the plain-text part alongside the HTML one so the link shows in every client.
<p>Hi,</p>
<p>Use this link to sign in to Acme.</p>
<p><a href="https://acme.com/signin/confirm?token=TOKEN">Sign in to Acme</a></p>
<p>This link works once and expires in 15 minutes. If the button does not work, copy this address into your browser:</p>
<p>https://acme.com/signin/confirm?token=TOKEN</p>
<p>If you did not ask to sign in, ignore this email. Do not share this link.</p>
<p>Need help? Reply to this email and a person will read it.</p>Hi,
Use this link to sign in to Acme:
https://acme.com/signin/confirm?token=TOKEN
This link works once and expires in 15 minutes. If you did not ask to sign in, ignore this email. Do not share this link.
Need help? Reply to this email and a person will read it.Subject lines for sign-in emails
- Sign in to Acme. The default, and the clearest.
- Your Acme sign-in link. Easy to find again when the reader searches their inbox.
- Your Acme sign-in link (expires in 15 minutes). Puts the deadline where it is seen without opening the email.
- Avoid 'Action required', 'Urgent' and exclamation marks. They read like phishing, which a sign-in email already gets mistaken for.
Do not put the one-time code or link in the subject line, since subjects show up in notifications and lock screens. A code goes in the body, in large text, on its own line.
Why magic link emails need to arrive fast
A reset email can take a minute without much harm. A sign-in email cannot: the person is staring at your page. Three things decide whether it arrives in time.
- Authentication. Send from a domain with SPF, DKIM and DMARC set up, ideally a subdomain used only for transactional mail so its reputation is not shared with newsletters.
- A plain message. Short, with one link and no tracking redirects. Rewriting the sign-in link through a click-tracking domain is a common way to make a sign-in link look suspicious or break it. Do not track clicks on a sign-in link.
- Short expiry and a resend. Let the person ask for another link after a short wait, and make the old one stop working when they do.
The spam guide covers the DNS records and checks that decide whether a message is delivered at all. Why your emails go to spam
Send a magic link with Boundry
With Boundry you send the sign-in email with one call to the emails API from the JavaScript, Python, Go or PHP SDK, or with POST /emails directly. The Ruby SDK is an unpublished preview, so Ruby users should call POST /emails. This example uses the JavaScript SDK, a domain you have verified in your project, and a token you have already created and stored as a hash.
import Boundry from "boundry-sdk";
const client = new Boundry({
apiKey: process.env["BOUNDRY_API_KEY"],
});
const link = `https://acme.com/signin/confirm?token=${token}`;
await client.emails.send({
from: "Acme <accounts@mail.acme.com>",
reply_to: "Acme Support <support@acme.com>",
to: user.email,
subject: "Sign in to Acme",
html: `<p>Use this link to sign in to Acme.</p><p><a href="${link}">Sign in to Acme</a></p><p>This link works once and expires in 15 minutes. If you did not ask to sign in, ignore this email.</p>`,
text: `Use this link to sign in to Acme:\n\n${link}\n\nThis link works once and expires in 15 minutes. If you did not ask to sign in, ignore this email.`,
});The from address must belong to a domain you have verified in the same project. Boundry sends the message and reports what happened to it through events and webhooks; the token, its expiry and the sign-in page stay in your application. Subscribe to the email.bounced and email.complained webhooks so you hear when sign-in emails to an address stop being delivered, and offer another way in for that person.
The send-email API reference lists every field the emails API accepts. Read the send-email API reference
Common problems
- The link says it has expired or been used, though the person never clicked it. A mail scanner or link previewer opened it. Move the sign-in to a button press on the page, as above.
- The person opens the link on their phone and the desktop tab never signs in. Offer a code, or explain in the email which device will be signed in.
- The email takes minutes to arrive. Check authentication and the delivery events first, then your own queue before the send call.
- The link is cut in two in some clients. Keep the token short enough for one line, or put the plain URL on its own line.
- People forward the email or share an inbox. Keep expiry short, bind the link to the browser, and tell the reader not to forward it.
If you also offer passwords, the reset flow has the same token rules and its own template. Password reset email template
Bounce classes and what to do about each are in the email bounces guide. Email bounces, explained
Sign-in emails sent without a reply address leave readers stuck when the link fails. See what to send from instead. No-reply email addresses
Sign-in links are a kind of transactional email. The overview explains the category. What is transactional email?
Frequently asked questions
What is a magic link?
A magic link is a single-use sign-in link sent to a person's email address. Opening it signs them in without a password, and it proves they control the inbox.
Are magic links secure?
They are as secure as the mailbox and the token flow behind them. Use a random token of at least 128 bits, store only its hash, make it single-use, expire it within minutes, and confirm the sign-in with a button press rather than on a plain GET.
How long should a magic link last?
Ten to fifteen minutes is common. Shorter is safer, and the person is waiting for the email, so a short expiry rarely costs anything. Let them request a new link, and cancel the old one when they do.
Why does my magic link say it has already been used?
Usually a mail scanner or link previewer opened the link first. Make the link open a page with a sign-in button, and check the token only when the button is pressed.
Magic link or one-time code?
A link is quicker on the device that has the email. A code works better when the person reads email on a different device from the one they are signing in on. Many products send both in one email.
Can I send magic links with Boundry?
Yes. Boundry sends the email through its emails API (POST /emails, or emails.send in an official SDK) from a domain you have verified. You create and check the token in your own application.