You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextAuth凭证登录:数据库生成会话但未返回至客户端

NextAuth凭证登录会话持久化问题:数据库已创建会话但客户端无法获取

我在NextAuth中使用MongoDB Adapter,希望用户通过凭证登录时将会话持久化到数据库。目前适配器已经能在数据库中创建会话,但session异步回调函数块似乎被跳过,导致无法将会话返回给客户端,不知道该怎么解决。

import NextAuth from "next-auth"
import { MongoDBAdapter } from "@next-auth/mongodb-adapter"
import clientPromise from "../auth/lib/mongodb"
import { MongoClient } from 'mongodb';
import bcrypt from 'bcrypt'
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import CredentialsProvider from "next-auth/providers/credentials";

// Modules needed to support key generation, token encryption, and HTTP cookie manipulation 
import { randomUUID } from 'crypto'
// import Cookies from 'cookies'
import { setCookie, getCookie } from 'cookies-next';
import { encode, decode } from 'next-auth/jwt'



const MONGODB_URI = process.env.MONGODB_URI
// next auth starts here

export default async function handler(req, res) {
  const adapter = MongoDBAdapter(clientPromise)
  let userAccount = null
  // Do whatever you want here, before the request is passed down to `NextAuth`
  const generate = {}
  generate.uuid = function () {
    return uuidv4()
  }

  generate.uuidv4 = function () {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    )
  }
// Helper functions to generate unique keys and calculate the expiry dates for session cookies
  const generateSessionToken = () => {
    // Use `randomUUID` if available. (Node 15.6++)
    return randomUUID?.() ?? generate.uuid()
  }

  const fromDate = (time, date = Date.now()) => {
    return new Date(date + time * 1000)
  }

  //    callbacks for the sessions
const callbacks = {
  async redirect({ url, baseUrl }) {
    // Allows relative callback URLs
    if (url.startsWith("/")) return `${baseUrl}${url}`
    // Allows callback URLs on the same origin
    else if (new URL(url).origin === baseUrl) return url
    return baseUrl
  },
  async signIn({ user, account, profile, email, credentials }) {
    console.log("User Signin Start: ", user)
    // Check if this sign in callback is being called in the credentials authentication flow. If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
    if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
      if (user) {
          const sessionToken =  generateSessionToken()
          const sessionMaxAge = 60 * 60 * 24 * 7; //7Days
          const sessionExpiry = fromDate(sessionMaxAge)

          console.log("Session token is: ", sessionToken)
           console.log("sessionExpiry: ", sessionExpiry);
          
          await adapter.createSession({
              sessionToken: sessionToken,
              userId: user.username,
              expires: sessionExpiry
          })
          
          setCookie("next-auth.session-token", sessionToken, {
            expires: sessionExpiry,
            req: req,
            res: res,
          })
          console.log("user Session: ", user)
         
      }   
  }
    return true
  },

  async jwt({ token, user, account, profile, isNewUser }) {
    console.log("JWT callback. Got User: ", user)
    if (typeof user !== typeof undefined) {
      token.user = user;
    }
    return token
  },

  async session({ session, token}) {

    console.log("Session. Got User: ", session, token)
    if (userAccount !== null) {
      console.log("UserAccount Session Generation: ", user)
      session.user = {
        name: userAccount.name,
        email: userAccount.email,
      };
    console.log("Session.user: ", session.user)
    // return session
  }
  if (
    token && typeof token.user !== typeof undefined && (typeof session.user === typeof undefined ||
      (typeof session.user !== typeof undefined && typeof session.user.id === typeof undefined))
  ) {
    session.user = token.user
  }
  if (typeof token !== typeof undefined) {
    session.token = token
  }
  console.log("Session: ", session)
  return session
},
}

  const options = {
      session: {
              strategy: "database",
              maxAge: 7 * 24 * 60 * 60,
              updateAge: 24 * 60 * 60,
              // generateSessionToken: () => {
              //   return randomUUID?.() ?? randomBytes(32).toString("hex")
              // }    
            },
      jwt: {
        // Customize the JWT encode and decode functions to overwrite the default behaviour of storing the JWT token in the session cookie when using credentials providers. Instead we will store the session token reference to the session in the database.
        encode: async (token, secret, maxAge) => {
            if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
                // const cookies = new Cookies(req,res)

                // const cookie = cookies.get('next-auth.session-token')
                const cookie = getCookie("next-auth.session-token", { req: req });

                console.log("pure Cookie: ", cookie);

                if(cookie) return cookie
                else return ''

            }
            // Revert to default behaviour when not in the credentials provider callback flow
            return encode(token, secret, maxAge)
        },
        decode: async (token, secret) => {
            if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
                return null
            }

            // Revert to default behaviour when not in the credentials provider callback flow
            return decode(token, secret)
        }
      },
      debug: process.env.NODE_ENV === "development",
      adapter,
      secret: process.env.NEXTAUTH_SECRET,
      providers: [

        CredentialsProvider({
          name: 'Credentials',
          async authorize(credentials, req) {
            const client = new MongoClient(MONGODB_URI, {},) 
            try {
                await client.connect()
            } catch (e) {
                console.error(e)
            }
            const email = credentials.email
            const password = credentials.password
            //Get all the users
            const users = client.db().collection('users')
            //Find user with the email  
            const user = await users.findOne(
              {
                  $or: [
                      { email: email }, { username: email }
                  ]
              }
            )

          //   //Not found - send error res
            if (!user) {
                client.close();
                throw new Error('No user found with this credential')
            }

            console.log("Authorize User Credentials: ", user)
            //Check hased password with DB password
            const checkPassword = bcrypt.compareSync(password, user.password)
            //Incorrect password - send response
            if (!checkPassword) {
                client.close();
                throw new Error('Password doesnt match')
            }
            //Incorrect password - send response
            if (!checkPassword) {
                client.close();
                throw new Error('Password doesnt match')
            }
            //Else send success response
            userAccount = {
              id: user._id,
              name: user.name,
              email: user.email,
            };
            client.close();
            return userAccount;
        },
          
        }),
        GoogleProvider({
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        allowDangerousEmailAccountLinking: true,
        }),
        FacebookProvider({
            clientId: process.env.FACEBOOK_CLIENT_ID,
            clientSecret: process.env.FACEBOOK_CLIENT_SECRET
        }),
      // ...add more providers here
    ],
    callbacks: callbacks,
    pages: {
      signIn: '/signin',
      signOut: '/signout',
      error: '/auth/error', // Error code passed in query string as ?error=
      verifyRequest: '/auth/verify-request', // (used for check email message)
      // newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
    }, 
    
  }

  return await NextAuth(req, res, options)

}

问题排查与修复方案

1. 修正会话与用户的关联错误

在signIn回调创建会话时,userId使用了user.username,这会导致数据库中会话无法关联到正确的用户。应该替换为用户的唯一ID:

await adapter.createSession({
    sessionToken: sessionToken,
    userId: user.id, // 替换user.username为user.id
    expires: sessionExpiry
})

2. 移除session回调对外部变量的依赖

userAccount是authorize函数内的外部变量,session回调触发时该变量可能已失效,应直接从JWT令牌中获取用户信息:

async session({ session, token}) {
    console.log("Session. Got User: ", session, token)
    // 直接从token中读取用户数据,无需依赖外部变量
    if (token?.user) {
        session.user = token.user
    }
    console.log("Session: ", session)
    return session
},

3. 修正MongoDB ObjectId类型问题

MongoDB的_id是ObjectId类型,需要转换为字符串避免类型不匹配:

userAccount = {
    id: user._id.toString(), // 将ObjectId转为字符串
    name: user.name,
    email: user.email,
};

4. 修复JWT编解码的参数传递

自定义JWT逻辑中,encode和decode函数的参数传递错误,应改为符合NextAuth要求的格式:

jwt: {
    encode: async ({ token, secret, maxAge }) => {
        if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
            const cookie = getCookie("next-auth.session-token", { req: req });
            return cookie || ''
        }
        return encode({ token, secret, maxAge })
    },
    decode: async ({ token, secret }) => {
        if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
            return null
        }
        return decode({ token, secret })
    }
},

修正后的完整代码

import NextAuth from "next-auth"
import { MongoDBAdapter } from "@next-auth/mongodb-adapter"
import clientPromise from "../auth/lib/mongodb"
import { MongoClient } from 'mongodb';
import bcrypt from 'bcrypt'
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import CredentialsProvider from "next-auth/providers/credentials";

import { randomUUID } from 'crypto'
import { setCookie, getCookie } from 'cookies-next';
import { encode, decode } from 'next-auth/jwt'

const MONGODB_URI = process.env.MONGODB_URI

export default async function handler(req, res) {
  const adapter = MongoDBAdapter(clientPromise)
  
  const generateSessionToken = () => {
    return randomUUID?.() ?? ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    )
  }

  const fromDate = (time, date = Date.now()) => {
    return new Date(date + time * 1000)
  }

  const callbacks = {
    async redirect({ url, baseUrl }) {
      if (url.startsWith("/")) return `${baseUrl}${url}`
      else if (new URL(url).origin === baseUrl) return url
      return baseUrl
    },
    async signIn({ user, account, profile, email, credentials }) {
      console.log("User Signin Start: ", user)
      if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
        if (user) {
            const sessionToken = generateSessionToken()
            const sessionMaxAge = 60 * 60 * 24 * 7;
            const sessionExpiry = fromDate(sessionMaxAge)
            
            console.log("Session token is: ", sessionToken)
            console.log("sessionExpiry: ", sessionExpiry);
            
            await adapter.createSession({
                sessionToken: sessionToken,
                userId: user.id,
                expires: sessionExpiry
            })
            
            setCookie("next-auth.session-token", sessionToken, {
              expires: sessionExpiry,
              req: req,
              res: res,
            })
            console.log("user Session: ", user)
        }   
      }
      return true
    },

    async jwt({ token, user, account, profile, isNewUser }) {
      console.log("JWT callback. Got User: ", user)
      if (user) {
        token.user = user;
      }
      return token
    },

    async session({ session, token}) {
      console.log("Session. Got User: ", session, token)
      if (token?.user) {
        session.user = token.user
      }
      console.log("Session: ", session)
      return session
    },
  }

  const options = {
      session: {
              strategy: "database",
              maxAge: 7 * 24 * 60 * 60,
              updateAge: 24 * 60 * 60,
            },
      jwt: {
        encode: async ({ token, secret, maxAge }) => {
            if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
                const cookie = getCookie("next-auth.session-token", { req: req });
                return cookie || ''
            }
            return encode({ token, secret, maxAge })
        },
        decode: async ({ token, secret }) => {
            if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
                return null
            }
            return decode({ token, secret })
        }
      },
      debug: process.env.NODE_ENV === "development",
      adapter,
      secret: process.env.NEXTAUTH_SECRET,
      providers: [
        CredentialsProvider({
          name: 'Credentials',
          async authorize(credentials, req) {
            const client = new MongoClient(MONGODB_URI) 
            try {
                await client.connect()
            } catch (e) {
                console.error(e)
            }
            const email = credentials.email
            const password = credentials.password
            const users = client.db().collection('users')
            const user = await users.findOne(
              {
                  $or: [
                      { email: email }, { username: email }
                  ]
              }
            )

            if (!user) {
                client.close();
                throw new Error('No user found with this credential')
            }

            console.log("Authorize User Credentials: ", user)
            const checkPassword = bcrypt.compareSync(password, user.password)
            if (!checkPassword) {
                client.close();
                throw new Error('Password doesnt match')
            }
            
            const userAccount = {
              id: user._id.toString(),
              name: user.name,
              email: user.email,
            };
            client.close();
            return userAccount;
        },
          
        }),
        GoogleProvider({
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        allowDangerousEmailAccountLinking: true,
        }),
        FacebookProvider({
            clientId: process.env.FACEBOOK_CLIENT_ID,
            clientSecret: process.env.FACEBOOK_CLIENT_SECRET
        }),
    ],
    callbacks: callbacks,
    pages: {
      signIn: '/signin',
      signOut: '/signout',
      error: '/auth/error',
      verifyRequest: '/auth/verify-request',
    }, 
  }

  return await NextAuth(req, res, options)
}

内容的提问来源于stack exchange,提问作者Engr Usman Bello Abdullahi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 06:30:47