I am trying to verify azure active directory ID and access tokens using the described flow in the ms docs. I am doing this in the browser. I know it's pointless to do it on the frontend. I still want to know how to do it. Just out of curiosity.
There is something similar on https://jwt.io/. It's meant to debug tokens. It can tell if a signature is valid or not.
In my code, the verification always fails. I am unsure If I am using SubtleCrypto correctly in this context. I am also unsure if I am choosing the right parameter for its functions, like the algorithm.
The header of the access token looks always something like this, using RS256.
{
"typ": "JWT",
"nonce": "<redacted>",
"alg": "RS256",
"x5t": "<redacted>",
"kid": "<redacted>"
}
The matched JWK looks like this.
{
"kty": "RSA",
"use": "sig",
"kid": "<redacted>",
"x5t": "<redacted>",
"n": "<redacted>",
"e": "<redacted>",
"x5c": ["<redacted>"]
}
I have created this function to get the JWK using the issuer URL in the claims and kid property in the header, and eventually validate the signature. crypto.subtle.verify
returns always false.
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
};
};
CodePudding user response:
The three parts of the JWT are Base64url encoded, so a Base64url decoding of the signature is necessary (instead of a UTF8 encoding). I.e. the line new TextEncoder().encode(signature)
must be replaced accordingly.
The sample data of the following code is taken from 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;
}
With this change, the verification is successful.