使用 SubtleCrypto 验证 azure 活动目录令牌

verify azure active directory token using SubtleCrypto

我正在尝试使用 ms docs 中描述的流程验证 azure 活动目录 ID 和访问令牌。我在浏览器中这样做。我知道在前端做这件事毫无意义。我仍然想知道该怎么做。只是出于好奇。

https://jwt.io/ 上有类似内容。它旨在调试令牌。它可以判断签名是否有效。

在我的代码中,验证总是失败。我不确定在这种情况下我是否正确使用了 SubtleCrypto。我也不确定我是否为它的功能选择了正确的参数,比如算法。

访问令牌的 header 看起来总是这样,使用 RS256。

{
  "typ": "JWT",
  "nonce": "<redacted>",
  "alg": "RS256",
  "x5t": "<redacted>",
  "kid": "<redacted>"
}

匹配的 JWK 是这样的。

{
  "kty": "RSA",
  "use": "sig",
  "kid": "<redacted>",
  "x5t": "<redacted>",
  "n": "<redacted>",
  "e": "<redacted>",
  "x5c": ["<redacted>"]
}

我创建了这个函数来使用声明中的发行者 URL 和 header 中的孩子 属性 获取 JWK,并最终验证签名。 crypto.subtle.verify returns 始终为假。

async function verifyToken(rawToken) {
  // parse the token into parts
  const [encodedHeaders, encodedClaims, signature] = rawToken.split(".");
  const header = JSON.parse(atob(encodedHeaders));
  const claims = JSON.parse(atob(encodedClaims));

  // get the openid config using the issuer url
  const issuer_url = claims.iss.endsWith("/") ? claims.iss : claims.iss + "/";
  const openIdConfiguration = await (
    await fetch(`${issuer_url}.well-known/openid-configuration`)
  ).json();

  // get the jwk list
  const jwkList = await (
    await fetch(openIdConfiguration.jwks_uri)
  ).json();

  // find the jwk for the kid in the token header
  const matchedKey = jwkList.keys.find(k => k.kid === header.kid)

  // return early if no jwk is found
  if (!matchedKey) return {
    rawToken,
    header,
    claims,
    signature,
    openIdConfiguration,
    jwkList,
  };
 
  // import the jwk into a a crypto key
  const pubkey = await crypto.subtle.importKey(
    "jwk",
    matchedKey,
    { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
    true,
    ["verify"],
  );

  // verify the signature
  const verified = await crypto.subtle.verify(
    { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
    pubkey,
    new TextEncoder().encode(signature),
    new TextEncoder().encode(encodedHeaders + "." + encodedClaims),
  );

  // return the results
  return {
    rawToken,
    header,
    claims,
    signature,
    openIdConfiguration,
    jwkList,
    matchedKey,
    verified
  };
};

JWT的三部分都是Base64url编码的,所以需要对签名进行Base64url解码(而不是UTF8编码)。 IE。 new TextEncoder().encode(signature) 行必须相应地替换。

以下代码的示例数据取自jwt.io for RS256:

const rawToken = `eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ`;

const [encodedHeaders, encodedClaims, signature] = rawToken.split(".");

const matchedKey = 
{
"kty":"RSA",
"e":"AQAB",
"kid":"fa05e6ef-ec59-45b4-ba43-51b8293f7f79",
"n":"u1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0_IzW7yWR7QkrmBL7jTKEn5u-qKhbwKfBstIs-bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW_VDL5AaWTg0nLVkjRo9z-40RQzuVaE8AkAFmxZzow3x-VJYKdjykkJ0iT9wCS0DRTXu269V264Vf_3jvredZiKRkgwlL9xNAwxXFg0x_XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC-9aGVd-Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmw"
};

(async () => {

    const pubkey = await crypto.subtle.importKey(
        "jwk",
        matchedKey,
        { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
        true,
        ["verify"],
    );
  
    // verify the signature
    const verified = await crypto.subtle.verify(
        { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
        pubkey,
        new base64url_decode(signature),
        new TextEncoder().encode(encodedHeaders + "." + encodedClaims),
    );
  
    console.log("Verification: ", verified);
  
})();

// Helper for Base64url encoding, from: https://thewoods.blog/base64url/
function base64url_decode(value) {
    const m = value.length % 4;
    return Uint8Array.from(atob(
        value.replace(/-/g, '+')
            .replace(/_/g, '/')
            .padEnd(value.length + (m === 0 ? 0 : 4 - m), '=')
    ), c => c.charCodeAt(0)).buffer;
}

修改后,验证成功。