Protect your Magic Links from email clients
5 March 2024When working on a project involving user authentication, it's crucial to incorporate email verification and password reset capabilities. Additionally, Magic links provide a seamless mechanism by allowing users to log in without a password altogether. All of these methods share a common feature: they rely on sending a link to the user via email.
In recent years, email clients have undergone significant evolution, introducing numerous features and security enhancements. However, these advancements can sometimes disrupt the integrity of embedded links, potentially resulting in unintended consequences. Notable among these features are the ability to crawl links within emails to generate inline previews and automated security scans that seek out malicious content.
Since Magic Links are designed for one-time use, one of those features can occasionally render the links invalid. Consequently, your users would encounter a broken link, disrupting their intended access.
Here's a sequence diagram to better explain what happens in the case of a reset password flow:
To address this issue, you need to implement an additional step by refraining from sending the magic link directly in the email. Instead, you can include a link to a specific page within your app that contains the required parameters in the URL. This setup would ensure that only a real user can trigger the redirection to the authentication service endpoint.
When performing reset the password flow, users typically receive an email containing a link structured as follows:
https://[subdomain].auth.[region].nhost.run/v1/verify?ticket=...&type=...&redirectTo=...
Instead of sending this link, you redirect users to your app with the all the required parameters. Fortunately we make it super easy to do this. You only have to edit the relevant email template in your nhost folder under nhost/emails/en/reset-password/body.html
.
_19<!DOCTYPE html>_19<html>_19_19<head>_19 <meta charset="utf-8" />_19</head>_19_19<body>_19 <h2>Reset Password</h2>_19 <p>Use this link to reset your password:</p>_19 <p>_19- <a href="${link}">_19+ <a href="${clientUrl}/verify?ticket=${ticket}&redirectTo=${redirectTo}&type=emailVerify">_19 Reset password_19 </a>_19 </p>_19</body>_19_19</html>
Then, in your app, you add another page that reads those parameters from the URL. This new page will include a button that users can click on to trigger the verification process and redirect them to the auth service. The following is an example of such a page in React.
_28import { useNhostClient } from '@nhost/react'_28import { useSearchParams } from 'react-router-dom'_28_28const VerifyPage = () => {_28 const nhost = useNhostClient()_28 const [searchParams] = useSearchParams()_28_28 const redirectToVerificationLink = () => {_28 const ticket = searchParams.get('ticket')_28 const type = searchParams.get('type')_28 const redirectTo = searchParams.get('redirectTo')_28_28 if (ticket && type && redirectTo) {_28 window.location.href = `${nhost.auth.url}/verify?ticket=${ticket}&type=${type}&redirectTo=${redirectTo}`_28 }_28 }_28_28 return (_28 <div>_28 <span>Please verify your account by clicking the link below.</span>_28 <button onClick={redirectToVerificationLink}>_28 Verify_28 </Button>_28 </div>_28 )_28}_28_28export default VerifyPage
Now here's the sequence diagram after applying the above strategy:
Wrap up
The strategy detailed in this blog post isn't limited to password reset flows but can also be applied in other use cases such as email verification and one-time file download links. By centralizing link handling within the application, the validity of such links remains intact across various email clients and scenarios.