Magic Link Pitfalls
65 points by Hales
65 points by Hales
God I hate those. Just give me two input fields, one for email/username, one for the password. On the same fricking page, please. Everything else just adds friction!
A similar pet peeve: services that implement 2FA by emailing or texting the code instead of simply letting you store the TOTP secret in your app of choice.
Just yesterday this failed when I wanted to login to a service and their email and texting infrastructure was extremely slow to send things.
Annoyingly TOTP is generally considered insecure because of relaying and cloning. Banks in Europe for instance are legally barred from using TOTP (does not pass the minimum requirements of the payments directive).
Yeah my Swedish bank uses the BankID system (as all banks here do), but it's not too much of a problem from a user perspective because it's an app on my phone and I don't have to wait for an email to (fail to) arrive.
There are similar systems, though, which are pretty much the same.
e.g., Photo TAN, which Commerzbank and Deutsche Bank use, where you scan a matrix code with either an app or device to activate it, and then for each transaction you get another such code, scan that, the device will generate a one-time token, which you then input.
Under the hood it's basically just TOTP/HOTP, but instead of using a timestamp or counter, it's using the actual transaction data that's presented in the per-transaction matrix code.
e.g., Photo TAN, which Commerzbank and Deutsche Bank use, where you scan a matrix code with either an app or device to activate it, and then for each transaction you get another such code, scan that, the device will generate a one-time token, which you then input.
I am not a Photo TAN user but pretty sure that the secret key for Photo TAN is literally your bank card smart card. Eg: you need physical possession of the chip that your account is linked to. As such it cannot be cloned. Photo TAN i believe also transfers information onto the device that lets you see what you are signing.
I run a small service that people only log into once or twice a year. We don't do magic link but it is very tempting, because lots of users forget their password.
Meanwhile they have constant access to their email, and their email conforms to whatever corporate security rules they have in place.
Implement passkey and magic link and get rid of passwords. Users will no longer have the 'forgot password' problem đ
Right, they will permanently lose their access as soon as they switch devices (and wonât remember theyâre supposed to add a new passkey from the new device). A smart way to increase the number of users!
I would prefer that a service supports passwords in some form, but Iâm not sure why you say that a service that supported only passkeys and magic links would cause users to permanently lose access âas soon as they switch devicesâ. The magic links are supposed to solve that.
How such a system might workWhen a user visits such a website on their new device and clicks âLog inâ, I think the website could ask the browser for a passkey, then display something like
Passkey not found on this device.
Enter your email: [ text field ] Email me a link to log in
After the user fills out the form, they would receive an email with a link. In the happy path, they would view their email on that same device, click the link, and be brought to a page on which the service offers to save a passkey for next time. Whether the user chooses to save a passkey or not, they would be logged in for that session. The user would never have to remember to do something with passkeys; they would just have to react to the siteâs prompts.
There are certainly flaws to that authentication flow. Users might not already be logged into their email on the device they are using, making the flow inconvenient. And a few users may be unable to receive or view the email due to spam filters, censorship, a conflict with their email provider, or domain name theft. Those users who cannot receive or view the email at all would permanently lose access. But saying that users in general would permanently lose access just from switching devices is overstating the problem.
A more robust authentication systemMy ideal solution would be that a service accepts passkeys and magic links, but also allows login with the userâs email address and a password. A twist: the password should be generated by the website when the user signs up, rather than asking the user for one. This would prevent users from setting a memorable password that is, as a side effect, liable to be easily cracked or the same password as the one they used on a site whose password database has leaked.
Users who save this generated password could log into the service on a new device without depending on email-related third parties. Users who have a password manager would experience no inconvenience from the randomly generated password. Users without a password manager could get that email independence by writing the password on a piece of paper or in a text file, but I bet most of them would stick to magic links, which work for most people.
If you can bypass passkey authentication, then what is the point of using a passkey in the first place?
Ah OK, so your objection has moved from 'users will permanently lose access' to 'what's the point' ;-)
The point is that passkeys are not 'the point', they are one way to achieve non-phishable auth. Email magic link is another way to achieve it. Unfortunately email magic link is annoying and we want to do it as little as possible. So we just do it on devices that don't have passkeys. And we use passkeys, which are fast, convenient, and secure, whenever we can.
The last time I griped about this my replies blew up with SAML people going on about SSO things I'm too small business to understand. Apparently there's a good reason for all of us to have our toes repeatedly stubbed this way :shrug:
You can also check whether an email is connected to an SSO provider while the user is entering the email, so by the time they press "<tab>" you can already show either a password field or a "Login with SSO" button.
This provides a weird experience if the password field disappears or something. What I really ask is to test with popular browsers and ensure that auto-fill works properly with your form, however it works.
Anyone claiming SSO to be the reason to have email and password be separate pages is just bullshitting.
I have plenty of services that integrate with my Authentik instance for SSO, and many of them have email + password field on the same page, with the option for SSO/IdP providers below it.
Roll that out at scale and now you have tons of users putting their SSO credentials into the third-partys login form and complaining it doesn't work. Or getting confused because they don't have a password (because they use other auth methods). Not everyone making different trade-offs then you is immediately "bullshitting".
Yeah, it's very hard to create enjoyable SSO flows that cover user error without splitting user email and password into two steps. I'm not happy about that either, but it's a really hard problem to solve.
Having SSO and magic link as the two options avoids that problem.
I trust my password manager and donât trust my SSO provider. Please donât get rid of username+password
On the same fricking page, please.
You can say that again. Multipage logins are so annoying and frustrating. Why do people even complicate this? Multipage logins can't possibly be simpler to implement than a couple of input fields.
I agree theyâre annoying. But theyâre largely a result of organisations having a variety of SSO rules. As a vendor can be tricky to work out without knowing which identity the user will be asserting. Not an excuse - just how we ended up here.
The only benefit of a magic link is proving you have control over an email address when logging in, which many more services want than is strictly necessary, because mailing lists are profitable or something
The only benefit of a magic link is proving you have control over an email address when logging in.
Which from a customer support perspective is very important. Generally magic links are maybe not optimal user experience on actual sign in, but it resolve the entire problem of people forgetting their passwords which happens all the time.
On the same fricking page, please. Everything else just adds friction!
Because of MFA/SSO/etc requirements, one is basically forced to move the password box to a different page to have any hope of UX sanity.
To avoid this pitfall, the link should do nothing but mark the code as âverifiedâ and instruct the user to return to their original browser tab, which should auto-refresh every few seconds to check whether the code is verified, and if so, log the user in.
Wouldn't this make the user vulnerable to phishing? An impersonating site can forward the login request to the legitimate site, and once the user clicks the magic link in their email, the attacker's code gets verified.
Somewhat alarmingly, some of these links were already claimed before I could click on them. Then I realized, some programs issue GET requests to render link previews.
Trial and error often helps discover problems, but the HTTP specification defines that safe methods, such as GET, do not change the state of anything. If an HTTP request changes something, use an unsafe method, such as POST.
Ooh boy is it fun when you find out that your edge proxy server has an error condition where they will retry a POST request.
People often say "the great thing about standards is there are so many to choose from." Sometimes there is only one standard to choose from, yet implementors still manage to choose something else.
It gets even more fun with redirects.
HTTP 301 and 302 were originally meant to preserve the request method and body, but many browsers implemented them by always discarding the request body and changing the method to GET when following the redirect.
So the spec got amended to allow this behavior, and a different set of redirect status codes (307 and 308) took over as the ones that require user agents to preserve the original request method and body.
I do remember this from many years ago. I think it was Chrome that implemented link prefetching and many people were complaining that they were logged out of different services (IIRC it was wordpress).
Took on a client around that time who was complaining that their "site kept getting hacked". Symptom: all of the content they'd added to the custom-built CMS was getting deleted. Upon further investigation I discovered that:
[X] delete button on each post was implemented as an unauthenticated GET requestGoogleBot, at the time, didn't really do JavaScript, so was not presented with any of the auth logic and just happily spidered every one of the delete links.
Another downside is you're training your users to click on links, which in the age of scams/malware/phising attempts is a bad practice.
Anyone that accesses your email can use the code (if it's still valid) . Send a code instead, it's only valid for a particualr session, even if someone intercepts the code they can't necessary use it.
Dear god YES. This 100%. Just today, I got an e-mail which was ostensibly from GitHub, about a signin from a "new" location in a country I've never visited and if I could please review it. I was on guard but the mail looked quite legit, except that the links were all rewritten by sendgrid (or it seemed like it was). So now I couldn't even see where the links went to.
I decided to log in to GitHub by just visiting the website manually and reviewing the sessions there. Of course, there was no trace of the session in question...
I got that one today too! It was very convincing, but upon closer inspection the sender was "alerts@ridemajestic.com", who I'm guessing got their sendgrid accounts compromised.
This reminds me of how my old bank, which we will say is named foobank, used foobank2.com in some of its URLs for reasons I don't understand. And the credit card management was done through foobank.mycreditcardaccess.com. It's like they were trying to make it impossible to build up a map of what URLs are trustworthy.
Every email message sent by an noreply@ address is a lost opportunity to engage with a customer.
A remarkable number of businesses are built on the core premise of not engaging with customers.
Plus the code is handy if you are trying to access the service from a different device to the one your email is on (very common for my users)
Another downside is you're training your users to click on links, which in the age of scams/malware/phising attempts is a bad practice.
Well, most phishing training materials teach people to be wary of unexpected links, but emphasize that when you're expecting to receive a link (or document, etc.) from someone that's a different situation. And really, for phishing and other email-based attacks the thing to do is not try to train people never to click a link, it's to create a situation where someone clicking a link is not an instantly ruinous game-over situation. Because someone is going to click a link sooner or later.
The database should store a hash of the secret code, not the code itself
That makes little sense and makes a lot of flows in practice tricky. In particular you want to reissue the same code if triggered again in a short period of time to avoid bad user interactions. Or at least keep the old code alive. Since they have to be short lived, there is absolutely no reason why you can't store them in plain text.
Also, secret codes are usually low entropy, so hashing them seems useless when it can be brute forced if leaked within seconds.
Rather than reissue the same code in a short time period, why not recognize that the already issued code is still valid and skip issuing the email, text, etc.?
Decreases the entropy if there are multiple valid ones. If you donât resend email / text you run into issues with people who accidentally deleted them or had networking issues.
I am generally in favor of people trying to understand and implement things themselves and not just defaulting to pulling in a library, but that doesn't mean that you should just try to invent everything from first principle without reading, learning, studying other solutions and so on. There are so many issues with what the article author is doing. Making the magic link validate a completely separate browser session makes this vulnerable to phishing, which makes a username/password combination just as secure as this. But in itself is not the real problem, the real problem is just thinking that you can test and reason your way into a secure system without learning about all of the possible ways that a secure solution can fall apart that have been discovered and resolved over time. This is not something you can solve via superior intellect alone.
Unlike most folks in the thread, I like magic links. They prevent me from having to track yet another password and so forth. In this sense, login is only a click away and I find that convenient.
You can do it like Slack does: Provide an option to login with magic links, but still allow traditional logins.
Agreed, but then this is also one of the reasons that some websites separate username and password fields across page (or JS re-render loop) boundaries, which many comments here are railing against.
Yes to all the "best practices" in this article, especially the extra click (which should be a POST) but it's important to consider the user base. There are a lot of people out there who can only barely manage their email password, so anything more complicated (like a password manager, even a built in browser one) may not work for those users. I helped run a multi-year basic income study where we used magic links because it was the lowest barrier our users in aggregate could manage.
I was never a fan of any "online" 2FA workfows, given I'm always expecting network interactions to fail. Better be able to log in with a code generated on my device than waiting for an e-mail that might never arrive. This could be the reason I started using TOTP somewhat early (Google says I'm using since 2012, not sure if I used it somewhere else before that), and while I didn't trust hardware keys that might be lost without backups, I liked passkeys ever since they became usable.
Still, to my surprise, it was only in recent years that I discovered that some people, both with and without technical backgrounds, simply reset their password every time they want to log in to a service they do it infrequently. My impression is that magic links were invented to solve this exact use case. As long as they are implemented in a safe-enough way (only valid for a single login, with a short expiration time, etc.), they are certainly more user-friendly than abusing the password resetting option.
It's a real shame that all these improvements make the service so much more stateful. Now you need to do database lookups on every login before even doing the lookups for what your program needs to display... It's a shame to make authenticaion so stateful.
But authentication is stateful process anyway⌠Due to the nature of web and authentication it must be stateful.
I don't think that's systemically true, since I believe that's part of the reason JWT was invented. If I sign a tuple of your user-id and some expiry with my RSA key then every server which has access to the public key can confirm that credential is valid without any change in the application's state. One may want to use caching/database/session/valkey/whatever but I don't think it's obligatory
Ironically, with that setup then unauthentication becomes stateful, if one needs to invalidate a token before that aforementioned expiry. I did actually wonder if that's what you meant but thought I'd take my chances with the comment anyway
You could argue that JWT logins are stateful, but the state is stored client-side. Used wisely, it's a great scalability enabler. For logouts, one way is to use a denylist, which is likely small enough (explicit logouts are rare, usually you just let the JWT time out) to keep synced in your service's memory.
I think we can probably add the security tag?
Use the |suggest| button to propose tag changes so that the comments are not cluttered with metadiscussion.