{"id":18729566,"url":"https://github.com/pbteja1998/remix-auth-email-link","last_synced_at":"2025-04-06T14:12:40.391Z","repository":{"id":42666886,"uuid":"440690953","full_name":"pbteja1998/remix-auth-email-link","owner":"pbteja1998","description":null,"archived":false,"fork":false,"pushed_at":"2024-03-22T14:14:12.000Z","size":296,"stargazers_count":93,"open_issues_count":6,"forks_count":26,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-30T12:07:19.132Z","etag":null,"topics":["authentication","remix","remix-auth"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pbteja1998.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-22T00:49:48.000Z","updated_at":"2024-12-18T16:39:36.000Z","dependencies_parsed_at":"2024-12-24T02:11:03.181Z","dependency_job_id":"a4d34565-68ed-4f0b-99da-0bca166e52a2","html_url":"https://github.com/pbteja1998/remix-auth-email-link","commit_stats":{"total_commits":67,"total_committers":12,"mean_commits":5.583333333333333,"dds":0.5223880597014925,"last_synced_commit":"1c08123c3b7ff8a1c23944731ebd45296e0f010d"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":"sergiodxa/remix-auth-strategy-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbteja1998%2Fremix-auth-email-link","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbteja1998%2Fremix-auth-email-link/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbteja1998%2Fremix-auth-email-link/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pbteja1998%2Fremix-auth-email-link/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pbteja1998","download_url":"https://codeload.github.com/pbteja1998/remix-auth-email-link/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247492565,"owners_count":20947545,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","remix","remix-auth"],"created_at":"2024-11-07T14:27:35.542Z","updated_at":"2025-04-06T14:12:40.373Z","avatar_url":"https://github.com/pbteja1998.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Email Link Strategy - Remix Auth\n\n\u003e This strategy is heavily based on **kcd** strategy present in the [v2 of Remix Auth](https://github.com/sergiodxa/remix-auth/blob/v2.6.0/docs/strategies/kcd.md). The major difference being we are using `crypto-js` instead of `crypto` so that it can be deployed on CF.\n\nThe Email Link Strategy implements the authentication strategy used on [kentcdodds.com](https://kentcdodds.com).\n\nThis strategy uses passwordless flow with magic links. A magic link is a special URL generated when the user tries to login, this URL is sent to the user via email, after the click on it the user is automatically logged in.\n\nYou can read more about how this work in the [kentcdodds.com/how-i-built-a-modern-website-in-2021](https://kentcdodds.com/blog/how-i-built-a-modern-website-in-2021#authentication-with-magic-links).\n\n## Supported runtimes\n\n| Runtime    | Has Support |\n| ---------- | ----------- |\n| Node.js    | ✅          |\n| Cloudflare | ✅          |\n\n\u003c!-- If it doesn't support one runtime, explain here why --\u003e\n\n## How to use\n\n\u003c!-- Explain how to use the strategy, here you should tell what options it expects from the developer when instantiating the strategy --\u003e\n\n## Setup\n\nBecause of how this strategy works you need a little bit more setup than other strategies, but nothing specially crazy.\n\n### Email Service\n\nYou will need to have some email service configured in your application. What you actually use to send emails is not important, as far as you can create a function with this type:\n\n```ts\ntype SendEmailOptions\u003cUser\u003e = {\n  emailAddress: string\n  magicLink: string\n  user?: User | null\n  domainUrl: string\n  form: FormData\n}\n\ntype SendEmailFunction\u003cUser\u003e = {\n  (options: SendEmailOptions\u003cUser\u003e): Promise\u003cvoid\u003e\n}\n```\n\nSo if you have something like `app/services/email-provider.server.ts` file exposing a generic function like `sendEmail` function receiving an email address, subject and body, you could use it like this:\n\n```tsx\n// app/services/email.server.tsx\nimport { renderToString } from 'react-dom/server'\nimport type { SendEmailFunction } from 'remix-auth-email-link'\nimport type { User } from '~/models/user.model'\nimport * as emailProvider from '~/services/email-provider.server'\n\nexport let sendEmail: SendEmailFunction\u003cUser\u003e = async (options) =\u003e {\n  let subject = \"Here's your Magic sign-in link\"\n  let body = renderToString(\n    \u003cp\u003e\n      Hi {options.user?.name || 'there'},\u003cbr /\u003e\n      \u003cbr /\u003e\n      \u003ca href={options.magicLink}\u003eClick here to login on example.app\u003c/a\u003e\n    \u003c/p\u003e\n  )\n\n  await emailProvider.sendEmail(options.emailAddress, subject, body)\n}\n```\n\nAgain, what you use as email provider is not important, you could use a third party service like [Mailgun](https://mailgun.com) or [Sendgrid](https://sendgrid.com), if you are using AWS you could use SES.\n\n### Create the strategy instance\n\nNow that you have your sendEmail email function you can create an instance of the Authenticator and the EmailLinkStrategy.\n\n```ts\n// app/services/auth.server.ts\nimport { Authenticator } from 'remix-auth'\nimport { EmailLinkStrategy } from 'remix-auth-email-link'\nimport { sessionStorage } from '~/services/session.server'\nimport { sendEmail } from '~/services/email.server'\nimport { User, getUserByEmail } from '~/models/user.server'\n\n// This secret is used to encrypt the token sent in the magic link and the\n// session used to validate someone else is not trying to sign-in as another\n// user.\nlet secret = process.env.MAGIC_LINK_SECRET\nif (!secret) throw new Error('Missing MAGIC_LINK_SECRET env variable.')\n\nexport let auth = new Authenticator\u003cUser\u003e(sessionStorage)\n\n// Here we need the sendEmail, the secret and the URL where the user is sent\n// after clicking on the magic link\nauth.use(\n  new EmailLinkStrategy(\n    { sendEmail, secret, callbackURL: '/magic' },\n    // In the verify callback,\n    // you will receive the email address, form data and whether or not this is being called after clicking on magic link\n    // and you should return the user instance\n    async ({\n      email,\n      form,\n      magicLinkVerify,\n    }: {\n      email: string\n      form: FormData\n      magicLinkVerify: boolean\n    }) =\u003e {\n      let user = await getUserByEmail(email)\n      return user\n    }\n  )\n)\n```\n\n### Setup your routes\n\nNow you can proceed to create your routes and do the setup.\n\n```tsx\n// app/routes/login.tsx\nimport { ActionArgs, LoaderArgs } from '@remix-run/node'\nimport { json } from '@remix-run/node'\nimport { Form, useLoaderData } from '@remix-run/react'\nimport { auth } from '~/services/auth.server'\nimport { sessionStorage } from '~/services/session.server'\n\nexport let loader = async ({ request }: LoaderArgs) =\u003e {\n  await auth.isAuthenticated(request, { successRedirect: '/me' })\n  let session = await sessionStorage.getSession(request.headers.get('Cookie'))\n  // This session key `auth:magiclink` is the default one used by the EmailLinkStrategy\n  // you can customize it passing a `sessionMagicLinkKey` when creating an\n  // instance.\n  return json({\n    magicLinkSent: session.has('auth:magiclink'),\n    magicLinkEmail: session.get('auth:email'),\n  })\n}\n\nexport let action = async ({ request }: ActionArgs) =\u003e {\n  // The success redirect is required in this action, this is where the user is\n  // going to be redirected after the magic link is sent, note that here the\n  // user is not yet authenticated, so you can't send it to a private page.\n  await auth.authenticate('email-link', request, {\n    successRedirect: '/login',\n    // If this is not set, any error will be throw and the ErrorBoundary will be\n    // rendered.\n    failureRedirect: '/login',\n  })\n}\n\n// app/routes/login.tsx\nexport default function Login() {\n  let { magicLinkSent, magicLinkEmail } = useLoaderData\u003ctypeof loader\u003e()\n\n  return (\n    \u003cForm action=\"/login\" method=\"post\"\u003e\n      {magicLinkSent ? (\n        \u003cp\u003e\n          Successfully sent magic link{' '}\n          {magicLinkEmail ? `to ${magicLinkEmail}` : ''}\n        \u003c/p\u003e\n      ) : (\n        \u003c\u003e\n          \u003ch1\u003eLog in to your account.\u003c/h1\u003e\n          \u003cdiv\u003e\n            \u003clabel htmlFor=\"email\"\u003eEmail address\u003c/label\u003e\n            \u003cinput id=\"email\" type=\"email\" name=\"email\" required /\u003e\n          \u003c/div\u003e\n          \u003cbutton\u003eEmail a login link\u003c/button\u003e\n        \u003c/\u003e\n      )}\n    \u003c/Form\u003e\n  )\n}\n```\n\n```tsx\n// app/routes/magic.tsx\nimport { LoaderArgs } from '@remix-run/node'\nimport { auth } from '~/services/auth.server'\n\nexport let loader = async ({ request }: LoaderArgs) =\u003e {\n  await auth.authenticate('email-link', request, {\n    // If the user was authenticated, we redirect them to their profile page\n    // This redirect is optional, if not defined the user will be returned by\n    // the `authenticate` function and you can render something on this page\n    // manually redirect the user.\n    successRedirect: '/me',\n    // If something failed we take them back to the login page\n    // This redirect is optional, if not defined any error will be throw and\n    // the ErrorBoundary will be rendered.\n    failureRedirect: '/login',\n  })\n}\n```\n\n```tsx\n// app/routes/me.tsx\nimport { LoaderArgs } from '@remix-run/node'\nimport { json } from '@remix-run/node'\nimport { useLoaderData } from '@remix-run/react'\nimport { auth } from '~/services/auth.server'\n\nexport let loader = async ({ request }: LoaderArgs) =\u003e {\n  // If the user is here, it's already authenticated, if not redirect them to\n  // the login page.\n  let user = await auth.isAuthenticated(request, { failureRedirect: '/login' })\n  return json({ user })\n}\n\nexport default function Me() {\n  let { user } = useLoaderData\u003ctypeof loader\u003e()\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eWelcome {user.name}\u003c/h1\u003e\n      \u003cp\u003eYou are logged in as {user.email}\u003c/p\u003e\n    \u003c/div\u003e\n  )\n}\n```\n\n## Email validation\n\nThe EmailLinkStrategy also supports email validation, this is useful if you want to prevent someone from signing-in with a disposable email address or you have some denylist of emails for some reason.\n\nBy default, the EmailStrategy will validate every email against the regular expression `/.+@.+/`, if it doesn't pass it will throw an error.\n\nIf you want to customize it you can create a function with this type and pass it to the EmailLinkStrategy.\n\n```ts\ntype VerifyEmailFunction = {\n  (email: string): Promise\u003cvoid\u003e\n}\n```\n\n### Example\n\n```ts\n// app/services/verifier.server.ts\nimport { VerifyEmailFunction } from 'remix-auth-email-link'\nimport { isEmailBurner } from 'burner-email-providers'\nimport isEmail from 'validator/lib/isEmail'\n\nexport let verifyEmailAddress: VerifyEmailFunction = async (email) =\u003e {\n  if (!isEmail(email)) throw new Error('Invalid email address.')\n  if (isEmailBurner(email)) throw new Error('Email not allowed.')\n}\n```\n\n```ts\n// app/services/auth.server.ts\nimport { Authenticator } from 'remix-auth'\nimport { Authenticator, EmailLinkStrategy } from 'remix-auth-email-link'\nimport { sessionStorage } from '~/services/session.server'\nimport { sendEmail } from '~/services/email.server'\nimport { User, getUserByEmail } from '~/models/user.model'\nimport { verifyEmailAddress } from '~/services/verifier.server'\n\n// This secret is used to encrypt the token sent in the magic link and the\n// session used to validate someone else is not trying to sign-in as another\n// user.\nlet secret = process.env.MAGIC_LINK_SECRET\nif (!secret) throw new Error('Missing MAGIC_LINK_SECRET env variable.')\n\nlet auth = new Authenticator\u003cUser\u003e(sessionStorage)\n\n// Here we need the sendEmail, the secret and the URL where the user is sent\n// after clicking on the magic link\nauth.use(\n  new EmailLinkStrategy(\n    { verifyEmailAddress, sendEmail, secret, callbackURL: '/magic' },\n    // In the verify callback you will only receive the email address and you\n    // should return the user instance\n    async ({ email }: { email: string }) =\u003e {\n      let user = await getUserByEmail(email)\n      return user\n    }\n  )\n)\n```\n\n## Options options\n\nThe EmailLinkStrategy supports a few more optional configuration options you can set. Here's the whole type with each option commented.\n\n```ts\ntype EmailLinkStrategyOptions\u003cUser\u003e = {\n  /**\n   * The endpoint the user will go after clicking on the email link.\n   * A whole URL is not required, the pathname is enough, the strategy will\n   * detect the host of the request and use it to build the URL.\n   * @default \"/magic\"\n   */\n  callbackURL?: string\n  /**\n   * A function to send the email. This function should receive the email\n   * address of the user and the URL to redirect to and should return a Promise.\n   * The value of the Promise will be ignored.\n   */\n  sendEmail: SendEmailFunction\u003cUser\u003e\n  /**\n   * A function to validate the email address. This function should receive the\n   * email address as a string and return a Promise. The value of the Promise\n   * will be ignored, in case of error throw an error.\n   *\n   * By default it only test the email against the RegExp `/.+@.+/`.\n   */\n  verifyEmailAddress?: VerifyEmailFunction\n  /**\n   * A secret string used to encrypt and decrypt the token and magic link.\n   */\n  secret: string\n  /**\n   * The name of the form input used to get the email.\n   * @default \"email\"\n   */\n  emailField?: string\n  /**\n   * The param name the strategy will use to read the token from the email link.\n   * @default \"token\"\n   */\n  magicLinkSearchParam?: string\n  /**\n   * How long the magic link will be valid. Default to 30 minutes.\n   * @default 1_800_000\n   */\n  linkExpirationTime?: number\n  /**\n   * The key on the session to store any error message.\n   * @default \"auth:error\"\n   */\n  sessionErrorKey?: string\n  /**\n   * The key on the session to store the magic link.\n   * @default \"auth:magiclink\"\n   */\n  sessionMagicLinkKey?: string\n  /**\n   * Add an extra layer of protection and validate the magic link is valid.\n   * @default false\n   */\n  validateSessionMagicLink?: boolean\n\n  /**\n   * The key on the session to store the email.\n   * It's unset the same time the sessionMagicLinkKey is.\n   * @default \"auth:email\"\n   */\n  sessionEmailKey?: string\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpbteja1998%2Fremix-auth-email-link","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpbteja1998%2Fremix-auth-email-link","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpbteja1998%2Fremix-auth-email-link/lists"}