提问者:小点点

如何在Vercel边缘函数中验证Firebase身份验证令牌?


Vercel边缘函数不支持Firebase管理员SDK,因此如何验证客户端请求并从id令牌获取Firebase用户对象(uid、电子邮件、名称、自定义声明)?

有一个介绍(https://firebase.google.com/docs/auth/admin/verify-id-tokens)来使用第三方库验证id令牌,但仅此而已。


共1个答案

匿名用户

下面的库模块利用jose来处理令牌的验证。它已经徒手编码,所以预计错别字。该模块公开了两种方法:verfyIdToken(req: NextRequest,fire baseIdToken:string)(错误时拒绝)和verfyIdTokenWellInlineError(req:NextRequest,fire baseIdToken:string)(从不拒绝,而是在返回的对象中返回错误)。使用适合您风格的方法。

注意:此代码当前不会与Firebase Auth签入以查看令牌是否已被撤销。

// lib/verify-jwt.ts
import * as jose from 'jose';

// Builds a callback function that makes API calls to the given endpoint
// only once the last response expires, otherwise a fresh call is made.
const buildCachedFetch = (src: string) => {
  let _cache = false,
    expiresAt = 0,
    init = async () => {
      const nowMS = Date.now(),
        res = await fetch(src);
      if (!res.ok) {
        const err = new Error("Unexpected response status code: " + res.status);
        err.res = res;
        throw err;
      }
      expiresAt = nowMS + (Number(/max-age=(\d+)/.exec(res.headers.get('cache-control'))?.[1] || 0) * 1000);
      return res.json();
    },
    refresh = () => {
      expiresAt = 0;
      _cache = init();
      _cache.catch(() => _cache = null);
      return _cache
    };
  
  return (forceRefresh?: boolean) => {
    return !forceRefresh && _cache && (expiresAt === 0 || expiresAt > Date.now())
      ? _cache
      : refresh()
  }
}

// build the callback to fetch the authentication certificates
const fetchAuthKeyStore = buildCachedFetch("https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com");

// Defines a callback to pass to the JWT verifier to resolve the appropriate public key
const authKeyResolver = async ({ alg: string, kid: string }) => {
  const keyStore = await fetchAuthKeyStore();
  if (!(kid in keyStore)) throw new Error("unexpected kid in auth token")
  return jose.importX509(keyStore[kid], alg);
};

// Verifies the Authorization header and returns the decoded ID token. Errors
// will reject the Promise chain.
export const verifyIdToken = async (req: NextRequest, firebaseProjectId: string) => {
  const jwt = /^bearer (.*)$/i.exec(req.headers.get('authorization'));
  
  if (!jwt) throw new Error("Unauthorised");
  
  const result = jose.jwtVerify(jwt, authKeyResolver, {
    audience: firebaseProjectId,
    issuer: `https://securetoken.google.com/${firebaseProjectId}`
  });
  
  result.payload.uid = result.payload.sub; // see https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.decodedidtoken.md#decodedidtokenuid
  
  return result;
}

// Verifies the Authorization header and returns the decoded ID token. Errors
// are returned in the return object instead of rejecting the Promise chain.
export const verifyIdTokenWithInlineError = (req: NextRequest, firebaseProjectId: string) => {
  return verifyIdToken(req, firebaseProjectId)
    .catch((error) => ({ error, payload: null, protectedHeader: null }));
}

使用Vercel的hello. ts示例作为基础,您可以这样使用它:

// pages/api/hello.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyIdTokenWithInlineError } from '../../lib/verify-jwt';

export const config = {
  runtime: 'edge', // this is a pre-requisite
  regions: ['iad1'], // only execute this function on iad1
};

const FIREBASE_PROJECT_ID = "<your-project-id>";

export default async (req: NextRequest) => {
  const { error, payload } = await verifyIdTokenWithInlineError(req, FIREBASE_PROJECT_ID);
  
  if (error) {
    return NextResponse.status(403).json({
      error: "You must be authenticated.",
    });
  }

  // if here, a valid auth token was provided. `payload` contains some
  // user info.

  return NextResponse.json({
    name: `Hello ${payload.name}! I'm an Edge Function!`,
  });
};