Сессия неожиданно удалена на клиенте с использованием NextJS и NextAuth

Вопрос или проблема

Я использую NextJS и Next 14.2.3, а также NextAuth 4.24.7 для создания сайта онлайн-курсов на сервере Vercel.
У меня большая проблема с сессией. Она часто пропадает (после 5 – 45 минут, иногда сразу после входа в систему), хотя я установил maxAge на 1 месяц, обратите внимание, что срок действия сессии правильно отображается в инструментах разработчика.
Эта проблема никогда не возникает на localhost, только в продакшене😥

/src/app/api/auth/[…nextauth]/authOption.ts

import { connectDatabase } from '@/config/database'
import UserModel, { IUser } from '@/models/UserModel'
import bcrypt from 'bcrypt'

// Модели: Пользователь
import '@/models/UserModel'

// Провайдеры
import CredentialsProvider from 'next-auth/providers/credentials'
import GitHubProvider from 'next-auth/providers/github'
import GoogleProvider from 'next-auth/providers/google'
import { SessionStrategy } from 'next-auth'

const authOptions = {
  secret: process.env.NEXTAUTH_SECRET!,
  session: {
    strategy: 'jwt' as SessionStrategy,
    maxAge: 30 * 24 * 60 * 60, // 30 дней
  },
  // debug: process.env.NODE_ENV === 'development',
  providers: [
    // GOOGLE
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    }),

    // GITHUB
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),

    // УЧЕТНЫЕ ДАННЫЕ
    CredentialsProvider({
      name: 'Данные для входа',
      credentials: {
        usernameOrEmail: { label: 'Имя пользователя или Email', type: 'text' },
        password: { label: 'Пароль', type: 'password' },
      },
      async authorize(credentials: Record<'usernameOrEmail' | 'password', string> | undefined) {
        console.log('- Учетные данные -')

        // соединение с базой данных
        await connectDatabase()

        // проверяем, не пустые ли учетные данные
        if (!credentials?.usernameOrEmail || !credentials?.password) {
          return null
        }

        // получаем данные из учетных данных
        const { usernameOrEmail, password } = credentials

        // ищем пользователя в базе данных
        const user: any = await UserModel.findOne({
          $or: [{ email: usernameOrEmail.toLowerCase() }, { username: usernameOrEmail }],
        }).lean()

        // проверяем, существует ли пользователь в базе данных
        if (!user) {
          throw new Error('Email или пароль неверны!')
        }

        // проверяем, является ли пользователь локальным
        if (user.authType !== 'local') {
          throw new Error('Эта учетная запись подтверждена ' + user.authType)
        }

        // проверяем пароль
        const isValidPassword = await bcrypt.compare(password, user.password)
        if (!isValidPassword) {
          // выкидываем ошибку в колбэк
          throw new Error('Email или пароль неверны!')
        }

        const { avatar: image, ...otherDetails } = user

        // возвращаем в колбэк сессии
        return {
          ...otherDetails,
          image,
          name: user.firstName + ' ' + user.lastName,
        }
      },
    }),

    // ...добавьте провайдеров здесь
  ],
  callbacks: {
    async jwt({ token, user, trigger, session }: any) {
      console.log('- JWT -', { token, user, trigger, session })

      // Новый вход
      if (user) {
        const userDB: IUser | null = await UserModel.findOne({
          email: user.email,
        }).lean()

        if (userDB) {
          const { password, ...userDBWithoutPassword } = userDB
          token = { ...token, ...userDBWithoutPassword }
        }
      }

      if (trigger === 'update' && token._id) {
        console.log('- Обновить токен -')
        const userDB: IUser | null = await UserModel.findById(token._id).lean()
        if (userDB) {
          // исключаем пароль
          const { password, ...userDBWithoutPassword } = userDB
          return { ...token, ...userDBWithoutPassword }
        }
      }

      return token
    },

    async session({ session, token }: any) {
      console.log('- Сессия -')

      session.user = token
      return session
    },

    async signIn({ user, account, profile }: any) {
      console.log('- Вход -')

      try {
        // соединение с базой данных
        await connectDatabase()

        if (account && account.provider != 'credentials') {
          if (!user || !profile) {
            return false
          }

          // получаем данные для аутентификации
          const email = user.email
          const avatar = user.image
          let firstName: string = ''
          let lastName: string = ''

          if (account.provider === 'google') {
            firstName = profile.given_name
            lastName = profile.family_name
          } else if (account.provider === 'github') {
            firstName = profile.name
            lastName=""
          }

          // ищем пользователя в базе данных для проверки существования
          const existingUser: any = await UserModel.findOne({ email }).lean()

          // проверяем, существует ли пользователь
          if (existingUser) {
            return true
          }

          // создаем нового пользователя с социальной информацией (проверенный email)
          await UserModel.create({
            email,
            avatar,
            firstName,
            lastName,
            authType: account.provider,
            verifiedEmail: true,
          })
        }

        return true
      } catch (err: any) {
        console.log(err)
        return false
      }
    },
  },
}

export default authOptions

/src/app/api/auth/[…nextauth]/route.ts

import NextAuth from 'next-auth'

// Модели: Пользователь
import '@/models/UserModel'
import authOptions from './authOptions'

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

/src/app/layouts.tsx

import StoreProvider from '@/libs/StoreProvider'
import type { Metadata } from 'next'
import { getServerSession } from 'next-auth'
import NextTopLoader from 'nextjs-toploader'
import { ReactNode } from 'react'
import { Toaster } from 'react-hot-toast'
import authOptions from './api/auth/[...nextauth]/authOptions'
import './globals.scss'

export const metadata: Metadata = {
  title: 'Mona Edu',
  description: 'Mona Edu - Непревзойденная платформа для онлайн-обучения в Вьетнаме',
  icons: {
    icon: ['/favicon.ico?v=4'],
    apple: ['/apple-touch-icon.png?v=4'],
    shortcut: ['/apple-touch-icon.png'],
  },
  manifest: '/site.webmanifest',
}

export default async function RootLayout({
  children,
}: Readonly<{
  children: ReactNode
}>) {
  const session = await getServerSession(authOptions)

  return (
    <html lang='vi'>
      <body suppressHydrationWarning={true}>
        <StoreProvider session={session}>
          {/* Toast */}
          <Toaster
            toastOptions={{
              style: {
                background: '#333',
                color: '#fff',
              },
            }}
          />

          {/* Top Loader */}
          <NextTopLoader
            color="#F7E360"
            initialPosition={0.08}
            crawlSpeed={200}
            height={3}
            crawl={true}
            showSpinner={false}
            easing='ease'
            speed={200}
            shadow='0 0 10px #F7E360,0 0 5px #F7E360'
            zIndex={1600}
            showAtBottom={false}
          />

          {children}
        </StoreProvider>
      </body>
    </html>
  )
}

Пожалуйста, помогите мне! 😥😥😥

Я попытался

  • сократить количество обновлений сессии из next-auth
  • уменьшить размер cookie сессии, в настоящее время он составляет 3 – 4 КБ, но это не является проблемой
    Я определил NEXTAUTH_URL, NEXTAUTH_SECRET в .env

Но все это бессмысленно. Проблема в Vercel?

Ответ или решение

Проблема, с которой вы сталкиваетесь, может быть вызвана несколькими факторами, связанными с конфигурацией и настройками вашего приложения Next.js и сервера Vercel. Давайте рассмотрим некоторые шаги и рекомендации, которые могут помочь вам решить эту проблему.

1. Проверка конфигурации переменных окружения

Убедитесь, что переменные окружения NEXTAUTH_URL и NEXTAUTH_SECRET корректно настроены и доступны в вашем производственном окружении. Эти переменные критически важны для корректной работы аутентификации и сессий.

Пример настройки в файле .env:

NEXTAUTH_URL=https://your-production-url.com
NEXTAUTH_SECRET=your_secret_key

Убедитесь, что конфигурация cookie корректна. Для использования JWT вам нужно убедиться, что настройки соответствуют тому, как ваше приложение управляет сессиями.

Добавьте параметр cookies в настройки NextAuth:

cookies: {
  sessionToken: {
    name: `next-auth.session-token`,
    // Если у вас есть конкретные требования к безопасности, можно настроить домен и путь.
    path: '/',
    sameSite: 'lax', // или 'none' для кросс-доменных запросов
    secure: process.env.NODE_ENV === 'production', // использовать secure cookie в производстве
  },
}

3. Настройки Vercel

Проверьте настройки сессий и таймауты на стороне сервера Vercel. Возможно, есть ограничения на хранение сессий или время их жизни. Убедитесь, что среда выполнения не ограничивает вас по времени бездействия.

4. Подсчет различных данных в токене

С уменьшением размера токена могут возникнуть проблемы. Попробуйте исключить из токена лишние данные. Например, если вы добавляете данные пользователя в токен в функции jwt, убедитесь, что они минимально необходимы. Убедитесь, что длина токена не превышает 4-8 КБ.

5. Обработка сессии

Убедитесь, что ваш session callback не добавляет избыточные данные и чистит токен при каждом вызове. Например:

async session({ session, token }) {
  session.user = {
    id: token._id,
    name: token.name,
    image: token.image,
  }
  return session
}

6. Отладка

Включите отладку в вашем коде, чтобы более точно выяснить, на каком этапе происходит сбой. Например, поэкспериментируйте с добавлением логов на разных этапах в jwt, session и authorize callbacks для отслеживания получения токенов.

7. Проверка сторонних библиотек

Иногда временные проблемы с сессиями могут быть связаны с тем, как библиотека next-auth взаимодействует с вашим приложением. Убедитесь, что вы используете последние версии всех библиотек и следите за известными проблемами на GitHub.

Резюме

Подводя итог, вам следует убедиться, что правильно установлены переменные окружения, оптимизирована конфигурация cookie, правильно настроены параметры сессии и использованы минимальные данные в токенах. Поскольку проблема возникает только в производственной среде, уделяйте внимание не только коду приложения, но и среде Vercel, которая может иметь свои особенности работы и ограничения.

Если проблема сохраняется, можно рассмотреть возможность контакта с поддержкой Vercel или сообществом next-auth, чтобы получить дополнительные рекомендации. Надеюсь, это поможет вам решить проблему!

Оцените материал
Добавить комментарий

Капча загружается...