In this tutorial, you'll learn how to set up authentication in your Next.js application using Next Auth. Whether you're a beginner or have some experience with authentication, this guide will walk you through everything you need to know to get up and running smoothly.
- Setting up Next Auth: Learn how to integrate Next Auth into your Next.js application for robust authentication solutions.
- Configuring the Google Provider: Step-by-step instructions on how to set up Google as an authentication provider.
- Authentication Sessions: Manage user sessions effectively to keep your application secure and user-friendly.
- Accessing Sessions on the Client;
- Accessing Sessions on the Server;
- Sign Out Users;
- Protecting Routes: Implement route protection to ensure only authenticated users can access certain pages.
- Database Adapters: Explore how to use different database adapters to store and manage user data efficiently.
- Configuring the Credentials Provider: Set up custom credentials for user authentication, giving you flexibility in how users log in.
- Registering Users;
By the end of this tutorial, you’ll have a fully functional authentication system integrated into your Next.js app, complete with secure sessions, protected routes, and more.
Let’s get started!
To set up NextAuth.js in a Next.js 13.2 application using the new App Router and Route Handlers, follow these steps:
First, install NextAuth.js and its dependencies:
npm install next-auth
Next, set up the route handler for NextAuth.js in the app
directory. Create the necessary folders and files:
- Create the following directory:
app/api/auth/[...nextauth]
. - Inside this directory, create a
route.ts
file.
The route.ts
file should contain the following code:
import NextAuth from 'next-auth';
const handler = NextAuth({});
export { handler as GET, handler as POST };
This setup ensures that all requests to /api/auth/*
are handled by NextAuth.js using the new Route Handlers introduced in Next.js 13.
Create a .env.local
file in the root of your project and add the following environment variables:
NEXTAUTH_SECRET=your-secret=
NEXTAUTH_URL=http://localhost:3000
In this chapter, we will configure the Google provider to allow users to authenticate with their Google accounts. This involves setting up OAuth credentials in the Google Cloud Console and integrating them with NextAuth.js in your Next.js application.
- Go to the providers => select google;
- Click on the project dropdown at the top and select "New Project";
- Enter your project name and location, then click "Create";
- Navigate to the "APIs & Services" > "OAuth consent screen."
- Select "External" as the user type and click "Create."
- Fill in the required fields such as App name, User support email, and Developer contact information.
- Click "Save and Continue."
- On the "Scopes" page, click "Add or Remove Scopes."
- Add the necessary OAuth scopes such as
email
andprofile
. - Click "Save and Continue."
- On the "Test Users" page, add the email addresses of the users you want to test with.
- Click "Save and Continue."
- Navigate to "APIs & Services" > "Credentials."
- Click on "Create Credentials" and select "OAuth 2.0 Client IDs."
- Choose "Web application" as the application type.
- Add the authorized JavaScript origins and redirect URIs. For local development, you can use
http://localhost:4200
andhttp://localhost:4200/api/auth/callback/google
. - Click "Create" and save your Client ID and Client Secret.
-
Open your
[...nextauth].ts
file in theapp/api/auth/[...nextauth]
directory. -
Configure the Google provider with the credentials you obtained from the Google Cloud Console.
import NextAuth from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; const authOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), // Add more providers here ], }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
-
Ensure your environment variables in
.env.local
are correctly set:GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret NEXTAUTH_SECRET=your-nextauth-secret
Replace the placeholders with your actual Google OAuth 2.0 credentials, which you can obtain from the Google Developer Console.
-
To test it let's update our navbar...
<li className="nav-item"> <Link className="nav-link" href={'/api/auth/signin'}> Login </Link> </li>
...now we should be able to login with Google.
When a user signs in, Next Auth creates an authentication session for that user. By default, authentication sessions are represented using JSON Web Tokens (JWTs). But sessions can also be stored in a database
To access the authentication session on the client, we have to wrap our application with SessionProvider. This component uses React Context to pass the authentication session down the component tree. Since React Context is only available in client components, we have to wrap SessionProvider with a client component
.
Wrap your application with the SessionProvider
to manage authentication sessions. This involves updating the layout.tsx
file in the app
directory:
-
Create a
auth
folder in theapp
directory. -
Inside the
auth
folder, create anprovider.tsx
file with the following content:'use client'; import React, { ReactNode } from 'react'; import { SessionProvider } from 'next-auth/react'; interface Props { children: ReactNode; } const AuthProvider = ({ children }: Props) => { return <SessionProvider>{children}</SessionProvider>; }; export default AuthProvider;
-
Modify the
layout.tsx
file to include theAuthProvider
:import './globals.css'; import Navbar from './components/Navbar'; import AuthProvider from './context/AuthProvider'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <AuthProvider> <Navbar /> {children} </AuthProvider> </body> </html> ); }
-
Let's consume our
AuthProvider
withuseSession()
const Navbar = () => { const { data: session, status } = useSession(); const [label, setLabel] = useState(''); useEffect(() => { handleLinks(); }, [status]); const handleLinks = () => { if (status === 'unauthenticated') { setLabel('Login'); } else { setLabel(session?.user.name); } }; return ( <nav className="navbar navbar-expand-lg navbar-light bg-light"> <div className="container-fluid"> <a className="navbar-brand">Navbar</a> <div className="collapse navbar-collapse d-flex justify-content-between" id="navbarNav" > <ul className="navbar-nav"> <li className="nav-item"> {status === 'authenticated' && ( <Link className="nav-link active" aria-current="page" href={'/todos'} > Todos </Link> )} </li> <li className="nav-item"> {status === 'authenticated' && ( <Link className="nav-link" href={'/products'}> Products </Link> )} </li> </ul> <ul className="navbar-nav"> <li className="nav-item"> <Link className="nav-link" href={'/api/auth/signin'} > {label} </Link> </li> </ul> </div> </div> </nav> ); };
Note: To consume the
useSession
provider, we had to transform the Navbar into a client component by adding 'use client'. We can achieve the same result using SSR (Server-Side Rendering) techniques.
NextAuth.js provides a way to access session information on the server side using the getServerSession
function. This function is particularly useful when you need to protect server-side rendered pages or perform server-side operations that require user authentication.
-
Import
getServerSession
andauthOptions
: Ensure you import thegetServerSession
function fromnext-auth/next
andauthOptions
from your NextAuth.js configuration file.import { getServerSession } from 'next-auth/next'; import { authOptions } from './api/auth/[...nextauth]/route';
-
Use
getServerSession
in Your Server Component: CallgetServerSession
within your server component or API route to retrieve the session.const Navbar = async () => { const session = await getServerSession(authOptions);
import Link from 'next/link';
import { authOptions } from '../api/auth/[...nextauth]/route';
import { getServerSession } from 'next-auth';
const Navbar = async () => {
const session = await getServerSession(authOptions);
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light">
<div className="container-fluid">
<a className="navbar-brand">Navbar</a>
<div
className="collapse navbar-collapse d-flex justify-content-between"
id="navbarNav"
>
<ul className="navbar-nav">
<li className="nav-item">
{session && (
<Link
className="nav-link active"
aria-current="page"
href={'/todos'}
>
Todos
</Link>
)}
</li>
<li className="nav-item">
{session && (
<Link className="nav-link" href={'/products'}>
Products
</Link>
)}
</li>
</ul>
<ul className="navbar-nav">
<li className="nav-item">
<Link
className="nav-link"
href={'/api/auth/signin'}
>
{(session && session.user.name) || 'Login'}
</Link>
</li>
</ul>
</div>
</div>
</nav>
);
};
export default Navbar;
Signing out users with NextAuth.js is very simple. You just need to add a link to the sign-out endpoint provided by NextAuth.js. Here's how you can do it:
<Link
className="nav-link"
href={'/api/auth/signout'}
>
Sign Out
</Link>
By adding this link, NextAuth.js will handle the sign-out process and return a logout prompt. When users click on this link, they will be signed out of your application.
Using middleware in Next.js, we can execute code before a request is completed. This allows us to redirect users to the login page if they attempt to access a private part of our application without an active session.
-
Create the Middleware File: Create a middleware file in your project root.
touch middleware.ts
Remember to spell
middleware.ts
correctly as part of the Next.js convention. -
Implement the Middleware: Open
middleware.ts
and add the following code:import { NextRequest, NextResponse } from 'next/server'; function middleware(req: NextRequest) { return NextResponse.redirect(new URL('/products', req.url)); } export const config = { matcher: ['/todos'] } export default middleware;
This middleware redirects the user to products everytime we hit the todos rotuer.
Matcher: The
config
object specifies the paths that the middleware should apply to. In this case, it matches the/users
path.
import middleware from 'next-auth/middleware';
export default middleware;
export const config = {
matcher: ['/todos'],
};
NextAuth.js comes with a variety of database adapters that allow you to store user and session data in different types of databases. Using these adapters, you can easily integrate NextAuth.js with your preferred database. Here’s how you can set up a database adapter in your Next.js application using Prisma.
-
Prisma Adapter:
npm i @next-auth/prisma-adapter
-
Update NextAuth.js Configuration: Configure NextAuth.js to use the Prisma adapter.
import { PrismaAdapter } from '@next-auth/prisma-adapter'; import NextAuth, { NextAuthOptions } from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; import prisma from '../../../../prisma/client'; export const authOptions: NextAuthOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), // Add more providers here ], adapter: PrismaAdapter(prisma), session: { strategy: 'jwt', }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
-
Define Your Schema: Update your
schema.prisma
file to include models for the NextAuth.js tables. First let's drop the user table:- delete
model User
and save; - run
npx prisma migrate dev
- copy and paste from here
model Account { id String @id @default(cuid()) userId String @map("user_id") type String provider String providerAccountId String @map("provider_account_id") refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") } model Session { id String @id @default(cuid()) sessionToken String @unique @map("session_token") userId String @map("user_id") expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? @map("email_verified") image String? accounts Account[] sessions Session[] @@map("users") } model VerificationToken { identifier String token String expires DateTime @@unique([identifier, token]) @@map("verificationtokens") }
- delete
-
Run Prisma Migrate: Run the migration to create the tables in your database.
npx prisma migrate dev
-
Verifying in the Database After setting up and running your application, you should be able to see the user data in your database. For example, when a user logs in using Google, their information will be stored in the users table:
id name email email_verified image clw6zg7xk0000bjjm3uepkxzb Valerio valerio@ga.co NULL https://lh3.googleusercontent.com/a/ACg8ocXXX... This table shows the
id
,name
,email
,email_verified
, andimage
fields for the user stored in the database.
Configuring the Credentials Provider
To use the Credentials provider in NextAuth.js for custom email and password authentication, follow these steps. This setup will include using bcrypt for hashing passwords.
-
Install Dependencies: Install bcrypt and its TypeScript types for password hashing.
npm install bcrypt npm install @types/bcrypt --save-dev
-
Update User Model: Add a
hashedPassword
field to your user model in Prisma schema.model User { id String @id @default(uuid()) name String? email String @unique emailVerified DateTime? image String? hashedPassword? String accounts Account[] sessions Session[] }
Run the Prisma migration to update your database schema.
npx prisma migrate dev
-
Configure NextAuth.js: Update the NextAuth.js configuration to include the Credentials provider. Use bcrypt to compare the hashed password stored in the database with the password provided during login.
import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import bcrypt from 'bcrypt'; import { PrismaAdapter } from '@next-auth/prisma-adapter'; import { prisma } from '@/lib/prisma'; export const authOptions = { providers: [ CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email', placeholder: 'Email' }, password: { label: 'Password', type: 'password', placeholder: 'Password', }, }, async authorize(credentials) { if (!credentials?.email || !credentials.password) { return null; } const user = await prisma.user.findUnique({ where: { email: credentials.email }, }); if (!user || !user.hashedPassword) { return null; } const isValidPassword = await bcrypt.compare(credentials.password, user.hashedPassword); if (!isValidPassword) { return null; } return user; }, }), // Add more providers here ], adapter: PrismaAdapter(prisma), session: { strategy: 'jwt', }, pages: { signIn: '/auth/signin', }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };
import { NextRequest } from 'next/server';
import userSchema from '../users/schema';
import { badRequest } from '../lib/handlers/errors/customResponses';
import prisma from '../../../prisma/client';
import { handleCreate } from '../lib/handlers/crudHandlers';
import bcrypt from 'bcrypt';
async function registerRoute(request: NextRequest) {
const { user: userModel } = prisma;
const body = await request.json();
const validation = userSchema.safeParse(body);
if (!validation.success) {
const { errors } = validation.error;
return badRequest(errors);
} else {
const clone = { ...body };
clone.hashedPassword = await bcrypt.hash(body.hashedPassword, 10);
return handleCreate(userModel, clone, { email: clone.email });
}
}
export { registerRoute as POST };