手动验证 Firebase Auth 令牌

Validating Firebase Auth tokens manually

我正在尝试使用 cloudflare workers 来执行经过身份验证的操作。

我正在使用 firebase 进行身份验证,并且可以访问通过的访问令牌,但是由于 firebase-admin 使用 nodejs 模块,它无法在平台上工作,所以我只能手动验证令牌。

我一直在尝试使用 Crypto API 进行身份验证,最后让它导入 public 密钥对令牌进行签名以检查其是否有效,但我总是得到 FALSE。我正在努力弄清楚为什么它总是返回错误的有效性。

我导入的加密密钥以“secret”类型出现,我希望它是“public”。

任何想法或帮助都是巨大的。在过去的几天里,我一直在苦苦思索 table 试图解决这个问题

这是我目前拥有的:

function _utf8ToUint8Array(str) {
    return Base64URL.parse(btoa(unescape(encodeURIComponent(str))))
}

class Base64URL {
    static parse(s) {
        return new Uint8Array(Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0)))
    }
    static stringify(a) {
        return btoa(String.fromCharCode.apply(0, a)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
    }
}


export async function verify(userToken: string) {
    let jwt = decodeJWT(userToken)
    var jwKey = await fetchPublicKey(jwt.header.kid);
    let publicKey = await importPublicKey(jwKey);
    var isValid = await verifyPublicKey(publicKey, userToken);
    console.log('isValid', isValid) // RETURNS FALSE
    return isValid;
}

function decodeJWT(jwtString: string): IJWT {
    // @ts-ignore
    const jwt: IJWT = jwtString.match(
        /(?<header>[^.]+)\.(?<payload>[^.]+)\.(?<signature>[^.]+)/
    ).groups;

    // @ts-ignore
    jwt.header = JSON.parse(atob(jwt.header));
    // @ts-ignore
    jwt.payload = JSON.parse(atob(jwt.payload));

    return jwt;
}

async function fetchPublicKey(kid: string) {
    var key: any = await (await fetch('https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com')).json();

    key = key[kid];
    key = _utf8ToUint8Array(key)
    return key;
}

function importPublicKey(jwKey) {
    return crypto.subtle.importKey('raw', jwKey, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']);
}

async function verifyPublicKey(publicKey: CryptoKey, token: string) {
    const tokenParts = token.split('.')
    let res = await crypto.subtle.sign({ name: 'HMAC', hash: { name: 'SHA-256' } }, publicKey, _utf8ToUint8Array(tokenParts.slice(0, 2).join('.')))
    return Base64URL.stringify(new Uint8Array(res)) === tokenParts[2];
}

您的代码存在一些问题:

  1. 您调用 URL 获取 public 密钥 returns x509 证书列表。这些不是用于验证签名的 public 密钥。您确定您不能直接访问 public 键吗?似乎可以从 x509 证书中获取 public 密钥信息(如此处所述:Extract PEM Public Key from X.509 Certificate),但我不确定 Cloudflare 工作人员是否可以这样做。

  2. importPublicKey 中,您告诉 import 方法,该密钥是原始格式并且它是一个 HMAC 密钥。这意味着加密将您的密钥视为对称 HMAC 密钥,而不是 public 密钥。根据文档:https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#subjectpublickeyinfo 您应该使用 spki 格式,因为这是导入 public 密钥的格式。您必须事先知道 JWT 访问令牌是使用 RSA 还是椭圆曲线算法签名的。 (例如检查 alg header 声明)

  3. 您正在使用 sign 方法来验证签名。它不是这样工作的。您应该使用 crypto.subtleverify 方法,此方法将为您验证签名。

我认为您不应该尝试手动验证 JWT,因为您很可能会做错(并为您的应用程序带来安全问题)。您应该使用处理 JWT 签名验证的库。这对您来说会更容易,对您的应用程序来说也会更安全。您必须弄清楚的一件事是您应该从哪里获取 public 密钥。

请注意,您可以从未记录的端点获取 jwks https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com

async function fetchPublicKey(kid) {
  const result = await (
    await fetch(
      "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"
    )
  ).json();

  return result.keys.find((key) => key.kid === kid);
}

使用密钥(下面命名为jwk),可以验证签名:

  const encoder = new TextEncoder();
  const data = encoder.encode([token.raw.header, token.raw.payload].join("."));
  const signature = new Uint8Array(
    Array.from(token.signature).map((c) => c.charCodeAt(0))
  );
  const key = await crypto.subtle.importKey(
    "jwk",
    jwk,
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    false,
    ["verify"]
  );

  return crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data);