Implementing Magic Link Email Authentication in NextJS + NodeJS Applications
Tags: nextjs, authentication, magiclink, webdev
Published: 2023-10-08 13:35:51

{% embed https://youtu.be/b_BZKpeLxO0 %}

🐙 [GitHub](https://github.com/radzionc/reactkit)  |  🎮 [Demo](https://increaser.org)

# Introduction to Magic Link Email Authentication

In this post, I will demonstrate a basic implementation of magic link email authentication for NextJS + NodeJS applications. This authentication method is convenient and efficient, simplifying the process for both users and developers by eliminating the need to deal with passwords. While this explanation covers both front-end and back-end basics, remember that this is a simplified solution. If your application manages sensitive information or financial transactions, you would find it prudent to either enhance the security measures detailed here, or employ a third-party service. Our discussion relies on the [Increaser](https://increaser.org) codebase. It's kept in a private repository, but rest assured; all the necessary code snippets will be included directly in this blog post. Furthermore, you can find all the reusable hooks, components, and utility tools in the [ReactKit](https://github.com/radzionc/reactkit) repository.

![Sign up page at Increaser](https://cdn-images-1.medium.com/proxy/1*aMzCsm_RYGmVG4HzBfNtrA.jpeg)

## Front-End Implementation: Setup and Data Collection

Let's begin with the front-end. I use the `AuthProviders` component on both sign-in and sign-up pages, which displays Facebook and Google authentication options as well as an `EmailAuthForm`. You can learn more about our OAuth implementation in my other [post](https://radzion.com/blog/oauth).

```tsx
import { VStack } from "@increaser/ui/ui/Stack"

import { OptionsDivider } from "../OptionsDivider"
import { EmailAuthForm } from "./EmailAuthForm"
import { OAuthOptions } from "../OAuthOptions"

export const AuthProviders = () => {
  return (
    <VStack gap={20}>
      <OAuthOptions />
      <OptionsDivider />
      <EmailAuthForm />
    </VStack>
  )
}
```

The `EmailAuthForm` component deploys `react-hook-form` to manage the form, although this can also be achieved without use of library, given the simplicity of having only an email address input. Upon submission, we employ the `useMutation` hook from `react-query` to forward email addresses to the GraphQL API. If you're interested in an efficient way to generate and utilize types for your GraphQL projects, feel free to check out another [post](https://radzion.com/blog/gql).

```tsx
import { analytics } from "analytics"
import { useForm } from "react-hook-form"
import { useMutation } from "react-query"
import { Button } from "@increaser/ui/ui/buttons/Button"
import { Form } from "@increaser/ui/ui/Form/Form"
import { TextInput } from "@increaser/ui/ui/inputs/TextInput"

import { useRouter } from "next/router"
import { Path } from "router/Path"
import { validateEmail } from "@increaser/utils/validation/validateEmail"
import { addQueryParams } from "@increaser/utils/addQueryParams"
import { useApi } from "api/useApi"
import { graphql } from "@increaser/api-interface/client"

interface EmailFormState {
  email: string
}

const sendAuthLinkByEmailMutationDocument = graphql(`
  mutation sendAuthLinkByEmail($input: SendAuthLinkByEmailInput!) {
    sendAuthLinkByEmail(input: $input)
  }
`)

export const EmailAuthForm = () => {
  const { query } = useApi()

  const { push } = useRouter()

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<EmailFormState>({
    mode: "onSubmit",
  })

  const { mutate: sendAuthLinkByEmail, isLoading } = useMutation(
    async ({ email }: EmailFormState) => {
      analytics.trackEvent("Start identification with email")

      await query(sendAuthLinkByEmailMutationDocument, {
        input: {
          email,
        },
      })

      return email
    },
    {
      onSuccess: (email) => {
        push(addQueryParams(Path.EmailConfirm, { email }))
      },
    }
  )

  return (
    <Form
      gap={4}
      onSubmit={handleSubmit((data) => {
        sendAuthLinkByEmail(data)
      })}
      content={
        <TextInput
          label="Email address"
          type="email"
          autoFocus
          placeholder="john@gmail.com"
          {...register("email", {
            required: "Please enter your email",
            validate: validateEmail,
          })}
          error={errors.email?.message}
        />
      }
      actions={
        <Button size="l" isLoading={isLoading}>
          Continue
        </Button>
      }
    />
  )
}
```

## Front-End Implementation: Email Confirmation Page

Before sending the request, we also trigger an analytics event for performance monitoring of our email authentication funnel. Thus, at Increaser, we've determined that email authentication has almost same conversion rate as OAuth options, but more people choose to log in via Google compared to email.

![Analytics funnel at Increaser](https://cdn-images-1.medium.com/proxy/1*Ypwx_ZbE5JBuD21U5qEZBA.jpeg)

After a successful request, we'll direct the user to the email confirmation page, which resides under the `/email-confirm` route at [Increaser](https://increaser.org).

```tsx
import { makeAuthPage } from "layout/makeAuthPage"
import { ConfirmEmailAuthView } from "@increaser/ui/auth/ConfirmEmailAuthView"

export default makeAuthPage(() => <ConfirmEmailAuthView sender="increaser" />)
```

![Email confirmation page at Increaser](https://cdn-images-1.medium.com/proxy/1*6Vw2avTMqgjTZucGHrSYTw.jpeg)

Within the `ConfirmEmailAuthView` component, we retrieve the email from the query parameters using the `useHandleQueryParams` hook from [ReactKit](https://github.com/radzionc/reactkit). Here, we prompt the user to check their email inbox and provide a return button in case they wish to amend their email address or try other login options.

```tsx
import { VStack } from "../ui/Stack"
import { Text } from "../ui/Text"
import { useState } from "react"
import { useRouter } from "next/router"
import { AuthView } from "./AuthView"
import { useHandleQueryParams } from "../navigation/hooks/useHandleQueryParams"
import { suggestInboxLink } from "@increaser/utils/suggestInboxLink"
import { ExternalLink } from "../navigation/Link/ExternalLink"
import { Button } from "../ui/buttons/Button"

interface EmailConfirmQueryParams {
  email: string
}

interface ConfirmEmailAuthViewProps {
  sender?: string
}

export const ConfirmEmailAuthView = ({ sender }: ConfirmEmailAuthViewProps) => {
  const [email, setEmail] = useState<string | undefined>()
  useHandleQueryParams<EmailConfirmQueryParams>(({ email }) => setEmail(email))

  const { back } = useRouter()

  const inboxLink = email && suggestInboxLink(email, sender)

  return (
    <AuthView title="Confirm your email">
      {email && (
        <>
          <Text height="large" centered size={18}>
            We emailed a magic link to <br />
            <Text as="span" weight="bold">
              {email}
            </Text>
            <br />
            Click the link to log in or sign up.
          </Text>

          <VStack gap={4} fullWidth>
            {inboxLink && (
              <ExternalLink style={{ width: "100%" }} to={inboxLink}>
                <Button size="xl" as="div">
                  Check your inbox
                </Button>
              </ExternalLink>
            )}
            <Button onClick={back} size="xl" kind="ghostSecondary">
              Back
            </Button>
          </VStack>
        </>
      )}
    </AuthView>
  )
}
```

## Front-end Interaction: Email Authentication Flow

We implement the `suggestInboxLink` function to infer the email client in use.

```tsx
import { isOneOf } from "./array/isOneOf"
import { extractEmailProvider } from "./extractEmailProvider"
import { match } from "./match"

const emailProvidersWithClient = [
  "gmail",
  "outlook",
  "hotmail",
  "live",
  "yahoo",
  "protonmail",
  "aol",
  "zoho",
] as const

const outlookInboxLink = "https://outlook.live.com/mail/0/inbox"

export const suggestInboxLink = (
  email: string,
  sender?: string
): string | undefined => {
  const emailProvider = extractEmailProvider(email)
  if (!emailProvider) return undefined

  const emailProviderWithClient = isOneOf(
    emailProvidersWithClient,
    emailProvider
  )
  if (!emailProviderWithClient) return undefined

  return match(emailProviderWithClient, {
    gmail: () => {
      const url = "https://mail.google.com/mail/u/0/#search"
      if (!sender) return url

      const searchStr = encodeURIComponent(`from:@${sender}+in:anywhere`)
      return [url, searchStr].join("/")
    },
    outlook: () => outlookInboxLink,
    hotmail: () => outlookInboxLink,
    live: () => outlookInboxLink,
    yahoo: () => "https://mail.yahoo.com/d/folders/1",
    protonmail: () => "https://mail.protonmail.com/u/0/inbox",
    aol: () => "https://mail.aol.com/webmail-std/en-us/inbox",
    zoho: () => "https://mail.zoho.com/zm/#mail/folder/inbox",
  })
}
```

Initially, we extract the provider's name from the email using a basic regex expression.

```tsx
export const extractEmailProvider = (email: string): string | undefined => {
  const regex = /(?<=@)\w+/

  const match = email.match(regex)
  return match ? match[0] : undefined
}
```

Then, we cross-reference the provider against a list of those that offer a web client, returning a link to the inbox if a match is found. For Gmail, we include a search query to highlight emails from the sender. Instead of a switch-case statement, we use the `match` function from [ReactKit](https://github.com/radzionc/reactkit).

## Back-End Implementation: Managing Email Authentication Process

At this point, the user should receive an email containing a link to the `/email-auth` page, complete with a special authentication code. We'll delve into email distribution shortly; for now, let's examine the final phase of authentication flow on the front-end.

```tsx
import { useHandleQueryParams } from "navigation/hooks/useHandleQueryParams"
import { useCallback } from "react"
import { AuthView } from "./AuthView"
import { getCurrentTimezoneOffset } from "@increaser/utils/time/getCurrentTimezoneOffset"
import { AuthConfirmationStatus } from "./AuthConfirmationStatus"
import { QueryApiError } from "api/useApi"
import { useAuthenticateWithEmailMutation } from "auth/hooks/useAuthenticateWithEmailMutation"

interface EmailAuthParams {
  code: string
}

export const EmailAuthContent = () => {
  const { mutate: identify, error } = useAuthenticateWithEmailMutation()

  useHandleQueryParams<EmailAuthParams>(
    useCallback(
      ({ code }) => {
        identify({
          code,
          timeZone: getCurrentTimezoneOffset(),
        })
      },
      [identify]
    )
  )

  return (
    <AuthView title={`Continue with email`}>
      <AuthConfirmationStatus error={error as QueryApiError | undefined} />
    </AuthView>
  )
}
```

The `EmailAuthContent` manages the query parameter through the same `useHandleQueryParams` hook. However, this time we'll call the `useAuthenticateWithEmailMutation` hook to dispatch the code to the GraphQL API. Here, the user is shown either a spinner (whilst the request is still being processed) or an error message (if something goes wrong). A successful request prompts an immediate redirection for the user to the home page. To display the status, we render the `AuthConfirmationStatus` component, which is also reused in the OAuth flow. We also provide a prompt to contact support in cases where the user encounters an error.

```tsx
import { CopyText } from "@increaser/ui/ui/CopyText"
import { Spinner } from "@increaser/ui/ui/Spinner"
import { VStack } from "@increaser/ui/ui/Stack"
import { Text } from "@increaser/ui/ui/Text"
import { Button } from "@increaser/ui/ui/buttons/Button"
import { InfoIcon } from "@increaser/ui/ui/icons/InfoIcon"
import { QueryApiError } from "api/useApi"
import Link from "next/link"
import { Path } from "router/Path"
import { AUTHOR_EMAIL } from "shared/externalResources"

interface AuthConfirmationStatusProps {
  error?: QueryApiError
}

export const AuthConfirmationStatus = ({
  error,
}: AuthConfirmationStatusProps) => {
  return (
    <VStack alignItems="center" gap={20}>
      <Text
        style={{ display: "flex" }}
        color={error ? "alert" : "regular"}
        size={80}
      >
        {error ? <InfoIcon /> : <Spinner />}
      </Text>
      {error ? (
        <>
          <Text style={{ wordBreak: "break-word" }} centered height="large">
            {error.message}
          </Text>
          <Link style={{ width: "100%" }} href={Path.SignIn}>
            <Button kind="secondary" style={{ width: "100%" }} size="l">
              Go back
            </Button>
          </Link>
          <Text centered color="supporting" size={14}>
            Nothing helps? Email us at <br />
            <CopyText color="regular" as="span" content={AUTHOR_EMAIL}>
              {AUTHOR_EMAIL}
            </CopyText>
          </Text>
        </>
      ) : null}
    </VStack>
  )
}
```

Within the `useAuthenticateWithEmailMutation` hook, we use the `useApi` hook that abstracts server interactions to send a request to our GraphQL API. Upon successful request, we'll update the state with the JWT token that we will use to authenticate the user on the front-end.

```tsx
import { graphql } from "@increaser/api-interface/client"
import { useMutation } from "react-query"

import { useApi } from "api/useApi"
import { AuthSessionWithEmailInput } from "@increaser/api-interface/client/graphql"
import { useAuthSession } from "./useAuthSession"

const authSessionWithEmailDocument = graphql(`
  query authSessionWithEmail($input: AuthSessionWithEmailInput!) {
    authSessionWithEmail(input: $input) {
      token
      expiresAt
      isFirst
    }
  }
`)

export const useAuthenticateWithEmailMutation = () => {
  const { query } = useApi()

  const [, updateSession] = useAuthSession()

  return useMutation(async (input: AuthSessionWithEmailInput) => {
    const { authSessionWithEmail } = await query(authSessionWithEmailDocument, {
      input,
    })

    updateSession(authSessionWithEmail)
  })
}
```

## Back-End Implementation: Generating and Sending Auth Link

We store the JWT token in the local storage, after which the `useApi` hook will attach the token to every request to the GraphQL API. The use of the `useAuthSession` wraps around the `usePersistentState` hook, which is expounded upon in my other [post](https://radzion.com/blog/storage). This stage also involves sending analytics events to monitor authentication flow, as this hook is the sole process by which we modify session state.

```tsx
import { AuthSession } from "@increaser/api-interface/client/graphql"
import { analytics } from "analytics"
import { useCallback } from "react"
import { useQueryClient } from "react-query"
import { PersistentStateKey, usePersistentState } from "state/persistentState"

export const useAuthSession = () => {
  const queryClient = useQueryClient()

  const [session, setSession] = usePersistentState<AuthSession | undefined>(
    PersistentStateKey.AuthSession,
    undefined
  )

  const onChange = useCallback(
    (session: AuthSession | undefined) => {
      if (session) {
        analytics.trackEvent("Finish identification")

        if (session.isFirst) {
          analytics.trackEvent("Finish Sign Up")
        }
      } else {
        queryClient.clear()
      }

      setSession(session)
    },
    [queryClient, setSession]
  )

  return [session, onChange] as const
}
```

An essential point of observation is that I disable React's strict mode; this prevents the component from re-rendering during local usage of the app and causing a second API request. This is achieved by adding `reactStrictMode: false` to the `next.config.js` file. If you know of a method that prevents a second request while maintaining React's strict mode, please share it.

```js
const nextConfig = {
  reactStrictMode: false,
  // ...
}
```

From our front-end interaction with the API, it's clear that there must be two GraphQL endpoints managing the email authentication process. The `sendAuthLinkByEmail` mutation sends an email containing a unique code to the user, while the `authSessionWithEmail` query authenticates the user and returns a session - this includes a JWT token, expiry date, and a flag signifying if it's the user's first time logging in. In the `AuthSessionWithEmailInput`, we include a `timeZone` parameter. This is specific to [Increaser](https://increaser.org), and may not be necessary for your application.

```graphql
type AuthSession {
  token: String!
  expiresAt: Int!
  isFirst: Boolean
}

input SendAuthLinkByEmailInput {
  email: String!
}

input AuthSessionWithEmailInput {
  code: String!
  timeZone: Int!
}

type Query {
  authSessionWithEmail(input: AuthSessionWithEmailInput!): AuthSession!
}

type Mutation {
  sendAuthLinkByEmail(input: SendAuthLinkByEmailInput!): Boolean
}
```

Next, let's explore the implementation of the `sendAuthLinkByEmail` mutation. We generate a JWT token for authentication, construct a login URL (which leads to the `/email-auth` page on our front-end with an attached auth code as a query parameter), and finally send the user an email comprising this login URL.

```ts
import { addQueryParams } from "@increaser/utils/addQueryParams"
import { MutationResolvers } from "../../gql/schema"
import { generateAuthLinkToken } from "../helpers/generateAuthLinkToken"
import { assertEnvVar } from "../../shared/assertEnvVar"
import { sendLoginLinkEmail } from "@increaser/email/utils/sendLogInLinkEmail"

export const sendAuthLinkByEmail: MutationResolvers["sendAuthLinkByEmail"] =
  async (_: any, { input: { email } }): Promise<boolean> => {
    const code = await generateAuthLinkToken(email)
    const loginUrl = addQueryParams(`${assertEnvVar("APP_URL")}/email-auth`, {
      code,
    })

    await sendLoginLinkEmail({
      loginUrl,
      email,
    })

    return true
  }
```

Creating the code involves the `jsonwebtoken` library. The token is signed with a secret coming from the `getSecret` function, which varies based on your infrastructure setup. Maybe you can safely use environment variables, or if you operate within AWS - Secrets Manager (which I've discussed in this [post](https://radzion.com/blog/secrets)) could be a good option. The code expires in 20 minutes. We employ the `convertDuration` function from [ReactKit](https://github.com/radzionc/reactkit) to translate this duration from minutes to seconds.

```tsx
import jwt from "jsonwebtoken"
import { getTokenExpirationTime } from "./getTokenExpirationTime"
import { getSecret } from "../../utils/getSecret"
import { convertDuration } from "@increaser/utils/time/convertDuration"

export const generateAuthLinkToken = async (email: string) =>
  jwt.sign(
    {
      email,
      exp: getTokenExpirationTime(convertDuration(20, "min", "s")),
    },
    await getSecret("EMAIL_SECRET")
  )
```

## Managing Email Packages and Delivery

I keep my packages in a monorepo where the `sendLoginLinkEmail` function exists within the `email` package. Here, we use the `react-email` library to render the email templates and the `sendEmail` function to dispatch the emails. My first experience with `react-email` has been very positive. Prior to this, I was using a simple HTML string template that I would send to myself to verify that everything looked as it should. However, `react-email` allows email previewing in the browser and a more comfortable interaction with React as opposed to the novel syntax of other libraries like MJML.

```ts
import { render } from "@react-email/render"
import { sendEmail } from "./sendEmail"
import { productName } from "@increaser/entities"
import { getEnvVar } from "./getEnvVar"
import LoginLinkEmail, { LoginLinkEmailProps } from "../emails/LoginLinkEmail"

export const sendLoginLinkEmail = async ({
  loginUrl,
  email,
}: LoginLinkEmailProps) => {
  const body = render(<LoginLinkEmail loginUrl={loginUrl} email={email} />, {
    pretty: true,
  })

  return sendEmail({
    email,
    body,
    subject: `Log in to ${productName}`,
    source: `Log in <noreply@${getEnvVar("EMAIL_DOMAIN")}>`,
  })
}
```

The `LoginLinkEmail` component generates a simple email, featuring an app logo, heading, text detailing the link's validity duration, a login button, and the recipient email address. Notably, we're able to import a theme from the `ui` package to color buttons and text. The `react-email` `Font` component is used to load the same font as we use on the front-end.

```tsx
import { lightTheme } from "@increaser/ui/ui/theme/lightTheme"
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Img,
  Preview,
  Section,
  Text,
  Font,
} from "@react-email/components"

export interface LoginLinkEmailProps {
  loginUrl: string
  email: string
}

export const LoginLinkEmail = ({
  loginUrl,
  email = "john@gmail.com",
}: LoginLinkEmailProps) => (
  <Html>
    <Head>
      <Font
        fontFamily="OpenSans"
        fallbackFontFamily="Verdana"
        webFont={{
          url: "https://fonts.gstatic.com/s/opensans/v36/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-mu0SC55I.woff2",
          format: "woff2",
        }}
        fontWeight={400}
        fontStyle="normal"
      />
      <Font
        fontFamily="OpenSans"
        fallbackFontFamily="Verdana"
        webFont={{
          url: "https://fonts.gstatic.com/s/opensans/v36/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1x4gaVIUwaEQbjA.woff2",
          format: "woff2",
        }}
        fontWeight={700}
        fontStyle="normal"
      />
    </Head>
    <Preview>Log in to Increaser</Preview>
    <Body style={main}>
      <Container style={container}>
        <Img
          src={`https://increaser.org/images/logo.png`}
          width="68"
          height="68"
          alt="Increaser"
        />
        <Heading style={heading}>Increaser</Heading>
        <Text>
          Click the button below to log in to <b>Increaser</b>.
          <br />
          This button will expire in 20 minutes.
        </Text>
        <Section style={buttonContainer}>
          <Button pY={20} pX={20} style={button} href={loginUrl}>
            Log in to Increaser
          </Button>
        </Section>
        <Text>
          Confirming this request will securely log you in using{" "}
          <a style={userEmail}>{email}</a>.
        </Text>
        <Text>- Increaser Team</Text>
      </Container>
    </Body>
  </Html>
)

export default LoginLinkEmail

const main = {
  backgroundColor: lightTheme.colors.background.toCssValue(),
  color: lightTheme.colors.contrast.toCssValue(),
  fontFamily:
    'OpenSans,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
}

const container = {
  margin: "0 auto",
  padding: "20px 0 48px",
  width: "500px",
}

const heading = {
  fontSize: "32px",
  fontWeight: "700",
  lineHeight: "1.3",
}

const button = {
  backgroundColor: lightTheme.colors.primary.toCssValue(),
  borderRadius: "8px",
  fontWeight: "700",
  color: lightTheme.colors.background.toCssValue(),
  fontSize: "18px",
  textDecoration: "none",
  textAlign: "center" as const,
  display: "block",
}

const buttonContainer = {
  padding: "8px 0 8px",
}

const userEmail = {
  textDecoration: "none",
  color: lightTheme.colors.primary.toCssValue(),
  fontWeight: 700,
}
```

The `render` function simply converts the React component to an HTML string. At Increaser I use AWS SES to send emails, a cost-effective solution that suits my volume. The problematic aspect is that it can't accommodate non-operational emails, as this can quickly lead to crossing the complaint threshold - I'm on the boundary of this while only sending transactional emails without any promotional content. In future projects, I plan to try another provider. The advantage of the monorepo approach is that it’s easy to replace the implementation of the `sendEmail` function in the `email` package while the other packages continue to function with the new implementation. However, updating environment variables in the packages that would utilize the new provider might be a bit cumbersome.

```tsx
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"
import { getEnvVar } from "./getEnvVar"

interface SendEmailParameters {
  email: string
  body: string
  subject: string
  source: string
}

const client = new SESv2Client({ region: getEnvVar("SES_AWS_REGION") })

export const sendEmail = ({
  email,
  body,
  subject,
  source,
}: SendEmailParameters) => {
  const command = new SendEmailCommand({
    Destination: {
      ToAddresses: [email],
    },
    Content: {
      Simple: {
        Body: {
          Html: {
            Data: body,
          },
        },
        Subject: {
          Data: subject,
        },
      },
    },
    FromEmailAddress: source,
  })

  return client.send(command)
}
```

## Last Step: User Authentication and Validation

In the `sendEmail` function, we generate a `SendEmailCommand` which is sent via the `SESv2Client`. I depend on the `getEnvVar` function to access environment variables; it houses a type variable name parameter and throws an error if the specified variable is undefined.

```ts
type VariableName = "SES_AWS_REGION" | "EMAIL_DOMAIN"

export const getEnvVar = (name: VariableName): string => {
  const value = process.env[name]
  if (!value) {
    throw new Error(`Missing ${name} environment variable`)
  }

  return value
}
```

The last step is to authenticate with the `authSessionWithEmail` query. Initial validation is carried out before returning a session.

```ts
import { OperationContext } from "../../gql/OperationContext"
import { QueryResolvers } from "../../gql/schema"
import { authenticateWithEmail } from "../utils/authenticateWithEmail"
import { authorize } from "../utils/authorize"

export const authSessionWithEmail: QueryResolvers<OperationContext>["authSessionWithEmail"] =
  async (_, { input: { code, timeZone } }, { country }) => {
    const result = await authenticateWithEmail({
      code,
    })

    return authorize({
      timeZone,
      country,
      ...result,
    })
  }
```

The `authenticateWithEmail` function verifies the JWT token and extracts the email from its payload.

```ts
import { getSecret } from "../../utils/getSecret"
import { AuthenticationResult } from "./AuthenticationResult"
import jwt from "jsonwebtoken"

interface AuthenticateWithEmailParams {
  code: string
}

interface EmailCodePayload {
  email: string
}

export const authenticateWithEmail = async ({
  code,
}: AuthenticateWithEmailParams): Promise<AuthenticationResult> => {
  const secret = await getSecret("EMAIL_SECRET")
  const { email } = jwt.verify(code, secret) as EmailCodePayload

  return {
    email,
  }
}
```

The `authorize` function checks if the user associated with the given email exists in the database, creating one if necessary.

```ts
import { getUserByEmail, putUser } from "@increaser/db/user"
import { AuthSession } from "../../gql/schema"
import { AuthenticationResult } from "./AuthenticationResult"
import { getAuthSession } from "./getAuthSession"
import { getUserInitialFields } from "@increaser/entities-utils/user/getUserInitialFields"

interface AuthorizeParams extends AuthenticationResult {
  timeZone: number
  country?: string
}

export const authorize = async ({
  email,
  name,
  country,
  timeZone,
}: AuthorizeParams): Promise<AuthSession> => {
  const existingUser = await getUserByEmail(email, ["id"])
  if (existingUser) {
    return getAuthSession(existingUser.id)
  }

  const newUser = getUserInitialFields({
    email,
    name,
    country,
    timeZone,
  })

  await putUser(newUser)

  return {
    ...getAuthSession(newUser.id),
    isFirst: true,
  }
}
```

It then returns a session with a JWT token, expiration date, and a flag denoting if the user is logging in for the first time using the `getAuthSession` function.

```ts
import { convertDuration } from "@increaser/utils/time/convertDuration"
import { AuthSession } from "../../gql/schema"
import jwt from "jsonwebtoken"
import { getSecret } from "../../utils/getSecret"

const tokenLifespanInDays = 300

export const getAuthSession = async (id: string): Promise<AuthSession> => {
  const expiresAt = Math.round(
    convertDuration(Date.now(), "ms", "s") +
      convertDuration(tokenLifespanInDays, "d", "s")
  )
  const secret = await getSecret("SECRET")
  const token = jwt.sign({ id, exp: expiresAt }, secret)

  return {
    token,
    expiresAt,
  }
}
```