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

Next.js集成Auth.js部署时遇CallbackRouteError证书过期问题

Next.js + Auth.js Credentials Provider 部署后出现 CERT_HAS_EXPIRED 错误

本地运行Next.js结合Auth.js的Credentials Provider登录功能正常,但部署到服务器后触发CallbackRouteError,错误详情显示证书过期(CERT_HAS_EXPIRED)。

登录页面组件代码

'use client'

import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { FormError } from '@/components/utility/FormError'
import { LoginChema } from '@/schemas/LoginSchema'
import { login } from '@/actions/login'
import { useState, useTransition } from 'react'

export const LoginTab = () => {
  const [error, setError] = useState<string | undefined>('')
  const [isPending, startTransition] = useTransition()

  const form = useForm<z.infer<typeof LoginChema>>({
    resolver: zodResolver(LoginChema),
    defaultValues: {
      login: '',
      password: ''
    }
  })

  async function onSubmit(values: z.infer<typeof LoginChema>) {
    setError('')

    startTransition(() => {
      login(values).then((data) => {
        setError(data?.error)
      })
    })

  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col gap-6'>
        <div className='flex w-full flex-col gap-2'>
          <Label htmlFor='login_type' className='text-base tracking-tight'>
            Login/Parolni kiriting
          </Label>
          <FormField
            control={form.control}
            name='login'
            render={({ field }) => (
              <FormItem>
                <FormLabel>Login</FormLabel>
                <FormControl>
                  <Input disabled={isPending} placeholder='login' {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name='password'
            render={({ field }) => (
              <FormItem>
                <FormLabel>Parol</FormLabel>
                <FormControl>
                  <Input disabled={isPending} type='password' placeholder='parol' {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>
        <FormError message={error} />
        <div className='flex w-full flex-col gap-3'>
          <Button disabled={isPending} type='submit' className='w-full bg-brandHighlight-500 text-white'>
            Kirish
          </Button>
        </div>
      </form>
    </Form>
  )
}

登录Action配置代码

import Credentials from 'next-auth/providers/credentials'

import { AuthError, NextAuthConfig } from 'next-auth'
import { LoginChema } from '@/schemas/LoginSchema'

const baseURL = process.env.NEXT_PUBLIC_API_URL + '/users/login'

export default {
  providers: [
    Credentials({
      async authorize(credentials) {
        const validatedFields = LoginChema.safeParse(credentials)

        if (validatedFields.success) {
          const { login, password } = validatedFields.data

          const res = await fetch(baseURL, {
            method: 'POST',
            body: JSON.stringify({
              username: login,
              password
            }),
            headers: { 'Content-Type': 'application/json' }
          })

          if (!res.ok) {
            throw new AuthError('CredentialsSignin')
          }

          const user = await res.json()

          if (!user) return null

          return user
        }

        return null
      }
    })
  ]
} satisfies NextAuthConfig

Auth配置文件代码

import NextAuth from 'next-auth'
import authConfig from '@/auth.config'

export const {
  handlers: {
    GET, POST
  }, auth,
  signIn,
  signOut
} = NextAuth({
  pages: {
    signIn: 'auth/login',
    error: '/auth/error'
  },
  callbacks: {
    async session({ session, token }) {
      session.user.access_token = token.access_token
      session.user.username = token.username
      session.user.role = token.role

      return session
    },
    async jwt({ token, user }) {
      if (user) {
        token.access_token = user.access_token
        token.username = user.username
        token.role = user.role
      }

      return token
    }
  },
  session: {
    strategy: 'jwt',
    maxAge: 8 * 60 * 60
  },
  ...authConfig
})

报错信息

[auth][error] CallbackRouteError
[auth][cause]: TypeError: fetch failed
at node:internal/deps/undici/undici:12344:11
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async globalThis.fetch (/root/projects/shaffof-open-data/.next/server/chunks/198.js:6:55448)
at async Object.authorize (/root/projects/shaffof-open-data/.next/server/app/auth/login/page.js:1:114277)
...
[auth][details]: { "code": "CERT_HAS_EXPIRED", "provider": "credentials" }

解决方案

1. 检查目标API的SSL证书

报错核心是CERT_HAS_EXPIRED,说明服务器请求的baseURL对应的API网站SSL证书已过期:

  • 直接在浏览器访问该API地址,查看地址栏锁图标确认证书状态
  • 联系API服务方更新SSL证书

2. 临时绕过证书验证(仅测试环境)

如果是测试环境且无法立即更新证书,可在fetch请求中添加证书跳过配置(生产环境绝对禁止):

import https from 'https'

// ...其他代码

const res = await fetch(baseURL, {
  method: 'POST',
  body: JSON.stringify({
    username: login,
    password
  }),
  headers: { 'Content-Type': 'application/json' },
  // 仅测试用,生产环境删除
  agent: new https.Agent({ rejectUnauthorized: false })
})

3. 同步服务器系统时间

服务器系统时间偏差过大也会导致证书验证失败:

  • 执行date命令查看服务器当前时间
  • 使用ntpdate或系统自带时间同步工具校准时间

4. 确认环境变量配置

检查服务器上的NEXT_PUBLIC_API_URL是否正确,确认未指向使用过期证书的错误域名。


内容的提问来源于stack exchange,提问作者Islom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 02:35:55