Email Link Authentication with Firebase and Next.js

Learn how to add Email Link authentication to your SaaS application with Firebase Auth and Next.js

·4 min read
Cover Image for Email Link Authentication with Firebase and Next.js

While being a less-known authentication method, Email Links are a great way to allow users to sign in to your application. It's easy and simple to use, and Firebase offers full support for it.

In this post, we will learn how to implement Email Link authentication in any Next.js application with Firebase Auth.

In this post, we assume you already have a bare-bone Next.js codebase with reactfire installed and the basic Firebase setup.

Some of the code below contains references to components and utilities contained in the Makerkit SaaS Boilerplate

First, we want to create a form in our authentication pages that can read an email address and generate an email link.

Here is what we will do:

  1. Create a simple form that takes an email address
  2. When submitted, we generate the link using the sendSignInLinkToEmail function from firebase/auth, and save the email in localStorage
EmailLinkAuth.tsx
const EmailLinkAuth: React.FC = () => {
  const auth = useAuth();
  const { state, setLoading, setData, setError } = useRequestState<void>();
 
  const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
    async (event) => {
      event.preventDefault();
 
      // read the email field of the form
      const target = event.currentTarget;
      const data = new FormData(target);
      const email = data.get('email') as string;
 
      setLoading(true);
 
      // set up return URL (where we will redirect the user)
      const settings = {
        url: getAuthUrl(),
        handleCodeInApp: true,
      };
 
      try {
        // send sign in link
        await sendSignInLinkToEmail(
          auth, 
          email, 
          settings
        );
 
        // save email in storage, so we can compare 
        // it when the user uses the link from the email
        storeEmailInStorage(email);
 
        // success (turns state.success to "true")
        setData();
      } catch (error) {
         setError(error);
      }
    },
    [auth, setData, setError, setLoading]
  );
  
  { /* SUCCESS! */ }
  if (state.success) {
    return (
      <span>
        Yay, link successfully sent!
      </span>
    );
  }
 
  return (
    <form className={'w-full'} onSubmit={onSubmit}>
      <div className={'flex flex-col space-y-2'}>
        <TextField>
          <TextField.Label>
            Email Address
 
            <TextField.Input
              data-cy={'email-input'}
              required
              type="email"
              placeholder={'your@email.com'}
              name={'email'}
            />
          </TextField.Label>
        </TextField>
 
        <Button loading={state.loading}>
          <If
            condition={state.loading}
            fallback={<>Send Email Link</>}
          >
            Sending Email Link...
          </If>
        </Button>
      </div>
 
      <If condition={state.error}>
        <span>
          Sorry, we encountered an error
        </span>
      </If>
    </form>
  );
};
 
function getAuthUrl() {
  const origin = window.location.origin;
  const path = '/auth/link';
 
  return [origin, path].join('');
}
 
function storeEmailInStorage(email: string) {
  window.localStorage.setItem('emailForSignIn', email);
}
 
export default EmailLinkAuth;

When using the Firebase Auth emulator, the link will be printed to the console that is running the emulators.

Now, we have to create the /auth/link page component, in which we will verify the code and, if successful, sign the user in.

As said before, we create this page at /auth/link.

Before being able to sign the user in, we're going to do a couple of checks:

  1. first, we use the Firebase function isSignInWithEmailLink to verify we're using the Email Link authentication method
  2. secondly, we verify we're able to retrieve the email address used to receive the link
  3. then, we sign the user in using the function signInWithEmailLink
  4. if successful, we delete the email from local storage and redirect the user to the application
  5. whenever we cannot sign the user in, we display the users an error
const EmailLinkAuthPage: React.FC = () => {
  const auth = useAuth();
  const router = useRouter();
  const requestExecutedRef = useRef<boolean>();
  const { state, setError } = useRequestState<void>();
 
  const redirectToAppHome = useCallback(() => {
    return router.push(appHome);
  }, [router]);
 
  // in this effect, we execute the functions to log the user in
  useEffect(() => {
    // let's prevent duplicate calls (which should only happen in dev mode)
    if (requestExecutedRef.current) {
      return;
    }
 
    const href = getOriginHref();
 
    // do not run on the server
    if (!href) {
      setError('generic');
 
      return;
    }
 
    // let's verify the auth method is email link
    if (!isSignInWithEmailLink(auth, href)) {
      setError('generic');
 
      return;
    }
 
    const email = getStorageEmail();
 
    // let's get email used to get the link
    if (!email) {
      setError('generic');
 
      return;
    }
 
    void (async () => {
      requestExecutedRef.current = true;
 
      try {
        // sign in with link, and retrieve the ID Token
        await signInWithEmailLink(auth, email, href);
 
        // let's clear the email from the storage
        clearEmailFromStorage();
 
        // redirect user to the home page
        await redirectToAppHome();
      } catch (e) {
        if (e instanceof FirebaseError) {
          setError(e.code);
        } else {
          setError('generic');
        }
      }
    })();
  }, [
    auth,
    loading,
    redirectToAppHome,
    setError,
  ]);
 
  return (
    <Layout>
      <If condition={loading}>
        Signing in...
      </If>
 
      <If condition={state.error}>
        <div className={'flex flex-col space-y-2'}>
          { state.error }
        </div>
      </If>
    </Layout>
  );
};
 
function getStorageEmail() {
  if (!isBrowser()) {
    return;
  }
 
  return window.localStorage.getItem(EMAIL_LOCAL_STORAGE_KEY);
}
 
function clearEmailFromStorage() {
  window.localStorage.removeItem(EMAIL_LOCAL_STORAGE_KEY);
}
 
function getOriginHref() {
  if (!isBrowser()) {
    return;
  }
 
  return window.location.href;
}
 
export default EmailLinkAuthPage;

🎉 And that's it! This is all you need to create a sound email authentication flow with Firebase Auth and Next.js.


Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS

Read more about Tutorials

Cover Image for Next.js 13: complete guide to Server Components and the App Directory

Next.js 13: complete guide to Server Components and the App Directory

·14 min read
Unlock the full potential of Next.js 13 with our most complete and definitive tutorial on using server components and the app directory.
Cover Image for Pagination with React.js and Supabase

Pagination with React.js and Supabase

·6 min read
Discover the best practices for paginating data using Supabase and React.js using the Supabase Postgres client
Cover Image for How to sell code with Lemon Squeezy and Github

How to sell code with Lemon Squeezy and Github

·7 min read
Sell and monetize your code by giving private access to your Github repositories using Lemon Squeezy
Cover Image for Writing clean React

Writing clean React

·9 min read
Level up your React coding skills with Typescript using our comprehensive guide on writing clean code. Start writing clean React code, today.
Cover Image for How to use MeiliSearch with React

How to use MeiliSearch with React

·12 min read
Learn how to use MeiliSearch in your React application with this guide. We will use Meiliseach to add a search engine for our blog posts
Cover Image for Setting environment variables in Remix

Setting environment variables in Remix

·3 min read
Learn how to set environment variables in Remix and how to ensure that they are available in the client-side code.