Home > front end >  verify azure active directory token using SubtleCrypto
verify azure active directory token using SubtleCrypto

Time:02-06

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.

  •  Tags:  
  • Related