
AuthJS v5 with Google OAuth: Production-Ready Setup for Next.js
Master the split config pattern, edge runtime compatibility, and secure OAuth implementation for your production SaaS with practical code examples.
AuthJS v5 with Google OAuth: Production-Ready Setup for Next.js
Your Next.js application needs authentication. You want users to sign in with Google, but also support email/password for those who prefer it. Sounds simple enough — until you hit the edge runtime, discover your middleware crashes, and realize Google isn't sending refresh tokens.
AuthJS v5 (the evolution of NextAuth.js) solves these problems elegantly, but requires understanding a few key patterns. This guide walks through setting up production-ready authentication with Google OAuth, the split configuration pattern for edge compatibility, and how to combine OAuth with traditional credentials.
Understanding Your Authentication Options
Before diving into code, let's clarify the two main authentication approaches in AuthJS.
OAuth Providers (Like Google)
OAuth providers delegate authentication to trusted third parties like Google, GitHub, or Microsoft. Instead of managing passwords directly, your application redirects users to the provider's login page. After successful authentication, the provider returns a secure token confirming the user's identity. The benefits are significant:
- Battle-tested security — Google handles password storage, brute-force protection, and abuse detection
- User convenience — No new password to remember
- Verified emails — Google confirms the email address belongs to the user
- Reduced liability — You never see or store user passwords
The Credentials Provider
The Credentials provider handles traditional username/password authentication. Unlike OAuth, you are responsible for:
- Storing passwords securely (bcrypt, argon2)
- Implementing rate limiting and brute-force protection
- Managing password resets and email verification
- Handling all security considerations OAuth providers handle for free
When to use Credentials: The Credentials provider is designed for integrating with existing authentication systems — legacy databases, enterprise LDAP, or custom identity solutions. If you're building a new application, OAuth providers offer significantly better security with less effort.
This guide shows both approaches, but Google OAuth should be your primary authentication method for new projects.
Installation and Initial Setup
Start by installing AuthJS:
npm install next-auth@beta
Generate a secret for signing tokens:
npx auth secret
This creates an AUTH_SECRET in your .env.local file automatically.
The Split Configuration Pattern
Here's where AuthJS v5 differs from v4. Next.js middleware runs in the Edge Runtime — a lightweight JavaScript environment that lacks Node.js features like TCP sockets. This means your middleware can't directly query databases or use libraries like bcrypt.
The solution is splitting your configuration into two files:
auth/
├── auth.config.ts # Edge-compatible (for middleware)
└── auth.ts # Full config (for API routes)
Step 1: Edge-Compatible Configuration
The edge config contains only what runs safely without Node.js:
// auth.config.ts
import type { NextAuthConfig } from 'next-auth';
import Google from 'next-auth/providers/google';
export const authConfig: NextAuthConfig = {
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
},
},
}),
],
pages: {
signIn: '/login',
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isProtected = nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !isLoggedIn) {
return Response.redirect(new URL('/login', nextUrl));
}
return true;
},
},
};
Three settings in the Google provider are critical for production:
prompt: "consent"— Forces the consent screen on every loginaccess_type: "offline"— Requests a refresh token from Googleresponse_type: "code"— Uses the authorization code flow
Why force consent? Google only sends refresh tokens on the first login. Without
prompt: "consent", returning users won't get new refresh tokens, and their sessions will expire without renewal.
Step 2: Full Configuration with Database Integration
The main configuration extends the edge config and adds database operations. These examples use PostgreSQL with Drizzle ORM, but the patterns apply to any database setup (Prisma, raw SQL, etc.):
// auth.ts
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';
import bcrypt from 'bcryptjs';
import { authConfig } from './auth.config';
// Drizzle ORM client connected to PostgreSQL
import { db } from '@/lib/db';
import { users } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
},
},
}),
Credentials({
name: 'credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
// Find user in PostgreSQL using Drizzle ORM
const user = await db.query.users.findFirst({
where: eq(users.email, credentials.email as string),
});
if (!user || !user.hashedPassword) {
return null;
}
// Verify password with bcrypt
const isValid = await bcrypt.compare(credentials.password as string, user.hashedPassword);
if (!isValid) {
return null;
}
// Return user object (never include password)
return {
id: user.id,
email: user.email,
name: user.name,
};
},
}),
],
callbacks: {
async signIn({ user, account, profile }) {
// Handle Google sign-in: create or update user in PostgreSQL
if (account?.provider === 'google' && profile?.email) {
const existingUser = await db.query.users.findFirst({
where: eq(users.email, profile.email),
});
if (!existingUser) {
// Create new user from Google profile using Drizzle
await db.insert(users).values({
email: profile.email,
name: profile.name ?? null,
image: profile.picture ?? null,
emailVerified: new Date(),
});
}
}
return true;
},
async jwt({ token, user, account }) {
// Initial sign-in: add user data to token
if (user) {
token.id = user.id;
}
// Store provider for later use
if (account) {
token.provider = account.provider;
}
return token;
},
async session({ session, token }) {
// Pass user ID to session
if (token.id) {
session.user.id = token.id as string;
}
return session;
},
},
session: {
strategy: 'jwt',
},
});
Step 3: API Route Handler
Create the authentication API route:
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;
Step 4: Edge-Compatible Middleware
The middleware imports from the edge config only:
// middleware.ts
import NextAuth from 'next-auth';
import { authConfig } from '@/auth.config';
export default NextAuth(authConfig).auth;
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
This middleware runs on the edge and uses the authorized callback from auth.config.ts to protect routes.
Environment Variables
Set up your environment with these variables:
# .env.local
# Required: Auth.js secret (generated by `npx auth secret`)
AUTH_SECRET="your-generated-secret"
# Google OAuth (from Google Cloud Console)
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"
# Your application URL (optional in most deployments)
AUTH_URL="http://localhost:3000"
Getting Google OAuth Credentials
- Go to Google Cloud Console
- Create a new project or select an existing one
- Navigate to APIs & Services → Credentials
- Click Create Credentials → OAuth 2.0 Client ID
- Select Web application
- Add authorized redirect URIs:
- Development:
http://localhost:3000/api/auth/callback/google - Production:
https://yourdomain.com/api/auth/callback/google
- Development:
Using Authentication in Your App
Server Components
// app/dashboard/page.tsx
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect('/login');
}
return (
<div>
<h1>Welcome, {session.user?.name}</h1>
<p>Email: {session.user?.email}</p>
</div>
);
}
Client Components
'use client';
import { useSession } from 'next-auth/react';
export function UserProfile() {
const { data: session, status } = useSession();
if (status === 'loading') {
return <div>Loading...</div>;
}
if (!session) {
return <div>Not signed in</div>;
}
return <div>Signed in as {session.user?.email}</div>;
}
Sign In and Sign Out
'use client';
import { signIn, signOut } from 'next-auth/react';
export function AuthButtons() {
return (
<div>
<button onClick={() => signIn('google')}>Sign in with Google</button>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}
Type Safety for Custom Session Data
Extend the default types to include custom properties:
// types/next-auth.d.ts
import { DefaultSession } from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
} & DefaultSession['user'];
}
}
Common Issues and Solutions
"Google isn't sending refresh tokens"
Ensure your Google provider includes these authorization params:
Google({
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
},
},
});
Without access_type: "offline", Google won't send refresh tokens. Without prompt: "consent", returning users won't trigger a new token grant.
"Middleware crashes with database errors"
Your middleware is importing from auth.ts instead of auth.config.ts. The edge runtime can't handle database connections or bcrypt. Always import from the edge-compatible config in middleware.
"UNTRUST_HOST error in production"
Add trustHost: true to your auth config when deploying behind reverse proxies:
export const authConfig: NextAuthConfig = {
trustHost: true,
// ... rest of config
};
"Session is null after sign-in"
Check that your callback URL matches exactly what's configured in Google Cloud Console. Mismatched URLs cause silent failures.
Security Best Practices
- Prefer OAuth over Credentials — Let Google handle password security
- Always use HTTPS in production — OAuth requires secure connections
- Validate email domains — Restrict sign-ups to specific domains if needed
- Implement rate limiting — Especially for the Credentials provider
- Keep secrets out of code — Use environment variables exclusively
- Rotate secrets periodically — Update
AUTH_SECRETon a schedule
Key Takeaways
- Use the split configuration pattern to support edge middleware
- Request offline access from Google to get refresh tokens
- The Credentials provider is for integrating existing auth systems, not building new ones
- Import from
auth.config.tsin middleware,auth.tseverywhere else - OAuth providers handle security concerns that are difficult to implement correctly yourself
AuthJS v5 provides a robust foundation for authentication. The split config pattern takes a few extra files to set up, but gives you edge compatibility without sacrificing functionality.
Ready to implement secure authentication? Check out our development services or get in touch to discuss your project.