使用 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;
}
修改后,验证成功。
我正在尝试使用 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;
}
修改后,验证成功。