Back to Blog
Product | Auth | Email | Magic Link

Protect your Magic Links from email clients

5 March 2024
Transparent lines
Banner of Protect your Magic Links from email clients

When 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:

Reset password flow without redirect

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 allow for redirection to the authentication service endpoint seamlessly.

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 and then redirects the user to the auth service. The following is an example of such page in React.


_33
import { useEffect, useState } from 'react'
_33
import { useNhostClient } from '@nhost/react'
_33
import { useSearchParams } from 'react-router-dom'
_33
_33
const VerifyPage: React.FC = () => {
_33
const nhost = useNhostClient()
_33
const [loading, setLoading] = useState(true)
_33
const [searchParams] = useSearchParams()
_33
_33
useEffect(() => {
_33
const ticket = searchParams.get('ticket')
_33
const redirectTo = searchParams.get('redirectTo')
_33
const type = searchParams.get('type')
_33
_33
if (ticket && redirectTo && type) {
_33
window.location.href = `${nhost.auth.url}/verify?ticket=${ticket}&type=${type}&redirectTo=${redirectTo}`
_33
}
_33
_33
setLoading(false)
_33
}, [searchParams, nhost?.auth?.url])
_33
_33
if (loading) {
_33
return null
_33
}
_33
_33
return (
_33
<div>
_33
<span>Failed to authenticate with magick link</span>
_33
</div>
_33
)
_33
}
_33
_33
export default VerifyPage

Now here's the sequence diagram after applying the above strategy:

Reset password flow with redirect

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.

Share this post

Twitter LogoLinkedIn LogoFacebook Logo