How to verify a JWT with RS256 signature in Deno?
Asked Answered
A

3

1

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?

Armipotent answered 6/6, 2020 at 6:42 Comment(0)
N
4

Support for RS256 is available since version 1.6 of djwt. And since Deno version 1.12, RSA signatures can be verified with crypto.subtle, without the need to import external libraries.

First I show a short example with hardcoded values for the JWK and a token that I made up for this demonstration. The verification is done with the crypto.subtle functions:

import { decode } from "https://deno.land/[email protected]/encoding/base64url.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"
}


// import the JWK to RSA Key
const key = await crypto.subtle.importKey(
    "jwk",
    pubJWK,
    {name: "RSASSA-PKCS1-v1_5", hash: "SHA-256"},
    true,
    ["verify"],
  )

console.log(key)

// split the token into it's parts for verifcation
const [headerb64, payloadb64, signatureb64] = jwt.split(".")
const encoder = new TextEncoder()
const data = encoder.encode(headerb64 + '.' + payloadb64)
const signature = decode(signatureb64)

// verify the signature
const result = await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data);
console.log(result)

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. Here I use djwt to verify the token after the key was imported.

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 { verify, 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.4 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) {
    // import the JWK to RSA Key
    const key = await crypto.subtle.importKey(
      "jwk",
      pubJWK,
      {name: "RSASSA-PKCS1-v1_5", hash: "SHA-256"},
      true,
      ["verify"],
    )

    // verify the signature based on the given public key
    try {
        console.log(await verify(jwt, key))
    }
    catch (error)
    {
        console.log(error)
    }

}
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 crypto.subtle.importKey(), {name: "RSASSA-PKCS1-v1_5", hash: "SHA-256"}is used, which is the 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.

This post is a complete update of the original version, which was based on the now no longer maintained God Crypto library.

Nannette answered 6/12, 2020 at 13:54 Comment(2)
fun fact: god crypto's author and me wrote over discord about this, he implemented the solution after i asked him about this, forgot to update the question here, but yeah i agree god_crypto is now a valid solution :)Armipotent
thanks for the response. Step by step god crypto and also djwt are getting better. I think with some further development on these libs, the code above above can be simplifid in future.Nannette
H
0

Let's try this code, more details visit this page jwt authentication in Deno

import { Context } from "https://deno.land/x/oak/mod.ts";
import users from "./users.ts";
import { makeJwt, setExpiration, Jose, Payload } from "https://deno.land/x/djwt/create.ts"
import key from './key.ts'

const header: Jose = {
  alg: "HS256",
  typ: "JWT",
}

export const login = async (ctx: Context) => {
  const {value} = await ctx.request.body();
  for (const user of users) {
    if (value.username === user.username && value.password === user.password) {
      const payload: Payload = {
        iss: user.username,
        exp: setExpiration(new Date().getTime() + 60000),
      }

      // Create JWT and send it to user
      const jwt = makeJwt({key, header, payload});
      if (jwt) {
        ctx.response.status = 200;
        ctx.response.body = {
          id: user.id,
          username: user.username,
          jwt,
        }
      } else {
        ctx.response.status = 500;
        ctx.response.body = {
          message: 'Internal server error'
        }
      }
      return;
    }
  }

  ctx.response.status = 422;
  ctx.response.body = {
    message: 'Invalid username or password'
  };
};
Hiawatha answered 6/6, 2020 at 9:5 Comment(1)
djwt as mentioned does not provide support for RS256, only HS256/HS512Armipotent
H
0

With the release of Deno v1.18 https://deno.land/x/jose is the most complete JWT/JWE/JWS/JWK module.

Verifying using a remote JWKSet is as simple as

import * as jose from 'https://deno.land/x/[email protected]/index.ts'

const JWKS = jose.createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'))

const { payload, protectedHeader } = await jose.jwtVerify(jwt, JWKS, {
  issuer: 'urn:example:issuer',
  audience: 'urn:example:audience'
})
console.log(protectedHeader)
console.log(payload)
Hauteur answered 20/1, 2022 at 17:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.