All PostsFeb 27, 2026ShipAI Team1 min read

How to Generate a NextAuth Secret (Auth.js v5 Guide)

How to generate, set, and rotate a NextAuth secret for Auth.js v4 and v5—covering the correct commands, environment variable setup, edge cases, and what happens when you get it wrong.

Auth.jsNextAuthAuthenticationNext.js

Contents

The most common auth setup error in Next.js projects is a missing or invalid AUTH_SECRET (formerly NEXTAUTH_SECRET). Auth.js throws a cryptic error or silently falls back to an insecure default, sessions don't persist, and JWT tokens can't be verified. This post covers exactly what the secret is, how to generate a correct one, and how to set it across environments.

What the Secret Does

Auth.js uses the secret to:

  1. Sign and verify JWTs: session tokens are signed with HMAC-SHA256 using this key. Without the correct secret, existing sessions become invalid.
  2. Encrypt session cookies: the HttpOnly session cookie value is encrypted. Changing the secret invalidates all existing sessions.
  3. Verify CSRF tokens: the secret is used in CSRF protection for sign-in/sign-out flows.

The secret must be:

  • At least 32 bytes (256 bits) of random data
  • Base64 encoded (standard or URL-safe)
  • Kept consistent across all instances (pods, workers) in a deployment
  • Different per environment (dev vs staging vs production)

Generating the Secret

openssl rand -base64 32

This generates 32 random bytes (256 bits) and base64 encodes them. Output looks like:

K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=

Copy the entire string, including the trailing = padding if present.

Method 2: Auth.js CLI (v5)

Auth.js v5 ships with a secret generation command:

npx auth secret

This generates a suitable secret and prints it. It can also write directly to your .env.local:

npx auth secret --copy  # copies to clipboard

Method 3: Node.js (if openssl isn't available)

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Method 4: 1Password / Bitwarden Generator

Any password manager with a 256-bit random generator works. Export as base64.

Do not use a passphrase or human-readable string

AUTH_SECRET=my-super-secret-app-key is not a valid secret for production. It's too short and not random. Use a cryptographically random value.


Setting the Secret

Auth.js v5 (current)

In Auth.js v5, the environment variable name changed from NEXTAUTH_SECRET to AUTH_SECRET.

# .env.local (development)
AUTH_SECRET=K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
// auth.ts (Auth.js v5 configuration)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
  // secret is automatically read from AUTH_SECRET env var
  // do NOT hardcode it here
});

Auth.js v5 reads AUTH_SECRET automatically. You do not need to pass secret: in the config object—and you should not, since hardcoding it in source code is a security issue.

Auth.js v4 / NextAuth v4 (legacy)

# .env.local (NextAuth v4)
NEXTAUTH_SECRET=K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=
NEXTAUTH_URL=http://localhost:3000
// pages/api/auth/[...nextauth].ts (NextAuth v4)
import NextAuth from "next-auth";

export default NextAuth({
  providers: [...],
  secret: process.env.NEXTAUTH_SECRET, // explicit in v4
});

Upgrading from v4 to v5?

Auth.js v5 accepts both NEXTAUTH_SECRET and AUTH_SECRET during the transition. Once fully migrated to v5, use only AUTH_SECRET. The NEXTAUTH_URL variable is no longer required in v5 on most platforms.


Setting the Secret in Production

Vercel

vercel env add AUTH_SECRET production
# paste your secret when prompted

Or in the Vercel dashboard: Project Settings → Environment Variables → Add Variable.

Do not commit the secret to source control. Vercel injects it at build time and runtime.

Fly.io

fly secrets set AUTH_SECRET="K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols="

Fly stores secrets encrypted and injects them as environment variables at runtime.

Railway

In the Railway dashboard: Variables → New Variable → AUTH_SECRET.

Or via CLI:

railway variables set AUTH_SECRET="K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols="

Docker / Self-Hosted

Pass via environment variable in your Docker run command or compose file:

# docker-compose.yml
services:
  app:
    image: your-app
    environment:
      AUTH_SECRET: ${AUTH_SECRET}  # from .env file or shell environment

Never hardcode the secret in the Dockerfile or docker-compose.yml if that file is committed to source control.


Environment Setup for Multiple Environments

Use a different secret per environment. If your production secret leaks, it shouldn't compromise your development sessions (though dev sessions don't matter much), and you can rotate production without affecting other environments.

# .env.local (development — this file should be in .gitignore)
AUTH_SECRET=dev-secret-different-from-production

# .env.production (if you use this file — also in .gitignore)
AUTH_SECRET=prod-secret-never-committed-to-git

Your .gitignore should contain:

.env
.env.local
.env.production
.env*.local

What Happens When the Secret Is Wrong

Missing secret in development: Auth.js v5 generates a random secret per process in development mode. Sessions work but are not persistent across restarts. This is intentional for local dev but will catch you off guard if you don't realize it.

Missing secret in production: Auth.js v5 throws an error on startup:

[auth][error] MissingSecret: Please define a `secret`. https://errors.authjs.dev#missingsecret

Mismatched secret across instances: If you have multiple server instances (e.g., two containers in a load balancer), they must share the same AUTH_SECRET. If they differ, sessions signed by instance A can't be verified by instance B. Users get logged out randomly.

Rotated secret with active sessions: When you change AUTH_SECRET in production, all existing sessions become invalid. Users are logged out. This is expected—plan for it if you need to rotate.

Session invalidation on secret rotation

Rotating AUTH_SECRET in production logs out all users immediately. If your user base is large, do this during low-traffic hours and communicate the forced logout in advance if sessions are business-critical (e.g., long-lived admin sessions).


Verifying the Secret Is Working

After setting the secret, verify Auth.js is picking it up correctly:

// Temporary debug route (remove after verifying)
// app/api/debug-auth/route.ts
import { auth } from "@/auth";

export async function GET() {
  const session = await auth();
  return Response.json({
    hasSession: !!session,
    userId: session?.user?.id ?? null,
  });
}

If the session persists across a server restart, the secret is set correctly. If the user is logged out on every restart, the secret is being regenerated (missing from environment).


Full Auth.js v5 Setup with Secret

Here's a complete, minimal Auth.js v5 setup with the secret configured correctly:

// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/db";

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID!,
      clientSecret: process.env.AUTH_GITHUB_SECRET!,
    }),
  ],
  // AUTH_SECRET is picked up automatically from environment
  // No need to pass secret: here
  callbacks: {
    session({ session, user }) {
      session.user.id = user.id;
      return session;
    },
  },
});
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
# Required environment variables
AUTH_SECRET=your-generated-secret-here
AUTH_GITHUB_ID=your-github-oauth-app-id
AUTH_GITHUB_SECRET=your-github-oauth-app-secret

Quick Reference

CommandPurpose
openssl rand -base64 32Generate a 32-byte secret (recommended)
npx auth secretAuth.js v5 CLI generation
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Node.js generation
vercel env add AUTH_SECRET productionSet on Vercel
fly secrets set AUTH_SECRET="..."Set on Fly.io
railway variables set AUTH_SECRET="..."Set on Railway

Next Steps

Ready to ship?

Stop rebuilding auth and billing from scratch.

ShipAI.today gives you a production-ready Next.js foundation. Every module pre-integrated — spend your time building your product, not plumbing.

Full source code · Commercial license · Lifetime updates