Skip to content

Next Auth

Next.js lets you build full websites with both frontend and backend. One very important part of most websites is letting users log in. In this guide, we will learn how to add login (authentication) to your Next.js app using a tool called Next Auth.

Next Auth is a free, ready-to-use tool that helps you add login to your Next.js app. It supports many ways to log in - like with Google, GitHub, Facebook, or even a plain email and password.

Docs: next-auth.js.org | authjs.dev | better-auth.com

  1. Install Next Auth: First, add the Next Auth package to your project by running one of these commands in your terminal:

    Terminal window
    npm install next-auth
    # or
    bun add next-auth
  2. Configure Next Auth: Create a new file called route.ts inside this folder: app/api/auth/[...nextauth]/. This file tells Next Auth how your login should work.

    // app/api/auth/[...nextauth]/route.ts
    import NextAuth from 'next-auth';
    const handler = NextAuth({})
    export { handler as GET, handler as POST };
  3. Add Environment Variables: Next Auth needs some secret keys to work. Create a file called .env.local in the root of your project and add these lines:

    NEXTAUTH_URL=http://localhost:3000
    NEXTAUTH_SECRET=your-long-secret-key

A “provider” is just the way someone logs in - like logging in with Google or GitHub. You set up your providers inside the route.ts file in the app/api/auth/[...nextauth] folder.

To let users log in with Google, you need to create a project in the Google Developers Console and get a Client ID and Client Secret (these are like a username and password for your app to talk to Google).

  1. Go to the Google API Console

  2. Create a new project: Click “New Project”, give it a name, and create it.

  3. Configure consent screen: Set up the OAuth consent screen. This is the screen users see before they log in. Fill in your app name, support email, and authorized domains. Save your changes.

  4. Configure Credentials:

    • Go to the “Credentials” section.
    • Click “Create Credentials” and choose “OAuth 2.0 Client IDs”.
    • Choose “Web application” as the type.
    • Add your origin URI (e.g., http://localhost:3000).
    • Add your redirect URI (e.g., http://localhost:3000/api/auth/callback/google).
  5. Save Credentials: After creating the credentials, you will get a Client ID and Client Secret. Add them to your .env.local file:

    GOOGLE_CLIENT_ID=your-google-client-id
    GOOGLE_CLIENT_SECRET=your-google-client-secret
  6. Configure Google Provider: Update your route.ts file to use the Google provider:

    // app/api/auth/[...nextauth]/route.ts
    import NextAuth from 'next-auth';
    import GoogleProvider from 'next-auth/providers/google';
    const handler = NextAuth({
    providers: [
    GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    ],
    });
    export { handler as GET, handler as POST };
  7. Now go to http://localhost:3000/api/auth/signin in your browser. You should see a “Sign in with Google” button. Clicking it will take you to Google’s login page, and after logging in, you will be sent back to your app.

To let users log in with GitHub, you need to create an OAuth App in your GitHub account settings and get a Client ID and Client Secret.

  1. Go to GitHub Developer Settings

  2. Create a new OAuth App: Click “New OAuth App” and fill in the details:

    • Application name: your app name
    • Homepage URL: http://localhost:3000
    • Authorization callback URL: http://localhost:3000/api/auth/callback/github
  3. Save Credentials: After creating the app, you will get a Client ID. Click “Generate a new client secret” to get the Client Secret. Add both to your .env.local file:

    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret
  4. Configure GitHub Provider: Update your route.ts file to add the GitHub provider:

    // app/api/auth/[...nextauth]/route.ts
    import NextAuth from 'next-auth';
    import GoogleProvider from 'next-auth/providers/google';
    import GitHubProvider from 'next-auth/providers/github';
    const handler = NextAuth({
    providers: [
    GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    GitHubProvider({
    clientId: process.env.GITHUB_CLIENT_ID!,
    clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
    ],
    });
    export { handler as GET, handler as POST };
  5. Now go to http://localhost:3000/api/auth/signin. You should see both “Sign in with Google” and “Sign in with GitHub” buttons.

A “session” is how your app remembers that a user is logged in. By default, Next Auth uses something called JWT (JSON Web Token) to manage sessions. You can tell Next Auth to use JWT by adding a session option:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
session: {
strategy: "jwt",
},
});
export { handler as GET, handler as POST };

You can check if a user is logged in inside a client component using the useSession hook. But to use it, you first need to wrap your app with a SessionProvider.

The problem is that layout.tsx is a server component, so you cannot add SessionProvider directly in it. Instead, create a separate client component and use it in the layout:

// app/components/SessionProviderWrapper.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
import React from 'react';
export default function SessionProviderWrapper({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
// app/layout.tsx
import SessionProviderWrapper from './components/SessionProviderWrapper';
import React from 'react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SessionProviderWrapper>
{children}
</SessionProviderWrapper>
</body>
</html>
);
}

Now you can use useSession in any client component to get the logged-in user’s info:

// app/components/UserProfile.tsx
'use client';
import { useSession } from 'next-auth/react';
export default function UserProfile() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <p>Loading...</p>;
}
if (!session) {
return <p>You are not logged in.</p>;
}
return (
<div>
<h1>Welcome, {session.user?.name}!</h1>
<p>Email: {session.user?.email}</p>
</div>
);
}

On the server side, you can use getServerSession to get the logged-in user’s info. You pass in the same authOptions you set up in your route.ts file:

// app/components/ServerUserProfile.tsx
import { getServerSession } from 'next-auth/next';
export default async function ServerUserProfile() {
const session = await getServerSession({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
});
if (!session) {
return <p>You are not logged in.</p>;
}
return (
<div>
<h1>Welcome, {session.user?.name}!</h1>
<p>Email: {session.user?.email}</p>
</div>
);
}

To log out a user, just link them to /api/auth/signout. Next Auth handles everything else for you:

// app/components/navbar.tsx
import Link from 'next/link';
export default function Navbar() {
return (
<nav>
<Link href="/api/auth/signout">Logout</Link>
</nav>
);
}

Middleware is code that runs before a page loads. You can use it to check if a user is logged in, and if not, redirect them to the login page. This is how you “protect” pages so only logged-in users can see them.

Create a middleware.ts file in the root of your project:

// middleware.ts
import { NextResponse } from 'next/server';
import middleware from 'next-auth/middleware';
export default middleware;
// or
export { default } from 'next-auth/middleware';
// you can create middleware too but this middleware is already provided by next-auth
// import { getServerSession } from 'next-auth/next';
// import { authOptions } from './app/api/auth/[...nextauth]/route';
// export async function middleware(req: Request) {
// const session = await getServerSession(authOptions);
// if (!session) {
// // Redirect to the login page if the user is not logged in
// return NextResponse.redirect(new URL('/api/auth/signin', req.url));
// }
// // Let the request continue if the user is logged in
// return NextResponse.next();
// }
// If you do not provide a config, middleware will run for all routes
export const config = {
matcher: ['/protected/:path*'], // Only run middleware on protected routes
};

The matcher config tells Next.js which pages the middleware should run on. You can use :path* (zero or more pages inside that route), :path+ (one or more), or :path? (zero or one). For more details, check the Next.js Middleware documentation.

By default, Next Auth does not save user info to your database. But you can use an “adapter” to connect Next Auth to a database. Here we use the Prisma adapter to save user data.

  1. We already set up the database in the previous chapter. Now install the Prisma adapter for Next Auth:

    Terminal window
    npm install @next-auth/prisma-adapter
  2. Import the Prisma adapter and add it to your route.ts file:

    // app/api/auth/[...nextauth]/route.ts
    import NextAuth from 'next-auth';
    import GoogleProvider from 'next-auth/providers/google';
    import { PrismaAdapter } from '@next-auth/prisma-adapter';
    import { prisma } from "../prisma/client"; // Adjust the path to your prisma client
    const handler = NextAuth({
    adapter: PrismaAdapter(prisma), // Use Prisma adapter
    providers: [
    GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID!,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),
    ],
    });
    export { handler as GET, handler as POST };
  3. Add the required database tables to your schema.prisma file. Next Auth needs these models to store users, sessions, and accounts. You can find the default schema in the Next Auth documentation.

    Here is an example of the default schema:

    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")
    password String?
    image String?
    accounts Account[]
    sessions Session[]
    @@map("users")
    }
    model VerificationToken {
    identifier String
    token String
    expires DateTime
    @@unique([identifier, token])
    @@map("verification_tokens")
    }
  4. After adding the models, run this command to apply the changes to your database:

    Terminal window
    npx prisma migrate dev --name init

This is the most common login method - where users type in their email and password. Next Auth has a built-in “credentials provider” for this.

Never save passwords as plain text in your database. Always hash (scramble) them first using a library like bcrypt. Run npm install bcrypt or bun add bcrypt to install it. Use bcrypt.hash(password, saltRounds) to hash a password and bcrypt.compare(plainPassword, hashedPassword) to check if a password is correct.

Set up the credentials provider in your route.ts file:

// app/api/auth/[...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { prisma } from "../prisma/client"; // Adjust the path to your prisma client
import bcrypt from 'bcrypt';
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma), // Use Prisma adapter
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email', placeholder: 'user@example.com' },
password: { label: 'Password', type: 'password', placeholder: 'Enter your password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null; // Return null if email or password is not provided
}
const user = await prisma.user.findUnique({
where: { email: credentials?.email },
});
if (user && credentials?.password && user.password != null) {
// Check if the typed password matches the hashed password in the database
const isValidPassword = await bcrypt.compare(credentials.password, user.password);
if (isValidPassword) {
return user;
}
}
return null; // Return null if login fails
}
})
],
session: {
strategy: "jwt",
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

Next Auth does not have a built-in signup feature, so you need to create your own API route for it. This route will take the user’s info, hash their password, and save them to the database.

// app/api/auth/signup/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '../prisma/client'; // Adjust the path to your prisma client
import bcrypt from 'bcrypt';
export async function POST(req: NextRequest) {
const { email, password, name } = await req.json();
if (!email || !password || !name) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
// Check if a user with this email already exists
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return NextResponse.json({ error: 'User already exists' }, { status: 400 });
}
// Hash the password before saving it to the database
const hashedPassword = await bcrypt.hash(password, 10);
// Save the new user to the database
const newUser = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
},
});
return NextResponse.json({ message: 'User created successfully', user: newUser }, { status: 201 });
}

Here is the signup form that sends the user’s info to the API route above:

// app/components/SignupForm.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignupForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
const [error, setError] = useState("");
const router = useRouter();
const handelform = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
const response = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, name }),
});
const data = await response.json();
if (data.error) {
setError(data.error);
} else {
router.push("/api/auth/signin"); // Go to the sign-in page after signup
}
};
return (
<form onSubmit={handelform}>
<div>
<label htmlFor="name">Name:</label>
<input type="text" id="name" value={name} onChange={(e) => setName(e.target.value)} />
</div>
<div>
<label htmlFor="email">Email:</label>
<input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</div>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">Sign Up</button>
</form>
);
}