How to verify a JWT with RS256 signature in Deno?

I want to verify a signature from a Google JWT which uses RS256 as signature algorithm as of right now (Certs from Google: https://www.googleapis.com/oauth2/v3/certs), and the only libary which i could find for Deno handles HS256 (https://deno.land/x/djwt).

I am really not into the whole Cipher game, maybe anybody got an idea how i can verify the signature maybe there already is something with an example? I really don't know what i need to hash with SHA-256 or how i use RSA, when i try to look up how to implement this, I see a lot of technical explanation but no real examples on what to do with what.

I usually just used Googles Scriptpackage on Node see: https://developers.google.com/identity/sign-in/web/backend-auth

I have functions to hash with SHA-256 but nothing about RSA?


Solution 1:

Support for RS256 is available since version 1.6 of djwt.

However, here I'm using a crypto module named God Crypto to verify a RS256 signed token. God Crypto has a function to parse a JSON Web Key (JWK), a feature that we need to verify a JWT provided by Google.

First I show a short example with hardcoded values for the JWK and a token that I made up for this demonstration:

import { RSA, encode } from "https://deno.land/x/[email protected]/mod.ts";

const jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlctNjduZWt0WVRjOEpWWVBlV0g1c1dlN1JZVm5uMFN5NzQxZjhUT0pfQWMifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.hiKxeC66LIyVKOXjiOk7iScFPy_5-ATw7hEfqGij8sBZmwXAeTPT5BRFYHitFKSXomGqmy_63LLvg4zbhcTTmNf8XIeDAuLsC32soO5woSByisswWHVf8BgxMkI_FPW_oEtEQ8Xv3FL_1rF9j9Oy3jIjgjqhFhXUtsSQWAeuGYH-OQljFwiuO5Bqexcw-H71OEWvQLQof_6KJ0viJyte8QEwEVridyO834-ppHzeaoW2sTvZ22ZNfxPCew0Ul2V_TxHTtO7ZuJCZ81EmeIV6dYJ2GrYh3UN1x1PHy4-tEn-PL4otlaO3PYOcXfCHxHa6xtPsquzPZJnB1Vq8zULLfQ"

// public key in JSON Web Key(JWK) format:
const pubJWK = {
    "kty": "RSA",
    "e": "AQAB",
    "use": "sig",
    "kid": "W-67nektYTc8JVYPeWH5sWe7RYVnn0Sy741f8TOJ_Ac",
    "alg": "RS256",
    "n": "kFpGoVmBmmKepvBQiwq3hU9lIAuGsAPda4AVk712d3Z_QoS-5veGp4yltnyEFYyX867GOKDpbH7OF2uIjDg4-FPZwbuhiMscbkZzh25SQmfRtCT5ocUloQiopBcNAE-sd1p-ayUJWjhPrFoBrBLZHYxVEjY4JrWevQDj7kSeX7eJpud_VuZ77TNoIzj7d_iUuJUUlqF1ZF540igHKoVJJ6ujQLHh4ob8_izUuxX2iDq4h0VN3-uer59GsWw6OHgkOt85TsjMwYbeN9iw_7cNfLEYpSiH-sVHBCyKYQw7f8bKaChLxDRhUUTIEUUjGT9Ub_A3gOXq9TIi8BmbzrzVKQ"
}

// parse the JWK to RSA Key
const publicKey = RSA.parseKey(pubJWK)
const rsa = new RSA(publicKey)

// split the token into it's parts for verifcation
const [headerb64, payloadb64, signatureb64] = jwt.split(".")

// verify the signature based on the given public key
console.log(await rsa.verify(
    encode.base64url(signatureb64),
    headerb64 + "." + payloadb64,
    { algorithm: "rsassa-pkcs1-v1_5", hash: "sha256" },
  ))

you can directly run the code above and get the result

true

for successful verification.

The second example loads the JWKS (JSON Web Key Set) from the google certs endpoint, tries to find the matching key and then verifies the token when a matching key was found.

The token header contains a key Id ("kid"), which identifies the key that should be used for verification.

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "W-67nektYTc8JVYPeWH5sWe7RYVnn0Sy741f8TOJ_Ac"
}
import { RSA, encode } from "https://deno.land/x/[email protected]/mod.ts";
import { decode } from "https://deno.land/x/[email protected]/mod.ts"

// the JWT that we want to verify
const jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlctNjduZWt0WVRjOEpWWVBlV0g1c1dlN1JZVm5uMFN5NzQxZjhUT0pfQWMifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.hiKxeC66LIyVKOXjiOk7iScFPy_5-ATw7hEfqGij8sBZmwXAeTPT5BRFYHitFKSXomGqmy_63LLvg4zbhcTTmNf8XIeDAuLsC32soO5woSByisswWHVf8BgxMkI_FPW_oEtEQ8Xv3FL_1rF9j9Oy3jIjgjqhFhXUtsSQWAeuGYH-OQljFwiuO5Bqexcw-H71OEWvQLQof_6KJ0viJyte8QEwEVridyO834-ppHzeaoW2sTvZ22ZNfxPCew0Ul2V_TxHTtO7ZuJCZ81EmeIV6dYJ2GrYh3UN1x1PHy4-tEn-PL4otlaO3PYOcXfCHxHa6xtPsquzPZJnB1Vq8zULLfQ"

// get the JSON Web Key Set (JWKS) from google certs endpoint
const certs = fetch("https://www.googleapis.com/oauth2/v3/certs");
var jwks = await certs.then((response) => {
  return response.json()
})


// decode the JWT to get the key Id ('kid') from the header
// in Version 2.2 of djwt decode returns a 3 tuple instead of an object
const [ header, payload, signature  ] = decode(jwt)
var keyId = Object(header).kid

// find the matching JSON Web Key (JWK) 
var pubjwk = findJWKByKeyId(String(keyId))

// parse the JWK to RSA Key
if (pubjwk) {
    const publicKey = RSA.parseKey(pubjwk)
    const rsa = new RSA(publicKey)

    // split the token into it's parts for verifcation
    const [headerb64, payloadb64, signatureb64] = jwt.split(".")

    // verify the signature based on the given public key
    console.log(await rsa.verify(
        encode.base64url(signatureb64),
        headerb64 + "." + payloadb64,
        { algorithm: "rsassa-pkcs1-v1_5", hash: "sha256" },
    ))
}
else
{
    console.log("key with kid (" + keyId +") not found")
}

// function to find a certain JWK by its Key Id (kid)
function findJWKByKeyId(kid:string) {
    return jwks.keys.find(
        function(x:string){ return Object(x).kid == kid }
    )
  }

In the token header you see "alg": "RS256", but in rsa.verify(), algorithm: "rsassa-pkcs1-v1_5"is used, which is ithe long form for the RS in RS256`, as Scott Brady explains.

As the given token (an example created on jwt.io) was not signed by Google, no matching key can be found and therefore it can't be verified. Use your own Google signed JWT to test the above code.

Parts of the verification code are based on examples from the God Crypto Github page