[{"data":1,"prerenderedAt":342},["ShallowReactive",2],{"blog-guides/nextauth":3},{"id":4,"title":5,"body":6,"category":321,"date":322,"dateModified":323,"description":324,"draft":325,"extension":326,"faq":327,"featured":325,"headerVariant":328,"image":327,"keywords":329,"meta":330,"navigation":331,"ogDescription":332,"ogTitle":327,"path":333,"readTime":327,"schemaOrg":334,"schemaType":335,"seo":336,"sitemap":337,"stem":338,"tags":339,"twitterCard":340,"__hash__":341},"blog/blog/guides/nextauth.md","NextAuth.js Security Guide for Vibe Coders",{"type":7,"value":8,"toc":298},"minimark",[9,13,17,40,45,48,52,62,67,77,81,87,91,94,98,104,108,114,118,121,127,131,137,141,144,150,160,164,167,173,177,217,245,249,252,272,279],[10,11,5],"h1",{"id":12},"nextauthjs-security-guide-for-vibe-coders",[14,15,16],"p",{},"Published on January 23, 2026 - 12 min read",[18,19,20],"tldr",{},[14,21,22,23,27,28,31,32,35,36,39],{},"NextAuth.js handles a lot of security automatically, but configuration mistakes can expose vulnerabilities. Always set a strong ",[24,25,26],"code",{},"NEXTAUTH_SECRET",", configure proper callback URLs (no wildcards), use the ",[24,29,30],{},"signIn"," and ",[24,33,34],{},"jwt"," callbacks to control access, and protect API routes with ",[24,37,38],{},"getServerSession",". Database sessions are more secure than JWTs for sensitive applications.",[41,42,44],"h2",{"id":43},"why-nextauthjs-security-matters-for-vibe-coding","Why NextAuth.js Security Matters for Vibe Coding",[14,46,47],{},"NextAuth.js (now Auth.js) is the most popular authentication library for Next.js. When AI tools generate NextAuth configuration, they often produce working auth flows but miss important security hardening. The defaults are good, but production apps need careful callback configuration and session validation.",[41,49,51],{"id":50},"essential-environment-variables","Essential Environment Variables",[53,54,59],"pre",{"className":55,"code":57,"language":58},[56],"language-text","# .env.local (never commit)\nNEXTAUTH_URL=https://yourdomain.com\nNEXTAUTH_SECRET=your-32-character-or-longer-secret-here\n\n# OAuth provider secrets\nGITHUB_CLIENT_ID=your-github-client-id\nGITHUB_CLIENT_SECRET=your-github-client-secret\n\nGOOGLE_CLIENT_ID=your-google-client-id\nGOOGLE_CLIENT_SECRET=your-google-client-secret\n","text",[24,60,57],{"__ignoreMap":61},"",[63,64,66],"h3",{"id":65},"nextauth_secret-is-critical","NEXTAUTH_SECRET is Critical",[14,68,69,70,72,73,76],{},"The ",[24,71,26],{}," is used to encrypt JWTs and session cookies. In production, NextAuth will throw an error if it's not set. Generate a strong secret with: ",[24,74,75],{},"openssl rand -base64 32",". Never use a weak or guessable secret.",[41,78,80],{"id":79},"secure-configuration","Secure Configuration",[53,82,85],{"className":83,"code":84,"language":58},[56],"// app/api/auth/[...nextauth]/route.ts\nimport NextAuth from \"next-auth\";\nimport GithubProvider from \"next-auth/providers/github\";\nimport { PrismaAdapter } from \"@auth/prisma-adapter\";\nimport { prisma } from \"@/lib/prisma\";\n\nconst handler = NextAuth({\n  adapter: PrismaAdapter(prisma), // Use database sessions for better security\n  providers: [\n    GithubProvider({\n      clientId: process.env.GITHUB_CLIENT_ID!,\n      clientSecret: process.env.GITHUB_CLIENT_SECRET!,\n    }),\n  ],\n  session: {\n    strategy: \"database\", // More secure than JWT\n    maxAge: 30 * 24 * 60 * 60, // 30 days\n    updateAge: 24 * 60 * 60, // 24 hours\n  },\n  cookies: {\n    sessionToken: {\n      name: \"__Secure-next-auth.session-token\",\n      options: {\n        httpOnly: true,\n        sameSite: \"lax\",\n        path: \"/\",\n        secure: true, // Always true in production\n      },\n    },\n  },\n  callbacks: {\n    async signIn({ user, account, profile }) {\n      // Control who can sign in\n      // Return false to deny access\n      return true;\n    },\n    async session({ session, user }) {\n      // Add user ID to session\n      session.user.id = user.id;\n      return session;\n    },\n  },\n});\n\nexport { handler as GET, handler as POST };\n",[24,86,84],{"__ignoreMap":61},[41,88,90],{"id":89},"callback-security","Callback Security",[14,92,93],{},"Callbacks control authentication flow. Use them to enforce security rules:",[63,95,97],{"id":96},"signin-callback","SignIn Callback",[53,99,102],{"className":100,"code":101,"language":58},[56],"callbacks: {\n  async signIn({ user, account, profile, email, credentials }) {\n    // Restrict to specific email domains\n    if (user.email && !user.email.endsWith(\"@yourcompany.com\")) {\n      return false; // Deny sign in\n    }\n\n    // Block specific users\n    const blockedUsers = await getBlockedUsers();\n    if (blockedUsers.includes(user.email)) {\n      return false;\n    }\n\n    // Require email verification for credentials\n    if (account?.provider === \"credentials\") {\n      const dbUser = await prisma.user.findUnique({\n        where: { email: user.email },\n      });\n      if (!dbUser?.emailVerified) {\n        return \"/auth/verify-email\"; // Redirect to verification\n      }\n    }\n\n    return true;\n  },\n}\n",[24,103,101],{"__ignoreMap":61},[63,105,107],{"id":106},"jwt-callback-if-using-jwt-strategy","JWT Callback (if using JWT strategy)",[53,109,112],{"className":110,"code":111,"language":58},[56],"callbacks: {\n  async jwt({ token, user, account }) {\n    if (user) {\n      // Add user data to token on sign in\n      token.userId = user.id;\n      token.role = user.role;\n    }\n\n    // Check if token should be invalidated\n    // (e.g., user was banned, password changed)\n    if (token.userId) {\n      const dbUser = await prisma.user.findUnique({\n        where: { id: token.userId },\n        select: { tokenVersion: true, banned: true },\n      });\n\n      if (!dbUser || dbUser.banned) {\n        // Return empty token to force re-auth\n        return {};\n      }\n\n      // Check if token version changed (password reset, etc.)\n      if (dbUser.tokenVersion !== token.tokenVersion) {\n        return {};\n      }\n    }\n\n    return token;\n  },\n}\n",[24,113,111],{"__ignoreMap":61},[41,115,117],{"id":116},"protecting-api-routes","Protecting API Routes",[14,119,120],{},"Always verify session server-side before processing requests:",[53,122,125],{"className":123,"code":124,"language":58},[56],"// app/api/protected/route.ts\nimport { getServerSession } from \"next-auth\";\nimport { authOptions } from \"@/lib/auth\";\n\nexport async function GET(request: Request) {\n  const session = await getServerSession(authOptions);\n\n  if (!session) {\n    return new Response(\"Unauthorized\", { status: 401 });\n  }\n\n  // User is authenticated\n  return Response.json({ userId: session.user.id });\n}\n\nexport async function POST(request: Request) {\n  const session = await getServerSession(authOptions);\n\n  if (!session) {\n    return new Response(\"Unauthorized\", { status: 401 });\n  }\n\n  // Check authorization (e.g., admin role)\n  if (session.user.role !== \"admin\") {\n    return new Response(\"Forbidden\", { status: 403 });\n  }\n\n  // Process admin-only request...\n}\n",[24,126,124],{"__ignoreMap":61},[63,128,130],{"id":129},"middleware-protection","Middleware Protection",[53,132,135],{"className":133,"code":134,"language":58},[56],"// middleware.ts\nimport { withAuth } from \"next-auth/middleware\";\n\nexport default withAuth({\n  callbacks: {\n    authorized: ({ token, req }) => {\n      // Protect all routes under /dashboard\n      if (req.nextUrl.pathname.startsWith(\"/dashboard\")) {\n        return !!token;\n      }\n\n      // Protect admin routes\n      if (req.nextUrl.pathname.startsWith(\"/admin\")) {\n        return token?.role === \"admin\";\n      }\n\n      return true;\n    },\n  },\n});\n\nexport const config = {\n  matcher: [\"/dashboard/:path*\", \"/admin/:path*\", \"/api/:path*\"],\n};\n",[24,136,134],{"__ignoreMap":61},[41,138,140],{"id":139},"csrf-protection","CSRF Protection",[14,142,143],{},"NextAuth includes CSRF protection by default. Don't disable it:",[53,145,148],{"className":146,"code":147,"language":58},[56],"// The csrfToken is automatically included in sign in forms\n// For custom forms, include the CSRF token:\n\nimport { getCsrfToken } from \"next-auth/react\";\n\nexport default function SignIn() {\n  const csrfToken = await getCsrfToken();\n\n  return (\n    \u003Cform method=\"post\" action=\"/api/auth/signin/email\">\n      \u003Cinput name=\"csrfToken\" type=\"hidden\" value={csrfToken} />\n      \u003Cinput name=\"email\" type=\"email\" />\n      \u003Cbutton type=\"submit\">Sign in\u003C/button>\n    \u003C/form>\n  );\n}\n",[24,149,147],{"__ignoreMap":61},[151,152,153,157],"warning-box",{},[63,154,156],{"id":155},"never-disable-csrf-protection","Never Disable CSRF Protection",[14,158,159],{},"Some AI-generated code disables CSRF protection to \"fix\" issues. This exposes your app to cross-site request forgery attacks. If you're having CSRF issues, the problem is usually with form submission or CORS, not the protection itself.",[41,161,163],{"id":162},"credentials-provider-security","Credentials Provider Security",[14,165,166],{},"If using email/password authentication, handle it carefully:",[53,168,171],{"className":169,"code":170,"language":58},[56],"import CredentialsProvider from \"next-auth/providers/credentials\";\nimport bcrypt from \"bcrypt\";\n\nCredentialsProvider({\n  name: \"Credentials\",\n  credentials: {\n    email: { label: \"Email\", type: \"email\" },\n    password: { label: \"Password\", type: \"password\" },\n  },\n  async authorize(credentials) {\n    if (!credentials?.email || !credentials?.password) {\n      throw new Error(\"Email and password required\");\n    }\n\n    const user = await prisma.user.findUnique({\n      where: { email: credentials.email },\n    });\n\n    if (!user || !user.passwordHash) {\n      // Use same error for both cases to prevent enumeration\n      throw new Error(\"Invalid credentials\");\n    }\n\n    const isValid = await bcrypt.compare(\n      credentials.password,\n      user.passwordHash\n    );\n\n    if (!isValid) {\n      // Log failed attempt for rate limiting\n      await logFailedAttempt(credentials.email);\n      throw new Error(\"Invalid credentials\");\n    }\n\n    // Check account status\n    if (user.banned) {\n      throw new Error(\"Account suspended\");\n    }\n\n    if (!user.emailVerified) {\n      throw new Error(\"Please verify your email\");\n    }\n\n    return {\n      id: user.id,\n      email: user.email,\n      name: user.name,\n    };\n  },\n})\n",[24,172,170],{"__ignoreMap":61},[63,174,176],{"id":175},"nextauthjs-security-checklist","NextAuth.js Security Checklist",[178,179,180,184,187,190,193,196,199,202,205,208,211,214],"ul",{},[181,182,183],"li",{},"Strong NEXTAUTH_SECRET set (32+ characters)",[181,185,186],{},"NEXTAUTH_URL matches your production domain",[181,188,189],{},"Database sessions used for sensitive apps",[181,191,192],{},"Session cookies are HTTP-only and Secure",[181,194,195],{},"signIn callback validates allowed users",[181,197,198],{},"API routes check session with getServerSession",[181,200,201],{},"Middleware protects sensitive routes",[181,203,204],{},"CSRF protection enabled (default)",[181,206,207],{},"OAuth callback URLs are exact matches",[181,209,210],{},"Credentials provider uses bcrypt and prevents enumeration",[181,212,213],{},"Failed login attempts are logged/rate limited",[181,215,216],{},"Session invalidation implemented for password changes",[218,219,220,227,233,239],"faq-section",{},[221,222,224],"faq-item",{"question":223},"Should I use JWT or database sessions?",[14,225,226],{},"Database sessions are more secure because you can instantly invalidate them (e.g., when a user changes their password or is banned). JWTs are stateless and faster but can't be revoked until they expire. Use database sessions for apps with sensitive data.",[221,228,230],{"question":229},"Why is my CSRF token invalid?",[14,231,232],{},"Common causes: form not including csrfToken, CORS issues with custom domains, or using HTTP instead of HTTPS in production. Check that your NEXTAUTH_URL matches your actual domain and uses HTTPS.",[221,234,236],{"question":235},"How do I force users to re-authenticate?",[14,237,238],{},"With database sessions, delete the session from the database. With JWTs, use a token version stored in your database. Increment it when you need to invalidate tokens, and check it in the jwt callback.",[221,240,242],{"question":241},"Is it safe to expose user IDs in the session?",[14,243,244],{},"Yes, if you're using proper authorization checks. The user ID helps identify who's making requests. Just ensure you're checking that the user has permission to access the resources they're requesting, not just that they're logged in.",[41,246,248],{"id":247},"what-checkyourvibe-detects","What CheckYourVibe Detects",[14,250,251],{},"When scanning your NextAuth.js project, CheckYourVibe identifies:",[178,253,254,257,260,263,266,269],{},[181,255,256],{},"Missing or weak NEXTAUTH_SECRET",[181,258,259],{},"OAuth secrets in client-side code",[181,261,262],{},"API routes without session verification",[181,264,265],{},"Disabled CSRF protection",[181,267,268],{},"Credentials provider without password hashing",[181,270,271],{},"Missing rate limiting on login endpoints",[14,273,274,275,278],{},"Run ",[24,276,277],{},"npx checkyourvibe scan"," to catch these issues before they reach production.",[280,281,282,288,293],"related-articles",{},[283,284],"related-card",{"description":285,"href":286,"title":287},"Complete security guide for Firebase. Master Firestore security rules, secure authentication flows, and protect your Fir","/blog/guides/firebase","Firebase Security Guide: Firestore Rules and Authentication",[283,289],{"description":290,"href":291,"title":292},"Learn how to secure your Fly.io deployments when vibe coding. Cover secrets management, private networking, machine secu","/blog/guides/fly-io","Fly.io Security Guide for Vibe Coders",[283,294],{"description":295,"href":296,"title":297},"Security guide for Framer users. Learn about site security, custom code safety, and protecting your Framer websites from","/blog/guides/framer","Framer Security Guide: Design-to-Code Protection",{"title":61,"searchDepth":299,"depth":299,"links":300},2,[301,302,306,307,311,314,317,320],{"id":43,"depth":299,"text":44},{"id":50,"depth":299,"text":51,"children":303},[304],{"id":65,"depth":305,"text":66},3,{"id":79,"depth":299,"text":80},{"id":89,"depth":299,"text":90,"children":308},[309,310],{"id":96,"depth":305,"text":97},{"id":106,"depth":305,"text":107},{"id":116,"depth":299,"text":117,"children":312},[313],{"id":129,"depth":305,"text":130},{"id":139,"depth":299,"text":140,"children":315},[316],{"id":155,"depth":305,"text":156},{"id":162,"depth":299,"text":163,"children":318},[319],{"id":175,"depth":305,"text":176},{"id":247,"depth":299,"text":248},"guides","2026-01-23","2026-02-09","Secure your NextAuth.js authentication when vibe coding. Learn session security, callback protection, CSRF prevention, and common configuration mistakes to avoid.",false,"md",null,"blue","NextAuth.js security, vibe coding authentication, Next.js auth, session security, OAuth security, CSRF protection",{},true,"Secure your NextAuth.js authentication with proper session handling, callbacks, and CSRF protection.","/blog/guides/nextauth","[object Object]","TechArticle",{"title":5,"description":324},{"loc":333},"blog/guides/nextauth",[],"summary_large_image","86PYx-RKeMr1TNKWj4g9bZ9XWDFY-Ur7HXsSxrW8fG8",1775843918616]