Firebase Auth模拟器生成的JWT含alg:none,无法验证求解决方案
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签名:
- 先升级Firebase CLI到最新版本(执行
npm install -g firebase-tools) - 在本地云函数中初始化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

