Introduction
Authentication is a crucial aspect of modern web applications. With Next.js 14 and Auth.js (formerly NextAuth.js) v5, implementing a robust authentication in Next.js has become more straightforward than ever. In this guide, we’ll explore how to create a production-ready authentication system that supports both email magic links and Google OAuth.
Key Features We’ll Implement
- Email Magic Link Authentication
- Google OAuth Integration
- Role-Based Access Control (RBAC)
- Account Linking/Unlinking
- Username Management
- Secure Session Handling
- Automatic Database Cleanup
Prerequisites
Before we begin, ensure you have:
- Node.js 14 or newer
- PostgreSQL database
- Google OAuth credentials
- Email service provider (like Amazon SES, SendGrid, etc.)
Setting Up HTTPS for Local Development
For local development with authentication, HTTPS is required. You have two options:
Option 1: Using mkcert (Recommended)
# Install mkcert
brew install mkcert # macOS
sudo apt install mkcert # Ubuntu/Debian
# Setup local certificates
mkcert -install
mkcert localhost
Option 2: Using Next.js Experimental HTTPS
Enable experimental HTTPS in your package.json
:
{
"scripts": {
"dev": "next dev --experimental-https"
}
}
Database Setup
We’re using PostgreSQL with the following schema structure:
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- User roles enum
CREATE TYPE "user_role" AS ENUM ('user', 'admin');
-- Core tables
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255),
email VARCHAR(255) UNIQUE,
role user_role DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(255),
provider_account_id VARCHAR(255),
refresh_token TEXT,
access_token TEXT,
expires_at BIGINT,
token_type VARCHAR(255),
scope VARCHAR(255),
id_token TEXT,
session_state VARCHAR(255)
);
CREATE TABLE verification_tokens (
identifier TEXT,
token TEXT,
expires TIMESTAMP,
PRIMARY KEY (identifier, token)
);
Auth.js Configuration
Here’s the core configuration for Auth.js v5:
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import Email from "next-auth/providers/email";
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
Email({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: parseInt(process.env.EMAIL_SERVER_PORT!),
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD,
},
},
from: process.env.EMAIL_FROM,
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = user.role;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id;
session.user.role = token.role;
}
return session;
},
},
});
Implementing Role-Based Access Control
RBAC can be implemented using middleware:
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isAdmin = req.auth?.user.role === "admin";
// Protect admin routes
if (req.nextUrl.pathname.startsWith("/admin")) {
if (!isAdmin) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
}
// Protect authenticated routes
if (req.nextUrl.pathname.startsWith("/dashboard")) {
if (!isLoggedIn) {
return NextResponse.redirect(new URL("/auth/signin", req.url));
}
}
});
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
Account Management Features
Linking/Unlinking Google Account
async function unlinkGoogleAccount(userId: string) {
await prisma.account.deleteMany({
where: {
userId,
provider: "google",
},
});
}
async function getAccountLinkStatus(userId: string) {
const account = await prisma.account.findFirst({
where: {
userId,
provider: "google",
},
});
return !!account;
}
Automatic Token Cleanup
To keep your database clean, implement a cleanup function for expired verification tokens:
async function cleanupExpiredTokens() {
await prisma.verificationToken.deleteMany({
where: {
expires: {
lt: new Date(),
},
},
});
}
Security Considerations
- Always use HTTPS in production
- Implement proper input validation
- Use secure session configuration
- Regular security audits
- Keep dependencies updated
Conclusion
With Auth.js v5 and Next.js 14, you can create a robust authentication system that handles various authentication methods while maintaining security and user experience. The combination of email magic links and social authentication provides flexibility for users while maintaining security standards.
Remember to check the official documentation for updates and best practices as both Next.js and Auth.js continue to evolve.