Next.js NextAuth.js
Authentication is a critical aspect of most web applications. In the Next.js ecosystem, NextAuth.js has emerged as the go-to solution for implementing authentication. This comprehensive guide will walk you through implementing authentication in your Next.js projects using NextAuth.js.
What is NextAuth.js?
NextAuth.js is an open-source authentication solution specifically designed for Next.js applications. It provides a simple way to add authentication to your Next.js application with support for:
- OAuth providers (Google, GitHub, Facebook, etc.)
- Email/passwordless authentication
- Credentials-based authentication (username/password)
- JWT and database sessions
- Customizable callbacks and events
Getting Started with NextAuth.js
Step 1: Installation
First, let's install NextAuth.js in your Next.js project:
npm install next-auth
# or
yarn add next-auth
# or
pnpm add next-auth
Step 2: Setting up API Routes
Create a file at pages/api/auth/[...nextauth].js
(for Pages Router) or app/api/auth/[...nextauth]/route.js
(for App Router) to handle authentication.
For Pages Router:
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
export default NextAuth({
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
// Add more providers here
],
// Additional configuration options
});
For App Router:
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
const handler = NextAuth({
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
// Add more providers here
],
// Additional configuration options
});
export { handler as GET, handler as POST };
Step 3: Environment Variables
Create a .env.local
file in your project root and add your provider credentials:
GITHUB_ID=your_github_client_id
GITHUB_SECRET=your_github_client_secret
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your_nextauth_secret
The NEXTAUTH_SECRET
is used to encrypt the NextAuth.js JWT. You can generate a random string for development.
Authentication Providers
NextAuth.js supports many authentication providers. Let's look at some common ones:
OAuth Providers
OAuth providers allow users to log in using their accounts from other services.
import GoogleProvider from 'next-auth/providers/google';
import FacebookProvider from 'next-auth/providers/facebook';
// Inside your NextAuth configuration:
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
FacebookProvider({
clientId: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
}),
],
Email Provider
For passwordless authentication via magic links sent to email:
import EmailProvider from 'next-auth/providers/email';
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,
}),
],
Credentials Provider
For traditional username/password authentication:
import CredentialsProvider from 'next-auth/providers/credentials';
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// Add your own logic here to validate credentials
// Return user object if valid, null if invalid
const user = await findUserByCredentials(credentials);
if (user) {
return user;
} else {
return null;
}
}
})
],
Using Authentication in Your Components
Session Provider
To use authentication in your components, you need to wrap your application with a session provider. Add this to your _app.js
(Pages Router) or layout.js
(App Router):
For Pages Router (_app.js
):
import { SessionProvider } from 'next-auth/react';
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default MyApp;
For App Router (layout.js
):
'use client'
import { SessionProvider } from 'next-auth/react';
export default function RootLayout({ children }) {
return (
<html>
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
);
}
Using Session in Components
Client-Side Authentication (Pages Router)
import { useSession, signIn, signOut } from 'next-auth/react';
export default function Component() {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
if (session) {
return (
<>
<p>Signed in as {session.user.email}</p>
<button onClick={() => signOut()}>Sign out</button>
</>
);
}
return (
<>
<p>Not signed in</p>
<button onClick={() => signIn()}>Sign in</button>
</>
);
}
Client-Side Authentication (App Router)
'use client'
import { useSession, signIn, signOut } from 'next-auth/react';
export default function Component() {
const { data: session, status } = useSession();
if (status === "loading") {
return <p>Loading...</p>;
}
if (session) {
return (
<>
<p>Signed in as {session.user.email}</p>
<button onClick={() => signOut()}>Sign out</button>
</>
);
}
return (
<>
<p>Not signed in</p>
<button onClick={() => signIn()}>Sign in</button>
</>
);
}
Server-Side Authentication (Pages Router)
import { getServerSession } from 'next-auth/next';
import { authOptions } from './api/auth/[...nextauth]';
export default function ServerComponent({ session }) {
return (
<>
{session ? (
<p>Welcome {session.user.name}!</p>
) : (
<p>Please sign in</p>
)}
</>
);
}
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
return {
props: {
session,
},
};
}
Server-Side Authentication (App Router)
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../api/auth/[...nextauth]/route';
export default async function ServerComponent() {
const session = await getServerSession(authOptions);
return (
<>
{session ? (
<p>Welcome {session.user.name}!</p>
) : (
<p>Please sign in</p>
)}
</>
);
}
Protecting Routes
Client-Side Route Protection
You can create a simple HOC (Higher Order Component) to protect routes:
'use client'
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function withAuth(Component) {
return function ProtectedRoute(props) {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === 'loading') return;
if (!session) router.replace('/login');
}, [session, status, router]);
if (status === 'loading') {
return <div>Loading...</div>;
}
return session ? <Component {...props} /> : null;
};
}
// Usage:
// const ProtectedPage = withAuth(YourComponent);
Server-Side Route Protection
For server components or API routes, you can check the session directly:
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../api/auth/[...nextauth]/route';
import { redirect } from 'next/navigation';
export default async function ProtectedPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/api/auth/signin');
}
return <div>Protected content here</div>;
}
Advanced Configuration
Custom Callbacks
NextAuth.js provides several callback functions that let you control the authentication flow:
export default NextAuth({
callbacks: {
// Called after a user signs in
async signIn({ user, account, profile, email, credentials }) {
return true; // Return false to prevent sign-in
},
// Called whenever a session is checked
async session({ session, user, token }) {
// Add custom properties to the session
session.user.role = user.role;
return session;
},
// Called whenever a JWT is created or updated
async jwt({ token, user, account, profile }) {
// Add custom properties to the token
if (user) {
token.role = user.role;
}
return token;
},
},
});
Database Integration
For persistent sessions and user management, integrate with a database:
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default NextAuth({
adapter: PrismaAdapter(prisma),
// Other configuration
});
Real-World Example: Building a Protected Dashboard
Let's create a simple dashboard application with authentication:
1. Auth Configuration
// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcrypt';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const authOptions = {
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email,
},
});
if (!user || !(await bcrypt.compare(credentials.password, user.password))) {
return null;
}
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
};
}
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
}
return token;
},
async session({ session, token }) {
session.user.role = token.role;
return session;
},
},
pages: {
signIn: '/login',
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
2. Login Page
// app/login/page.js
'use client'
import { useState } from 'react';
import { signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
const result = await signIn('credentials', {
redirect: false,
email,
password,
});
if (result?.error) {
setError('Invalid email or password');
} else {
router.replace('/dashboard');
}
};
return (
<div className="login-container">
<h1>Login</h1>
{error && <p className="error">{error}</p>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Login</button>
</form>
<div className="social-login">
<p>Or sign in with:</p>
<button onClick={() => signIn('github')}>GitHub</button>
</div>
</div>
);
}
3. Dashboard Page (Protected)
// app/dashboard/page.js
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../api/auth/[...nextauth]/route';
import { redirect } from 'next/navigation';
import DashboardClient from './dashboard-client';
export default async function Dashboard() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/login');
}
// Fetch user-specific data
// const userData = await fetchUserData(session.user.id);
return (
<div className="dashboard">
<h1>Welcome to your Dashboard, {session.user.name}!</h1>
<p>Role: {session.user.role}</p>
<DashboardClient session={session} />
</div>
);
}
// app/dashboard/dashboard-client.jsx
'use client'
import { signOut } from 'next-auth/react';
export default function DashboardClient({ session }) {
return (
<div className="dashboard-actions">
<h2>Your Account</h2>
<p>Email: {session.user.email}</p>
<button onClick={() => signOut({ callbackUrl: '/' })}>
Sign Out
</button>
</div>
);
}
Common Authentication Patterns
Role-Based Access Control (RBAC)
export default async function AdminPage() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/login');
}
if (session.user.role !== 'ADMIN') {
return (
<div className="unauthorized">
<h1>Unauthorized</h1>
<p>You do not have permission to access this page.</p>
</div>
);
}
return (
<div className="admin-dashboard">
<h1>Admin Dashboard</h1>
{/* Admin-specific content */}
</div>
);
}
Refreshing Sessions
// In your NextAuth configuration
callbacks: {
async session({ session, token }) {
// Always update the session from the token
session.user = token;
// Check if the session needs to be refreshed
const now = Math.floor(Date.now() / 1000);
const shouldRefresh = now > token.exp - 60 * 30; // Refresh 30 minutes before expiry
if (shouldRefresh) {
// Custom logic to refresh the token
const refreshedToken = await refreshAccessToken(token);
return {
...session,
user: refreshedToken,
expires: new Date(refreshedToken.exp * 1000).toISOString(),
};
}
return session;
},
}
Summary
NextAuth.js provides a powerful and flexible authentication solution for Next.js applications. In this guide, we've covered:
- Setting up NextAuth.js in your Next.js application
- Configuring various authentication providers (OAuth, Credentials, Email)
- Using sessions for client-side and server-side authentication
- Protecting routes and implementing role-based access control
- Building a real-world example with login and dashboard pages
- Advanced configurations like callbacks and database integration
By following the steps and examples outlined in this guide, you should be able to implement secure authentication in your Next.js application and customize it to meet your specific requirements.
Additional Resources
Exercises
- Implement a basic authentication system with GitHub OAuth
- Create a protected admin dashboard with role-based access control
- Add email/password registration and login functionality
- Implement passwordless authentication using the Email provider
- Create a user profile page that displays and allows updating user information
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)