Validating Firebase Auth tokens manually
Asked Answered
A

3

7

I'm trying to use cloudflare workers to perform authenticated actions.

I'm using firebase for authentication and have access to the Access Tokens coming through but since firebase-admin uses nodejs modules it can't work on the platform so i'm left manually validating the token.

I've been attempting to authenticate with the Crypto API and finally got it to import the public key sign the token to check if its valid but I keep getting FALSE. I'm struggling to figure out why its always returning false for validity.

The crypto key I imported is coming in as type "secret" where I would expect it to be "public".

Any thoughts or assistance would be huge. Been banging my head against a table for the last couple of days trying to figure this out

This is what I have so far:

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/[email protected]')).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];
}
Alansen answered 27/3, 2022 at 17:37 Comment(2)
Cloudflare workers do support Cloudflare worker JWT package. have you tried using that?Subrogate
yeah, no luck unfortunatelyAlansen
H
2

There are a few issues with your code:

  1. The URL you call to obtain public keys returns a list of x509 certificates. These are not public keys used to verify signatures. Are you sure you don't have access directly to the public keys? It seems like it's possible to get the public key information from an x509 certificate (as described here: Extract PEM Public Key from X.509 Certificate), though I'm not sure whether that's possible from a Cloudflare worker.

  2. In importPublicKey you're telling the import method, that the key is in raw format and that it is an HMAC key. This means that crypto treats your key as a symmetric HMAC key, not as a public key. According to the docs: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#subjectpublickeyinfo you should be using spki format as this is the one to import a public key. You would have to know up front whether the JWT access token is signed using RSA or Elliptic Curve algorithm. (e.g. check the alg header claim)

  3. You're using sign method to verify the signature. That's not how it works. You should be using the verify method of crypto.subtle and this method will verify the signature for you.

I think you shouldn't be trying to verify JWTs manually, as you will most probably do it wrong (and create security issues for your app). You should be using libraries that deal with the verification of JWT signatures. It will be much easier for you and more secure for your app. One thing you have to figure out is to where you should take the public key from.

Hals answered 28/3, 2022 at 15:27 Comment(2)
Hi Michal, Thank you for your thoughts. Ill dig into 1 and fix 2 & 3. Agreed on verifying JWTs manually my hope is the crypto library would help. My biggest issue is that I can't really find a JWT library that doesnt use node to do that verification. I stumbled on 1 by this documentation: firebase.google.com/docs/auth/admin/…Alansen
Was able to get it working with jsrsasign thank you for your help!. I moved to verifiy. the X.509 certificates seemed to workAlansen
C
7

Note that you can get the jwks from the endpoint https://www.googleapis.com/service_accounts/v1/jwk/[email protected] (documented here).

async function fetchPublicKey(kid) {
  const result = await (
    await fetch(
      "https://www.googleapis.com/service_accounts/v1/jwk/[email protected]"
    )
  ).json();

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

Using the key (named jwk below), you can verify the signature:

  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);
Commission answered 24/4, 2022 at 12:17 Comment(2)
I found using the jwk format to be much easier than using the documented x509 certificate and a library to convert that to a public key. Less code FTW! Thank you, Anton.Th
Did not know about this jwks endpoint. You saved me a lot of headache dealing with x509 certificates. Thank you!!Neodymium
S
3

You can use @codehelios/verify-tokenid Library to verify Firebase ID Token on Cloudflare Workers.

Example:

import { verifyTokenId } from "@codehelios/verify-tokenid";

const tokenId = "<ID_TOKEN>"

const { isValid, decoded, error } = await verifyTokenId(tokenId, "https://securetoken.google.com/<projectId>", "<projectId>");
Storyteller answered 16/7, 2022 at 10:46 Comment(0)
H
2

There are a few issues with your code:

  1. The URL you call to obtain public keys returns a list of x509 certificates. These are not public keys used to verify signatures. Are you sure you don't have access directly to the public keys? It seems like it's possible to get the public key information from an x509 certificate (as described here: Extract PEM Public Key from X.509 Certificate), though I'm not sure whether that's possible from a Cloudflare worker.

  2. In importPublicKey you're telling the import method, that the key is in raw format and that it is an HMAC key. This means that crypto treats your key as a symmetric HMAC key, not as a public key. According to the docs: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#subjectpublickeyinfo you should be using spki format as this is the one to import a public key. You would have to know up front whether the JWT access token is signed using RSA or Elliptic Curve algorithm. (e.g. check the alg header claim)

  3. You're using sign method to verify the signature. That's not how it works. You should be using the verify method of crypto.subtle and this method will verify the signature for you.

I think you shouldn't be trying to verify JWTs manually, as you will most probably do it wrong (and create security issues for your app). You should be using libraries that deal with the verification of JWT signatures. It will be much easier for you and more secure for your app. One thing you have to figure out is to where you should take the public key from.

Hals answered 28/3, 2022 at 15:27 Comment(2)
Hi Michal, Thank you for your thoughts. Ill dig into 1 and fix 2 & 3. Agreed on verifying JWTs manually my hope is the crypto library would help. My biggest issue is that I can't really find a JWT library that doesnt use node to do that verification. I stumbled on 1 by this documentation: firebase.google.com/docs/auth/admin/…Alansen
Was able to get it working with jsrsasign thank you for your help!. I moved to verifiy. the X.509 certificates seemed to workAlansen

© 2022 - 2024 — McMap. All rights reserved.