Back to Blog
AuthJS v5 with Google OAuth: Production-Ready Setup for Next.js

AuthJS v5 with Google OAuth: Production-Ready Setup for Next.js

January 3, 2026
Stefan Mentović
authjsoauthnext-jssecurityauthentication

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 login
  • access_type: "offline" — Requests a refresh token from Google
  • response_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

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Navigate to APIs & Services → Credentials
  4. Click Create Credentials → OAuth 2.0 Client ID
  5. Select Web application
  6. Add authorized redirect URIs:
    • Development: http://localhost:3000/api/auth/callback/google
    • Production: https://yourdomain.com/api/auth/callback/google

#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

  1. Prefer OAuth over Credentials — Let Google handle password security
  2. Always use HTTPS in production — OAuth requires secure connections
  3. Validate email domains — Restrict sign-ups to specific domains if needed
  4. Implement rate limiting — Especially for the Credentials provider
  5. Keep secrets out of code — Use environment variables exclusively
  6. Rotate secrets periodically — Update AUTH_SECRET on 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.ts in middleware, auth.ts everywhere 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.

Enjoyed this article? Stay updated: