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

Firebase Auth模拟器生成的JWT含alg:none,无法验证求解决方案

Firebase本地模拟器生成ID Token含alg:"none"导致验证失败的解决方案

问题描述

背景

我使用搭载Firebase Admin SDK的Node云函数,本地测试时通过模拟器生成自定义令牌。该令牌发送至React应用后转换为IdToken,随后尝试将其作为认证令牌用于Golang REST服务器,目前正通过Firebase Admin SDK验证它。

核心问题

本地模拟的云函数通过Auth模拟器生成Firebase自定义令牌时,JWT的alg头部始终为none,但部署后的函数不会出现此问题。
令牌生成代码示例:admin.auth().createCustomToken(request.body.username...)

解码后的IdToken(登录后调用.getIdToken()获取)缺少kid头部,导致无法通过Golang Admin SDK的VerifyIDTokenAndCheckRevoked()验证;同时因alg为none,自定义JWT验证器直接判定令牌无效,错误信息:token is unverifiable: 'none' signature type is not allowed。

自定义验证器代码:

func (s *authService) VerifyToken(ctx context.Context, token string) (*CustomClaims, error) {
    var customClaims *CustomClaims

    parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
        // 即使注释这段验证也无效...
        if val, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("> Wrong signing method: %v", token.Method.Alg())
        } else {
            if val.Alg() != "RS256" {
                return nil, fmt.Errorf("> Unexpected signing method: %v", val.Alg())
            }
        }

        // 为了手动解码令牌,需要给Parse函数提供私钥
        hmacSecret := []byte(os.Getenv("FIREBASE_CREDENTIALS_PRIVATE_KEY"))
        return hmacSecret, nil
    })
    if err != nil {
        if errors.Is(err, jwt.ErrTokenMalformed) {
            return nil, fmt.Errorf("> Malformed authentication: %s", err.Error())
        } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
            return nil, fmt.Errorf("> Expired authentication token: %s", err.Error())
        } else {
            return nil, fmt.Errorf("> Something is wrong with the authentication: %s", err.Error())
        }
    }

    if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
        byteClaims, err := json.Marshal(claims)
        if err != nil {
            return nil, err
        }

        err = json.Unmarshal(byteClaims, customClaims)
        if err != nil {
            return nil, err
        }

        return customClaims, nil

    }
    
    return nil, fmt.Errorf("> Something is wrong with the authentication claims")
    

    /* 因Admin SDK无法验证无kid头部的自定义令牌,此方法已废弃*/
    // client, err := s.app.Auth(ctx)
    // if err != nil {
    //  return nil, err
    // }

    // authToken, err := client.VerifyIDTokenAndCheckRevoked(ctx, token)
    // if err != nil {
    //  return nil, err
    // }

}

更新尝试

我自行实现JWT令牌生成,但无效:能生成可用令牌,但获取Firebase认证的IdToken时,它会被转换为带alg":"none"的无效令牌。已确认自身操作无误,问题出在Firebase IdToken生成器上。


解决方案

方案1:配置模拟器生成带正确签名的令牌

Firebase本地Auth模拟器默认生成无签名令牌是为了简化测试,但可以通过配置强制使用RS256签名:

  1. 先升级Firebase CLI到最新版本(执行npm install -g firebase-tools)
  2. 在本地云函数中初始化Admin SDK时,指定服务账号密钥并强制连接模拟器:
const admin = require('firebase-admin');
if (process.env.FUNCTIONS_EMULATOR === 'true') {
  // 加载本地下载的Firebase服务账号密钥文件
  admin.initializeApp({
    credential: admin.credential.cert(require('./service-account-key.json')),
    authDomain: '你的项目ID.firebaseapp.com'
  });
  // 绑定到本地Auth模拟器
  admin.auth().useEmulator('http://localhost:9099');
} else {
  admin.initializeApp();
}

配置后,模拟器生成的自定义令牌会用RS256签名,转换后的IdToken也会带有正确的alg和kid字段,可直接通过Golang Admin SDK验证。

方案2:本地开发时跳过签名验证(仅限开发环境)

如果暂时无法修改模拟器配置,可以在Golang验证器中针对开发环境跳过alg检查和签名验证,注意:此方法绝对不能用于生产环境:

func (s *authService) VerifyToken(ctx context.Context, token string) (*CustomClaims, error) {
    var customClaims CustomClaims

    // 本地开发环境跳过签名验证,仅检查基本字段
    if os.Getenv("ENVIRONMENT") == "development" {
        parsedToken, _, err := new(jwt.Parser).ParseUnverified(token, &customClaims)
        if err != nil {
            return nil, fmt.Errorf("> 解析令牌失败: %s", err.Error())
        }
        // 手动检查过期时间
        if float64(time.Now().Unix()) > parsedToken.Claims.(*CustomClaims).ExpiresAt {
            return nil, fmt.Errorf("> 认证令牌已过期")
        }
        return &customClaims, nil
    }

    // 生产环境正常验证(修复原代码用私钥验证的错误,改用公钥)
    parsedToken, err := jwt.ParseWithClaims(token, &customClaims, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("> 签名方法错误: %v", token.Method.Alg())
        }
        // 从Firebase官方公钥列表获取对应kid的公钥(可提前缓存)
        kid, ok := token.Header["kid"].(string)
        if !ok {
            return nil, fmt.Errorf("> 缺少kid头部")
        }
        // 可通过环境变量注入公钥,或从本地文件读取
        pubKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(os.Getenv("FIREBASE_PUBLIC_KEY")))
        if err != nil {
            return nil, err
        }
        return pubKey, nil
    })
    if err != nil {
        if errors.Is(err, jwt.ErrTokenMalformed) {
            return nil, fmt.Errorf("> 令牌格式错误: %s", err.Error())
        } else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
            return nil, fmt.Errorf("> 认证令牌已过期: %s", err.Error())
        } else {
            return nil, fmt.Errorf("> 认证验证失败: %s", err.Error())
        }
    }

    if !parsedToken.Valid {
        return nil, fmt.Errorf("> 无效的认证令牌")
    }

    return &customClaims, nil
}

额外提示

本地测试时,确保React应用也连接到Auth模拟器,避免和生产环境混淆:

import { getAuth, connectAuthEmulator } from "firebase/auth";
const auth = getAuth();
if (process.env.NODE_ENV === 'development') {
  connectAuthEmulator(auth, "http://localhost:9099");
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 21:45:06