如何在 Deno 中验证带有 RS256 签名的 JWT?

How to verify a JWT with RS256 signature in Deno?

我想验证来自 Google JWT 的签名,它现在使用 RS256 作为签名算法(来自 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)。

我真的不喜欢整个密码游戏,也许有人知道我如何验证签名,也许已经有一些例子?我真的不知道我需要用 SHA-256 散列什么或者我如何使用 RSA,当我试图查找如何实现它时,我看到了很多技术解释但没有关于如何处理的真实示例。

我通常只是在 Node 上使用 Googles Scriptpackage 见:https://developers.google.com/identity/sign-in/web/backend-auth

我有使用 SHA-256 散列的函数,但没有关于 RSA 的函数?

让我们试试这段代码,更多详情请访问此页面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'
  };
};

djwt 的 1.6 版本开始支持 RS256。 从 Deno 1.12 版开始,可以使用 crypto.subtle 验证 RSA 签名,而无需导入外部库。

首先,我展示了一个简短示例,其中包含 JWK 的硬编码值和我为该演示编写的令牌。使用 crypto.subtle 函数完成验证:

import { decode } from "https://deno.land/std@0.95.0/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)

直接运行上面的代码就可以得到 结果

true

验证成功。

第二个示例从 google 证书端点加载 JWKS(JSON Web 密钥集),尝试查找匹配密钥,然后在找到匹配密钥时验证令牌。这里我在导入key后使用djwt验证token

令牌 header 包含一个密钥 ID(“kid”),它标识应该用于验证的密钥。

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "W-67nektYTc8JVYPeWH5sWe7RYVnn0Sy741f8TOJ_Ac"
}
import { verify, decode } from "https://deno.land/x/djwt@v2.4/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 }
    )
}

在令牌 header 中您看到 "alg": "RS256",但在 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 中看到

由于给定的令牌(在 jwt.io 上创建的示例)未由 Google 签名,因此找不到匹配的密钥,因此无法验证。使用你自己的 Google 签名的 JWT 来测试上面的代码。

此 post 是 original version 的完整更新,它基于现在不再维护的 God Crypto 库。

随着 Deno v1.18 的发布 https://deno.land/x/jose 是最完整的 JWT/JWE/JWS/JWK 模块。

使用远程 JWKSet 进行验证非常简单

import * as jose from 'https://deno.land/x/jose@v4.3.8/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)