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

如何使用NextAuth Credentials Provider刷新Firebase Access Token?

如何在NextAuth中自动刷新Firebase Access Token?

问题分析

你的现有代码仅在用户登录时获取一次Firebase Access Token,但未处理令牌过期后的自动刷新逻辑。Firebase ID Token默认有效期为1小时,过期后API请求会因认证失败返回错误。此外,当前代码未保存刷新令牌(refresh token)和令牌过期时间,无法触发自动刷新流程。

解决方案

要实现自动刷新,需要完成以下几个核心步骤:

  • 登录时保存Firebase用户的uid、refresh token以及Access Token的过期时间
  • 在NextAuth的jwt回调中检查令牌是否即将过期,若过期则调用Firebase的token刷新接口获取新令牌
  • 更新session回调,将最新的Access Token传递给前端
  • 完善错误处理,让前端能感知刷新失败并引导用户重新登录

修改后的完整代码

import { auth } from "@/config/firebaseApp";
import { signInWithEmailAndPassword } from "firebase/auth";
import { AuthOptions, Session, User } from "next-auth";
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";

// 定义自定义用户类型,避免any类型
interface FirebaseAuthUser extends User {
  uid: string;
  role: string;
  access_token: string;
  refresh_token: string;
  expires_at: number;
}

const authOptions: AuthOptions = {
  pages: {
    signIn: "/signIn",
  },
  session: {
    strategy: "jwt",
  },
  providers: [
    CredentialsProvider({
      type: "credentials",
      credentials: {},
      authorize: async (credentials): Promise<FirebaseAuthUser | null> => {
        try {
          const { email, password } = credentials as { email: string; password: string };
          const userCredentials = await signInWithEmailAndPassword(auth, email, password);
          
          const user = userCredentials.user;
          const accessToken = await user.getIdToken();
          const refreshToken = user.refreshToken;
          // Firebase Token有效期为1小时,计算过期时间
          const expiresAt = Date.now() + 3600 * 1000;

          return {
            uid: user.uid,
            role: "admin", // 这里可以根据实际业务逻辑设置角色
            access_token: accessToken,
            refresh_token: refreshToken,
            expires_at: expiresAt,
          };
        } catch (err: any) {
          // 抛出错误,让NextAuth捕获并提示用户登录失败
          throw new Error(err.message);
        }
      },
    }),
  ],

  callbacks: {
    async jwt({ token, user }) {
      // 用户登录时,初始化JWT数据
      if (user) {
        const firebaseUser = user as FirebaseAuthUser;
        return {
          uid: firebaseUser.uid,
          role: firebaseUser.role,
          access_token: firebaseUser.access_token,
          refresh_token: firebaseUser.refresh_token,
          expires_at: firebaseUser.expires_at,
        };
      }

      // 检查Token是否即将过期(提前1分钟刷新,避免刚好过期时请求失败)
      if (Date.now() < (token.expires_at as number) - 60 * 1000) {
        return token;
      }

      // Token过期,调用Firebase接口刷新
      try {
        const response = await fetch(
          `https://securetoken.googleapis.com/v1/token?key=${process.env.NEXT_PUBLIC_FIREBASE_API_KEY}`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
            body: new URLSearchParams({
              grant_type: "refresh_token",
              refresh_token: token.refresh_token as string,
            }),
          }
        );

        const data = await response.json();

        if (!response.ok) throw data;

        return {
          ...token,
          access_token: data.id_token,
          // 如果返回新的refresh token则更新,否则保留原token
          refresh_token: data.refresh_token || token.refresh_token,
          expires_at: Date.now() + data.expires_in * 1000,
        };
      } catch (error) {
        console.error("Token刷新失败:", error);
        // 刷新失败时添加错误标记,前端可据此引导用户重新登录
        return { ...token, error: "RefreshTokenError" };
      }
    },

    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string;
        session.user.access_token = token.access_token as string;
        // 将错误信息传递到session,前端可监听处理
        if (token.error) {
          session.error = token.error as string;
        }
      }
      return session;
    },
  },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

关键细节说明

  1. 保存必要信息:登录时不仅保存Access Token,还保存refresh token和过期时间,这是自动刷新的基础
  2. Token过期检查:提前1分钟触发刷新,避免在请求API时刚好遇到Token过期的情况
  3. Firebase刷新接口:使用官方的Token刷新端点,传入refresh token获取新的ID Token
  4. 错误处理:刷新失败时在JWT中添加错误标记,前端可通过useSession钩子监听该错误,引导用户重新登录
  5. 类型安全:定义自定义用户类型,避免使用any,提升代码可维护性

前端使用提示

在前端调用API时,可通过useSession获取最新的access_token,并在遇到401错误或session中的error为RefreshTokenError时,引导用户重新登录或触发session刷新:

import { useSession } from "next-auth/react";

const { data: session, update } = useSession();

// 调用API时携带token
const fetchData = async () => {
  try {
    const response = await fetch("/api/your-endpoint", {
      headers: {
        Authorization: `Bearer ${session?.user.access_token}`,
      },
    });
    if (response.status === 401 || session?.error === "RefreshTokenError") {
      // 触发session刷新,会调用jwt回调尝试刷新token
      await update();
      // 刷新后重新请求
      const newResponse = await fetch("/api/your-endpoint", {
        headers: {
          Authorization: `Bearer ${session?.user.access_token}`,
        },
      });
      // 处理新响应
    }
  } catch (err) {
    // 处理错误
  }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 11:44:54