1

I've been able to verify a GCP ID token on jwt.io's web UI okay, but am struggling to replicated it in code in JS.

I've used both the jose and the jsrsasign libraries to little success.

A bit of my own code to get the basics

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

  // For simplicity trust that the urlBase64toStr function works
  // The parsed JWT is identical to what I see on jwt.io
  jwt.header = JSON.parse(urlBase64toStr(jwt.header));
  jwt.payload = JSON.parse(urlBase64toStr(jwt.payload));

  return jwt;
}

const jwt = decodeJWT('<....JWT string here......>')

const encoder = new TextEncoder();
const byteArrays = {
    signature: encoder.encode(jwt.signature),
    body: encoder.encode(
      JSON.stringify(jwt.header) + "." + JSON.stringify(jwt.payload)
    )
};

// Google's public certs at https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com
const cert = '-----BEGIN CERTIFICATE-----\n<........>' 

Verifying with jose gives false

  const joseKey = await jose.importX509(cert, "RS256");
  console.log(
      await crypto.subtle.verify(
        joseKey.algorithm.name,
        joseKey,
        byteArrays.signature,
        byteArrays.body
      )
  ) 

// Note the following works

console.log(jose.jwtVerify(jwtRaw, joseKey))

Using jsrsaassign also gives false

  var c = new jsrsasign.X509();
  c.readCertPEM(cert);

  var jsRsaAssignKey = await crypto.subtle.importKey(
    "jwk",
    jsrsasign.KEYUTIL.getJWKFromKey(c.getPublicKey()),
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    true,
    ["verify"]
  ); // Gets RSAKey first, then transforms into a JWK, then imported to get CryptoKey
  console.log(
      await crypto.subtle.verify(
        jsRsaAssignKey.algorithm.name,
        jsRsaAssignKey,
        byteArrays.signature,
        byteArrays.body
      )
  )

Where am I going wrong here?

Note: Please do not suggest a NodeJS library. The environment I need to run the script in doesn't support Node core modules.

David Min
  • 1,246
  • 8
  • 27
  • 1
    I am not a JavaScript developer. I am not sure what you are trying to do here **encoder.encode(jwt.signature),**. The JWT signature is base-64 encoded. Therefore I think you need to base64 decode the signature before calling **crypto.subtle.verify**. – John Hanley Oct 27 '21 at 18:29

2 Answers2

1

In the crypto.subtle.verify(algo, key, signature, data) parameters,

  1. the signature supplied to the function should be a TypedArray (Uint8Array) of the URL-base64 decoded version of raw signature string supplied in the JWT. It should not be the TypedArray of the raw signature string.
  2. the data supplied to the function should be a TypedArray of the string of <header>.<payload> as supplied in the raw JWT string. It should not be the decoded, parsed, and then stringified header and payload that looks like {"typ": "JWT"}.{"iss": "https://issuer.com/"}

It must also be noted that the JS built-in TextEncoder by default will not be returning the correct Uint8Array. Instead of using it to transform the string into a TypedArray, use the strToUint8Array function given below.

function strToUint8Array(value: string): Uint8Array {
  return Uint8Array.from(
    Array.from(value).map((letter) => letter.charCodeAt(0))
  );
}

Thanks to @John Hanley for part of the answer on signature decoding.

David Min
  • 1,246
  • 8
  • 27
0

// Note the following works

console.log(await jose.jwtVerify('<....JWT string here......>', joseKey))

Then you might as well use that, jose is a universal module that works outside of Node.js.

Nevetheless, your manual verify inputs are wrong. data should just the jwt without the second dot and signature, encoded as base64url expressed as an ArrayBufferView. signature should be the JWT signature part decoded from base64url expressed as ArrayBufferView.