Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validating Firebase Auth tokens manually

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];
}
like image 257
DimlyAware Avatar asked Jul 21 '26 23:07

DimlyAware


1 Answers

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);
like image 109
Anton Alstes Avatar answered Jul 24 '26 13:07

Anton Alstes