15 min

Setting up Authjs with Nextjs and Prisma tutorial

Setting up Authjs with Nextjs and Prisma tutorial

Next-auth

Why next-auth

Its currently most popular auth solution for nextjs and also integrates well with the nextjs.

Also it allows the entire auth flow to function from within the app and our own database, compared to solutions like clerk.dev and Auth0 which use a third party hosted solution and our user data is not in our own database.

next-auth is rebranding to be more framework agnostic and rebranding to authjs, However authjs is the v5 version of the library. The v4 version is still known as next-auth in the docs. For simplicity I will just use the term next-auth but we are using the latest v5 version in this tutorial. (Sorry, I know its confusing)

Older v4 Docs (can be partially used for the latest library)
https://next-auth.js.org

Newer Docs
https://authjs.dev

Next-auth setup

First we can setup our next-auth. We use a Google Oauth and email provider setup for authentication.

We first initialize a Prisma client, then pass it in to the authjs prisma adapter.

Then we need to initialize our Google client with the right crediantials. How to create google credentials can be found below.
Create Google Oauth Credentials

Next we need to setup up our Email Provider. There are a couple ways to do this. We will simply pass in custom sendVerificationRequest that we will go over in the next section.

Full config options can be found below:
Authjs Setup Docs

We use Session auth for authentication. Session auth requires us to make a request to the db to check auth status vs JWT which just checks for a valid token.

Session is better in SAAS apps because it allows us to maintain a single source of truth in a multi tenancy app.

For example if a user is deleted by an admin, they would still be able to log in if they were using a JWT.

Signout everywhere also only works with session auth.

In some apps jwt can be more efficient since we dont need to make a database call when checking session.

Checkout this comparison of jwt vs session for more info:
Jwt vs Session

Installation

To use v5 we need to install the beta version.

npm install next-auth@beta

We can also set our env variables.

NEXTAUTH_URL= is only used in prod. We can comment it out in development mode.

NEXTAUTH_SECRET= is user defined

1NEXTAUTH_URL=
2NEXTAUTH_SECRET=

Below we have our main auth config object.


auth/authConfig.ts
1import NextAuth from 'next-auth';
2import { PrismaAdapter } from '@auth/prisma-adapter';
3import { PrismaClient } from '@prisma/client';
4import Google from 'next-auth/providers/google';
5import EmailProvider from 'next-auth/providers/email';
6import { sendVerificationRequest } from './sendEmail';
7
8const prisma = new PrismaClient();
9
10export const {
11 handlers: { GET, POST },
12 auth
13} = NextAuth({
14 providers: [
15 Google({
16 clientId: process.env.GOOGLE_CLIENT_ID,
17 clientSecret: process.env.GOOGLE_CLIENT_SECRET
18 }),
19 EmailProvider({
20 sendVerificationRequest
21 })
22 ],
23 adapter: PrismaAdapter(prisma),
24 session: { strategy: 'database' },
25 pages: {
26 signIn: '/auth/signin'
27 },
28 callbacks: {
29 async session({ session, user }) {
30 if (user || session) {
31 session.user.id = user.id;
32 }
33 }
34 }
35});
36

Login and Sessions

After setting up the next-auth config object, we can define our login functions.

We will use both Google Oauth and Email provider for our authentication.

We can use the built in signIn() function from next-auth for this.


auth/login.ts
1import 'client-only';
2import { signIn, signOut } from 'next-auth/react';
3import { EmailFormValues } from '@/lib/types/validations';
4
5export const Login = async ({ email }: EmailFormValues) => {
6 try {
7 const signInResult = await signIn(
8 'email',
9 {
10 email: email.toLowerCase(),
11 redirect: false,
12 callbackUrl: '/dashboard'
13 }
14 );
15 } catch (err) {
16 throw err;
17 }
18};
19
20export const GoogleLogin = async () => {
21 try {
22 const signInResult = await signIn('google', {
23 callbackUrl: '/dashboard'
24 });
25
26 } catch (err) {
27 throw err;
28 }
29};
30
31export const Logout = async () => {
32 try {
33 await signOut({ callbackUrl: 'login' });
34 } catch (err) {
35 throw err;
36 }
37};
38
39
40// In your login form
41// loginForm.tsx
42
43...
44const onSubmit = async (values: EmailFormValues) => {
45 const props = { email: values.email };
46
47 await Login(props);
48
49 router.push('/auth/confirmed');
50};
51
52const handleGoogleSignIn = async () => {
53 await GoogleLogin();
54};
55...
56

Working with emails in next-auth

Next-auth requires using emails for the email auth provider. We need to setup a way to work with emails locally and not send actual emails in development and we also need a transactional email server that can send real emails in production.

We will use 2 different tools for this. We will work with emails locally with Maildev and we can use Resend for sending real emails in prod.

MailDev

Resend

Sending Emails

Here we will setup our custom sendVerificationRequest function that we set in the next-auth config.


auth/sendEmail.ts
1import { SendVerificationRequestParams } from 'next-auth/providers';
2import MagicLinkEmail from '../MagicLink';
3import { renderAsync } from '@react-email/render';
4import transporter from '../transporter';
5import { Resend } from 'resend';
6
7const resend = new Resend(process.env.RESEND_API_KEY);
8
9export const sendVerificationRequest = async ({
10 url,
11 identifier
12}: SendVerificationRequestParams) => {
13
14 //https://github.com/resendlabs/resend-node/issues/256
15 const html = await renderAsync(
16 MagicLinkEmail({
17 actionUrl: url,
18 siteName: 'example site'
19 })
20 );
21
22 try {
23 if (process.env.NODE_ENV === 'production') {
24 await resend.emails.send({
25 from: 'My SaaS <onboarding@resend.dev>',
26 to: 'delivered@resend.dev', // for testing resend, use identifier when going live.
27 subject: "Login to Account",
28 html,
29 });
30 } else {
31 await transporter.sendMail({
32 to: identifier,
33 subject: "Login to Account",,
34 text: url
35 html
36 });
37 }
38 } catch (error) {
39 throw new Error('Failed to send verification email.');
40 }
41};

Setting up the transporter and maildev

These there the transporter settings for maildev. When we send an email in our app they will be captured by maildev.

auth/transporter.ts
1// Used for local development and e2e testing
2
3import nodemailer from 'nodemailer';
4
5const host = 'localhost';
6const port = 1025;
7
8const transporter = nodemailer.createTransport({
9 host,
10 port
11});
12
13export default transporter;

Setting up the email

Here we can build an email using react components with the react-email library.

actionUrl will be the callback Url we received from our sendVerificationRequest function and we can pass it as a href here.

auth/MagicLink.tsx
1import {
2 Body,
3 Button,
4 Container,
5 Head,
6 Html,
7 Section,
8 Tailwind,
9 Text
10} from '@react-email/components';
11import { Icons } from '@/components/Icons';
12
13type MagicLinkEmailProps = {
14 actionUrl: string;
15 siteName: string;
16};
17
18export const MagicLinkEmail = ({ actionUrl, siteName }: MagicLinkEmailProps) => (
19 <Html>
20 <Head />
21 <Tailwind>
22 <Body className="bg-white font-sans">
23 <Container className="mx-auto py-5 pb-12">
24 <Icons.Command className="m-auto block h-10 w-10" />
25 <Text className="text-base">Welcome to {siteName} Activate your account.</Text>
26 <Section className="my-5 text-center">
27 <Button
28 className="inline-block rounded-md bg-zinc-900 px-4 py-2 text-base text-white no-underline"
29 href={actionUrl}
30 >
31 Activate Account
32 </Button>
33 </Section>
34 </Container>
35 </Body>
36 </Tailwind>
37 </Html>
38);
39
40export default MagicLinkEmail;

Getting Sessions and User Data

Here we can get our session and User data from the exported auth object in the auth config file.

We can use it to protect auth routes and display user data.

page.tsx
1import { auth } from './auth';
2
3export default async function Page() {
4 const session = await auth();
5 if (!session) redirect('/login');
6
7 return (
8 <div>
9 User Email: {session.user.email}
10 </div>
11 );
12}
13

Testing with playwright

For testing we can setup auth in a setup function and save the auth session in a file. This will allow us to reuse the auth data without having to login in before every test.

We can also call the mail dev api directly to get the redirect URL for email auth.

Playwright Authentication

tests/authsetup.ts
1import { test as setup, expect } from '@playwright/test';
2
3import { routes, user } from '../config';
4
5const userFile = 'playwright/.auth/user.json'
6
7setup('authenticate as Owner', async ({ page, request }) => {
8 await page.goto('http://localhost:3000/');
9 await page.getByRole('link', { name: 'Login' }).click();
10
11 await page.waitForURL('**/auth/login');
12 await page.getByPlaceholder('Email').fill("test@yahoo.com");
13 await page.getByRole('button', { name: 'Login with Email' }).click();
14
15 await page.waitForURL('/confirmed');
16 await expect(page.getByRole('heading')).toContainText('Email Sent');
17
18 //maildev api
19 const emails = await request.get('http://localhost:1080/email');
20 const res = JSON.parse(await emails.text());
21 const lastEmail = res.slice(-1)[0];
22 const redirectUrl = lastEmail.text;
23
24 await page.goto(redirectUrl);
25 await page.waitForURL('/user');
26
27 // End of authentication steps.
28 await page.context().storageState({ path: userFile });
29});