Overview
In this tutorial, we are going to explore using NextAuth and Fauna to set up passwordless authentication in a Next.js application. Each user will get a smooth and secure login experience using mailed magic links. In other words, users will be able to login into the application only using their email address (no password needed).
The final version of the project's code can be found on Github. You can use it as a starting point for any Next.js app that requires passwordless authentication.
You can also preview the example live here.
This tutorial will cover:
- Configuring Next.js, NextAuth, and Fauna to work together seamlessly
- Using Next.js dynamic API routes to handle authentication requests
- Using Fauna and the Fauna Adapter for next-auth to persist users, email sign in tokens, and sessions
- Creating custom login and confirmation pages with React + Tailwind CSS
- Customizing the sign-in email and sending a welcome email to new users
There is a lot to cover, so let's get started!
Prerequisites
You will need Node.js version 12.13 or later installed to follow along.
In addition, make sure to create an account on Fauna.com before getting started. As of this writing, it is free and doesn't require a credit card.
Finally, you'll need an email server up and running to send the verification and welcome emails to your users. So make sure to have your email server settings (host, port, user, password) in hand as well.
Create a Next.js app
The first thing we need to do is create a new Next.js application. The simplest
way to do it is by using the tool called create-next-app
, which bootstraps a
Next.js app for you without the hassle of configuring everything yourself.
So, open your terminal, and run the following command:
npx create-next-app magic-next-auth
Note that magic-next-auth
is the name of the project's directory I've
chosen. Of course, you can use any other name you'd like.
You can now cd
into your project directory:
cd magic-next-auth/
And run the development server with the following command:
npm run dev
If everything is working fine, your Next.js development server should be running on port 3000. Open http://localhost:3000/ from your browser to check this out.
Install and configure Tailwind CSS
As we will create custom sign-in and confirmation pages (more on this later), we'll need a way of styling those pages. To do that, we'll use the great Tailwind CSS framework to save us some time and energy.
So, start by installing Tailwind and the following dependencies using npm
:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
Note that here, in addition to Tailwind, we are installing postcss
and
autoprefixer
.
PostCSS is a well-known CSS preprocessor that uses JS plugins for transforming styles. In our case, we'll configure Tailwind as a PostCSS plugin to generate the corresponding CSS from the utility classes inside our markup.
And since Tailwind does not automatically add vendor prefixes to the CSS it generates, we are also installing Autoprefixer to handle this for us. It is one of the most popular PostCSS plugins.
Once you have everything installed, generate your tailwind.config.js
and
postcss.config.js
files with:
npx tailwindcss init -p
This will create a minimal Tailwind config file at the root of your project:
module.exports = { purge: [], darkMode: false, // or 'media' or 'class' theme: { extend: {}, }, variants: { extend: {}, }, plugins: [], };
It will also create the PostCSS config file already configured with
tailwindcss
and autoprefixer
as plugins:
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, };
Next, enable Tailwind
Just-in-Time mode and
configure the purge
option from your tailwind.config.js
file:
module.exports = { mode: 'jit', purge: ['./pages/**/*.{js,ts,jsx,tsx}'], // ... }
Finally, make sure to include the Tailwind CSS directives in your CSS. So, open
the ./styles/globals.css
file and replace the original file content with:
@tailwind base; @tailwind components; @tailwind utilities;
You can also delete any other CSS file that Next.js generated for you by
default, such as the styles/Home.module.css
file, for instance. Make sure to
remove any dependencies for this file in your project, like in the
pages/index.js
.
Alright! We are done with installing and configuring Tailwind CSS in our Next.js application.
Add authentication with NextAuth + Fauna
1. Set up NextAuth
To add authentication to our Next.js app, we will use NextAuth.js, a full-featured authentication system with built-in providers such as Google, Facebook, or Github. It also supports JWT, email/password, and magic links authentication.
So, first, install next-auth
using npm
.
npm install --save next-auth
Next, from within your _app.js
file, import the Provider
from
next-auth/react
, wrap everything inside this provider, and set the session
prop using pageProps.session
to allow the session state to be shared between
pages.
import { SessionProvider as AuthProvider } from 'next-auth/react'; // ... function MyApp{ Component, pageProps: { session, ...pageProps } }) { return ( <AuthProvider session={session}> <Component {...pageProps} /> </AuthProvider> ); } export default MyApp;
Then, create a new
Next.js dynamic API route by
creating a file in pages/api/auth
named [...nextauth].js
.
Since next-auth.js
v4, you also need to install nodemailer
to send emails,
as it is no longer included as a dependency by default. So, make sure to install
it with the following command:
npm i nodemailer
Then inside the [...nextauth].js
file, import NextAuth
, the EmailProvider
from next-auth/providers/email
, and nodemailer
.
import NextAuth from 'next-auth'; import EmailProvider from 'next-auth/providers/email'; import nodemailer from 'nodemailer';
Next, call and export as default NextAuth to automatically create and handle the API routes for authentication.
export default NextAuth();
From within NextAuth, you can then pass an object to configure one or more authentication providers. Here, we are just going to use EmailProvider which uses email to send "magic links" to sign in users seamlessly with no password.
For a full list of all the supported authentication providers, check out the official documentation.
export default NextAuth({ providers: [ EmailProvider({ server: { host: process.env.EMAIL_SERVER_HOST, port: process.env.EMAIL_SERVER_PORT, auth: { user: process.env.EMAIL_SERVER_USER, pass: process.env.EMAIL_SERVER_PASSWORD, }, }, from: process.env.EMAIL_FROM, maxAge: 10 * 60, // Magic links are valid for 10 min only }), ], });
As you can see, I've set the magic link in emails to only be valid for the next 10 minutes. You can use any other value (in seconds) you'd like. And if you don't set a value, the default is one day.
Also, I've used environment variables to configure the email provider. So,
create a new file named .env.local
at the root of your project's directory and
add the following variables:
EMAIL_SERVER_HOST=smtp.example.com.com EMAIL_SERVER_PORT=465 EMAIL_SERVER_USER=<YOUR_SMTP_USER> EMAIL_SERVER_PASSWORD=<YOUR_SMTP_PASSWORD> EMAIL_FROM=no-reply@example.com
Make sure to replace the values with your own from your email server settings.
You'll also need to restart your development server to load those new environment variables.
2. Set up Fauna
The NextAuth email provider requires a database to persist users, email sign-in tokens, and sessions. So, let's create a new Fauna database and use it with NextAuth to store those information.
After creating an account on Fauna, go to your dashboard, and click Create database.
Set the database name, select the region group, and leave the checkbox "Use demo data" unchecked as we don't want to populate our database with demo data.
Click "Create".
Then, from within your database's page, click on the "Security" tab, and click "New key" to create a new secret key you would use to get access to the Fauna API.
From here, select your newly created database, set the role to "Server", and click "Save".
You should now be presented with your key's secret. Make sure you copy this key because it won't be displayed again, as stated by the message on your screen.
Now that you have your key, create a new environment variable FAUNA_SECRET_KEY
inside your .env.local
file and restart your development server.
FAUNA_SECRET_KEY=<YOUR_SECRET_KEY>
Once we have our Fauna database and secret key, we can link it to NextAuth. To do so, we can use a NextAuth adapter to connect NextAuth to our Fauna database.
NextAuth provides this adapter out-of-the-box, so we don't need to create all the plumbing ourselves.
So, let's install the required dependencies using npm
:
npm install faunadb @next-auth/fauna-adapter@next
Then, open the pages/api/[...nextauth].js
file and import faunadb
and
@next-auth/fauna-adapter
.
import { Client as FaunaClient } from 'faunadb'; import { FaunaAdapter } from '@next-auth/fauna-adapter'; // ...
At the top of your file, instantiate a Fauna database's client by passing in the Fauna secret key from the environment variables.
const client = new FaunaClient({ secret: process.env.FAUNA_SECRET_KEY, });
And add this adapter to your NextAuth configuration object.
// ... export default NextAuth({ providers: [...], adapter: FaunaAdapter(client), });
Finally, the last step is to set up all the collections and indexes to store and retrieve the users' information and accounts in our Fauna database.
So, run the following queries inside of the Shell tab in the Fauna dashboard:
CreateCollection({ name: 'accounts' }); CreateCollection({ name: 'sessions' }); CreateCollection({ name: 'users' }); CreateCollection({ name: 'verification_tokens' });
CreateIndex({ name: 'account_by_provider_and_provider_account_id', source: Collection('accounts'), unique: true, terms: [ { field: ['data', 'provider'] }, { field: ['data', 'providerAccountId'] }, ], }); CreateIndex({ name: 'session_by_session_token', source: Collection('sessions'), unique: true, terms: [{ field: ['data', 'sessionToken'] }], }); CreateIndex({ name: 'user_by_email', source: Collection('users'), unique: true, terms: [{ field: ['data', 'email'] }], }); CreateIndex({ name: 'verification_token_by_identifier_and_token', source: Collection('verification_tokens'), unique: true, terms: [{ field: ['data', 'identifier'] }, { field: ['data', 'token'] }], });
You are now all set! Your Next.js app is connected to your Fauna database through the NextAuth adapter.
So now, let's try to login into our application.
To do so, visit http://localhost:3000/api/auth/signin, which is a default login page generated by NextAuth. Then enter your email address and click Sign in with email.
You should then be redirected to http://localhost:3000/api/auth/verify-request?provider=email&type=email.
Next, check your inbox and click the magic link from the email you just received. You should be redirected back to the homepage of your application.
To ensure everything has successfully worked, you can check your Fauna database. A new document for both the sessions and the users collections has been created for the authenticated user.
3. Check if someone is signed in
Now, let's design the homepage of our application to display information about the user who is signed in, and give them the ability to sign out from the app.
To interact with sessions from our React components/pages, NextAuth provides a
custom hooks named useSession
that gives us the current session, if any.
So, from the /pages/index.js
file, import this hook from next-auth/react
:
import { useSession } from 'next-auth/react';
Then, at the top of your Home
component, call the useSession
hook to
retrieve the current session:
export default function Home() { const { data: session } = useSession(); // ... }
Next, inside your JSX, either render a Link
to redirect the current visitor to
the login page if there is no session or display the email address of the
authenticated user in case there is a session.
import Link from 'next/link'; import { LightningBoltIcon } from '@heroicons/react/outline'; import { useSession } from 'next-auth/react'; export default function Home() { const { data: session } = useSession(); return ( <div className="min-h-screen container mx-auto px-6 py-12 flex flex-col items-center justify-center"> <h1 className="inline-flex flex-col sm:flex-row items-center space-y-2 sm:space-y-0 sm:space-x-2"> <LightningBoltIcon className="shrink-0 w-14 h-14 sm:w-16 sm:h-16 text-blue-500" /> <span className="sm:h-16 text-4xl sm:text-6xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-blue-400 to-blue-700 text-center"> Magic NextAuth </span> </h1> <p className="mt-4 text-gray-500 text-xl sm:text-2xl text-center"> Magic Link Authentication in Next.js with NextAuth and Fauna </p> <div className="mt-8"> {session?.user ? ( <div className="text-lg flex flex-col space-y-1 bg-gray-200 rounded-lg px-6 py-3"> <p> Signed in as <strong>{session.user.email}</strong> </p> </div> ) : ( <Link href="/api/auth/signin"> <a className="px-6 py-3 rounded-md text-lg text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-opacity-50 transition"> Get started </a> </Link> )} </div> </div> ); }
Note that I've used the Tailwind utility classes to style the page. Feel free to tweak as you wish.
I'm also using an SVG icon from the @heroicons
library for the logo. Make sure
to install it with npm i @heroicons/react
if you'd like to use those icons.
Finally, let's also add a button to let the user sign out from the application.
To do so, we can use the signOut()
method from NextAuth and call it once the
user clicks Sign Out
.
import { signOut, useSession } from 'next-auth/react'; // ... export default function Home() { const { data: session } = useSession(); return ( <div> {/* ... */} <div> {session?.user ? ( <div> {/* ... */} <button onClick={signOut} className="font-semibold underline opacity-70 hover:opacity-100" > Sign Out </button> </div> ) : ( { /* ... */ } )} </div> </div> ); }
Alright! You should now be able to see the authenticated user's email address from the homepage if you are logged in already, and you should be able to sign out from the app. And when you are not authenticated, you should see a big blue button redirecting you to the default login page on click.
Create a custom login page
Having default unbranded authentication pages is great when starting a new project. However, most of the time, we'd like to design our own pages to match our brand and business.
Fortunately, NextAuth let us define custom pages by using the pages
option
from within the NextAuth configuration object.
So, let's go ahead and do it.
export default NextAuth({ pages: { signIn: '/auth/signin', signOut: '/', }, // ... });
Here, I've defined a custom path for the login page and used /
for the sign
out page so that when the user signs out, they are redirected back to the
homepage of the application.
Let's keep going and create our custom login page in pages/auth/signin.js
:
import { useState } from 'react'; import { LightningBoltIcon } from '@heroicons/react/outline'; const SignIn = () => { const [email, setEmail] = useState(''); return ( <div className="min-h-screen flex flex-col items-center justify-center px-4 py-12"> <LightningBoltIcon className="shrink-0 w-14 h-14 sm:w-16 sm:h-16 text-blue-500" /> <h1 className="mt-2 text-2xl sm:text-4xl text-center font-bold"> Sign in to your account </h1> <form className="mt-8 rounded-lg shadow-md bg-white px-4 py-6 sm:px-8 sm:py-8 space-y-6 w-full max-w-md"> <div className="flex flex-col space-y-1"> <label htmlFor="email" className="text-gray-500 text-sm"> Email address </label> <input id="email" type="email" required value={email} onChange={e => setEmail(e.target.value)} placeholder="elon@spacex.com" className="py-2 px-4 w-full border rounded-md border-gray-300 focus:outline-none focus:ring-4 focus:ring-opacity-20 focus:border-blue-400 focus:ring-blue-400 transition disabled:opacity-50 disabled:cursor-not-allowed " /> </div> <button type="submit" className="px-6 py-2 rounded-md text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-4 focus:ring-blue-500 focus:ring-opacity-50 w-full disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-500 transition" > Sign in </button> </form> </div> ); }; export default SignIn;
You should get this beautiful login page when visiting http://localhost:3000/auth/signin.
At this point, make sure to replace the link to the login page in the
/pages/index.js
from /api/auth/signin
to /auth/signin
.
Now that we have created our custom login page, let's implement the sign-in
logic when the user clicks Sign in
after entering their email address.
So, create a new function named handleSignIn
inside your component, and call
it from the onSubmit
event of the form tag.
const handleSignIn = async e => { e.preventDefault(); }; <form onSubmit={handleSignIn} .../>
Inside this function, call the signIn
method from next-auth/react
(don't
forget to import it at the top):
import { signIn } from 'next-auth/react'; // ... try { // Perform sign in const { error } = await signIn('email', { email, redirect: false, callbackUrl: `${window.location.origin}/auth/confirm-request`, }); // Something went wrong if (error) { throw new Error(error); } } catch(error) { // handle error here (eg. display message to user) }
Here, we set the sign-in method to email
, and we pass an option
object which
contains:
- the email address the user entered in the input field
- the
redirect
option set tofalse
as we want the user to stay on this page - and the
callbackUrl
set to${window.location.origin}/auth/confirm-request
which is the base URL of the magic link sent to the user. We will create this page below.
In addition to calling the signIn
method and because we set the redirect
option to false
, let's display a dialog to the user once the sign-in email has
been sent successfully.
For doing so, we first need to create the corresponding Modal
component:
import { createPortal } from 'react-dom'; const MagicLinkModal = ({ show = false, email = '' }) => { if (!show) return null; return createPortal( <div className="fixed inset-0 z-10 bg-white bg-opacity-90 backdrop-filter backdrop-blur-md backdrop-grayscale"> <div className="min-h-screen px-6 flex flex-col items-center justify-center animate-zoomIn"> <div className="flex flex-col items-center justify-center text-center max-w-sm"> <MailOpenIcon className="shrink-0 w-12 h-12 text-blue-500" /> <h3 className="mt-2 text-2xl font-semibold">Confirm your email</h3> <p className="mt-4 text-lg"> We emailed a magic link to <strong>{email}</strong>. Check your inbox and click the link in the email to login. </p> </div> </div> </div>, document.body ); };
Then, create a new state variable to show/hide this dialog.
const [showModal, setShowModal] = useState(false);
Next, show the dialog after calling the signIn
method only if there is no
error.
const handleSignIn = async e => { e.preventDefault(); try { // Perform sign in const { error } = await signIn('email', { email, redirect: false, callbackUrl: `${window.location.origin}/auth/confirm-request`, }); // Something went wrong if (error) { throw new Error(error); } setShowModal(true); } catch (error) { // ... } };
Finally, render the dialog component at the end of your JSX along with its props.
return ( <> <div>{/* ... */}</div> <MagicLinkModal show={showModal} email={email} /> </> );
Create a custom confirmation page
In addition to this custom sign-in page, let's create a custom confirmation page to let the user knows they have successfully logged into the application after clicking the magic link from the email.
So, create a new file named confirm-request.js
inside the /pages/auth
folder.
Inside this file, use the useSession
hook to check if the user has been
properly authenticated. If not, redirect them to the homepage using the
useRouter
hook from next/router
.
Otherwise, display a message on the screen to let the user knows they have successfully logged in.
import Link from 'next/link'; import { useRouter } from 'next/router'; import { useSession } from 'next-auth/react'; import { CheckCircleIcon } from '@heroicons/react/outline'; const ConfirmRequest = () => { const { data: session, status } = useSession(); const loading = status === 'loading'; const router = useRouter(); if (!loading && !session) { router.push('/auth/signin'); } return ( <div className="min-h-screen flex flex-col items-center justify-center text-center px-4 py-12 max-w-md mx-auto"> {loading ? ( <p>Loading...</p> ) : !session ? ( <p>Redirecting...</p> ) : ( <> <CheckCircleIcon className="w-14 h-14 sm:w-16 sm:h-16 text-blue-600 shrink-0" /> <h1 className="text-2xl sm:text-4xl font-bold mt-4"> You're logged in! </h1> <p className="text-lg sm:text-2xl mt-4"> Go back to your original tab. </p> <p className="text-normal sm:text-lg text-gray-500 mt-6"> You can close this window or click{' '} <Link href="/"> <a className="text-blue-500 hover:underline hover:text-blue-600"> this link </a> </Link>{' '} to go back to the homepage. </p> </> )} </div> ); }; export default ConfirmRequest;
Customize the sign-in email and send a welcome email to new users
Well done! You now have implemented passwordless authentication into your application and created custom login and confirmation pages.
But, let's go a step further and also customize the verification email we send to the user, and send a welcome email to our new users.
First, install the handlebars
package as we are going to need it for creating
custom dynamic emails:
npm i handlebars
Then, inside the pages/api/auth/[...nextauth].js
file, import handlebars
at
the top.
import Handlebars from 'handlebars';
Create a reusable transporter object using your SMTP settings:
const transporter = nodemailer.createTransport({ host: process.env.EMAIL_SERVER_HOST, port: process.env.EMAIL_SERVER_PORT, auth: { user: process.env.EMAIL_SERVER_USER, pass: process.env.EMAIL_SERVER_PASSWORD, }, secure: true, });
Edit the email provider configuration by removing all the SMTP settings and add
the sendVerificationRequest
option to it:
export default NextAuth({ // ... providers: [ EmailProvider({ maxAge: 10 * 60, sendVerificationRequest, }), ], // ... });
And define the sendVerificationRequest
function above:
import { readFileSync } from 'fs'; import path from 'path'; // ... const emailsDir = path.resolve(process.cwd(), 'emails'); const sendVerificationRequest = ({ identifier, url }) => { const emailFile = readFileSync(path.join(emailsDir, 'confirm-email.html'), { encoding: 'utf8', }); const emailTemplate = Handlebars.compile(emailFile); transporter.sendMail({ from: `"⚡ Magic NextAuth" ${process.env.EMAIL_FROM}`, to: identifier, subject: 'Your sign-in link for Magic NextAuth', html: emailTemplate({ signin_url: url, email: identifier, }), }); }; // ...
This function is doing a few things.
-
First, it is reading the confirmation email file and using Handlebars to create a template we can then use to inject dynamic values.
-
Then, it uses the
transporter
object to send the email to theidentifier
, which is the user's email address we get from the function's arguments. -
Finally, the
html
option of thesendMail
function receives the HTML of our email after injecting a few values, such as the magic link and the user's email, and compiling it with Handlebars.
If you don't have an HTML template for your email, you can use this one.
Here's the email you should now receive when trying to login into your application.
Finally, four our last step in this tutorial, we will send a welcome email to
our new users, using the NextAuth createUser
event.
This event is an asynchronous function called by NextAuth when the adapter (Fauna) creates a new user on sign-in. It is very useful because it gives us the ability to run side effects when such an event occurs.
For the full list of the NextAuth events, check out the official documentation here.
So, as usual, edit the NextAuth configuration, and add the createUser
event to
it.
export default NextAuth({ // ... events: { createUser: sendWelcomeEmail } )};
Them, create the sendWelcomeEmail
function above it and send the welcome email
to the user like with Nodemailer and Handlebars like we did before for
the verification request.
const sendWelcomeEmail = async ({ user }) => { const { email } = user; try { const emailFile = readFileSync(path.join(emailsDir, 'welcome.html'), { encoding: 'utf8', }); const emailTemplate = Handlebars.compile(emailFile); await transporter.sendMail({ from: `"⚡ Magic NextAuth" ${process.env.EMAIL_FROM}`, to: email, subject: 'Welcome to Magic NextAuth! 🎉', html: emailTemplate({ base_url: process.env.NEXTAUTH_URL, support_email: 'support@alterclass.io', }), }); } catch (error) { console.log(`❌ Unable to send welcome email to user (${email})`); } };
You can find the HTML template for the welcome email here.
Summary
Congratulations on reaching the end of this tutorial! You now have a Next.js application all set with passwordless authentication using NextAuth and Fauna DB for persisting user information. You even have learned how to create custom authentication pages using React and Tailwind CSS, and customize the emails you send to your users.
If you liked this tutorial, follow me on Twitter and subscribe to my YouTube channel.
And if you've got any questions, please ask them on Twitter!
Guest Authors
Write for the AlterClass blog to empower the developer community and grow your brand.
Join usStay up to date
Subscribe to the newsletter to stay up to date with tutorials, courses and much more!