Last active
July 8, 2023 14:04
-
-
Save phillipharding/229292754a56a9fcf6ea49bc9b4a3780 to your computer and use it in GitHub Desktop.
Verifies an access token issued by the Azure AD identity platform
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "jwt-validate", | |
"version": "1.0.0", | |
"description": "", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "ISC", | |
"dependencies": { | |
"jsonwebtoken": "^8.5.1", | |
"jwks-rsa": "^2.0.5", | |
"node-fetch": "^2.6.1", | |
"rsa-pem-from-mod-exp": "^0.8.4" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** References: | |
* https://www.npmjs.com/package/azure-ad-verify-token | |
* https://github.com/justinlettau/azure-ad-verify-token | |
* https://liangjunjiang.medium.com/verify-and-decode-azure-activity-directory-token-bc72cf7010bc | |
* https://liangjunjiang.medium.com/azure-active-directory-api-v1-0-vs-v2-0-5c75fb2b1154 | |
* https://www.npmjs.com/package/jwks-rsa | |
* https://www.npmjs.com/package/jsonwebtoken | |
* https://www.voitanos.io/blog/validating-azure-ad-generated-oauth-tokens | |
* | |
* NOTE: | |
* This does not work for access tokens where the audience is the Graph API - which introduces a | |
* "nonce" value into the header of the access token, making it all but impossible to verify the access | |
* token signature. | |
*/ | |
const jwksClient = require("jwks-rsa"); | |
const jwt = require("jsonwebtoken"); | |
const fetch = require("node-fetch"); | |
const getPem = require('rsa-pem-from-mod-exp'); | |
const AADOID_JWKS_URL = "https://login.microsoftonline.com/<your tenant id>/discovery/v2.0/keys"; | |
const accessTokenToVerify = "<access token to verify>"; | |
const expectedTokenAud = "https://management.azure.com"; | |
const expectedTokenIss = "https://sts.windows.net/<your tenant id>"; | |
console.clear(); | |
const displayToken = (token) => { | |
const decodedToken = jwt.decode(token, { complete: true }); | |
console.log(`\ndecodedToken header:`, JSON.stringify(decodedToken.header, undefined, 3)); | |
console.log(`\ndecodedToken payload:`, JSON.stringify(decodedToken.payload, undefined, 3)); | |
console.log(`\ndecodedToken signature:`, JSON.stringify(decodedToken.signature, undefined, 3)); | |
return Promise.resolve(decodedToken.header); | |
}; | |
/** return the JSON web key set from the Azure AD OID configuration | |
response = { | |
keys: [ | |
{ | |
"kty": "RSA", | |
"use": "sig", | |
"kid": "nOo3ZDrODXEK1jKWhXslHR_KXEg", | |
"x5t": "nOo3ZDrODXEK1jKWhXslHR_KXEg", | |
"n": "oaLLT9hkcSj2tGfZsjbu7Xz1Krs0qEicXPmEsJKOBQHauZ_kRM1HdEkgOJbUznUspE6xOuOSXjlzErqBxXAu4SCvcvVOCYG2v9G3-uIrLF5dstD0sYHBo1VomtKxzF90Vslrkn6rNQgUGIWgvuQTxm1uRklYFPEcTIRw0LnYknzJ06GC9ljKR617wABVrZNkBuDgQKj37qcyxoaxIGdxEcmVFZXJyrxDgdXh9owRmZn6LIJlGjZ9m59emfuwnBnsIQG7DirJwe9SXrLXnexRQWqyzCdkYaOqkpKrsjuxUj2-MHX31FqsdpJJsOAvYXGOYBKJRjhGrGdONVrZdUdTBQ", | |
"e": "AQAB", | |
"x5c": [ | |
"MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R" | |
], | |
"issuer": "https://login.microsoftonline.com/9b4f4fcf-59ca-4fb5-878f-4dea4046b5c6/v2.0" | |
} | |
] | |
} | |
*/ | |
const getOIDJwksCollection = async (kid) => { | |
const response = await fetch(AADOID_JWKS_URL); | |
const body = await response.json(); | |
const key = body.keys.find( (k) => (k.kid === kid) ); | |
if (key === null) { | |
throw new Error(`Key ${kid} not found!`); | |
} | |
return key; | |
}; | |
const verifyToken = async (token, tokenHeader, pemKey, x5cKey) => { | |
const client = jwksClient({ | |
jwksUri: AADOID_JWKS_URL | |
}); | |
console.log(`KID:`, tokenHeader.kid); | |
const key = await client.getSigningKey(tokenHeader.kid); | |
console.log(`JWKS KEY:`); | |
console.log(key); | |
const signingKey = key.publicKey || key.rsaPublicKey; | |
console.log(`Signing Key:`); | |
console.log(signingKey); | |
console.log(`\nVerify Token (1):`); | |
try { | |
const verify = await jwt.verify(token, signingKey, { | |
algorithms: ['RS256'], | |
issuer: `${expectedTokenIss}`, | |
audience: `${expectedTokenAud}` | |
}); | |
console.log(verify); | |
} catch (e) { | |
console.error(e); | |
} | |
console.log(`\nVerify Token (2):`); | |
try { | |
const key1 = `-----BEGIN CERTIFICATE-----\n${x5cKey}\n-----END CERTIFICATE-----`; | |
const verify = await jwt.verify(token, key1, { | |
algorithms: ['RS256'], | |
issuer: `${expectedTokenIss}`, | |
audience: `${expectedTokenAud}` | |
}); | |
console.log(verify); | |
} catch (e) { | |
console.error(e); | |
} | |
console.log(`\nVerify Token (3):`); | |
try { | |
const verify = await jwt.verify(token, pemKey, { | |
algorithms: ['RS256'], | |
issuer: `${expectedTokenIss}`, | |
audience: `${expectedTokenAud}` | |
}); | |
console.log(verify); | |
} catch (e) { | |
console.error(e); | |
} | |
return Promise.resolve(); | |
}; | |
(async () => { | |
console.log("\nDISPLAY TOKEN\n======================================================"); | |
const tokenHeader = await displayToken(accessTokenToVerify); | |
let pemKey = ""; | |
let x5cKey = ""; | |
try { | |
console.log("\nGET KEYS\n======================================================"); | |
const key = await getOIDJwksCollection(tokenHeader.kid); | |
x5cKey = key.x5c; | |
pemKey = getPem(key.n, key.e); // pass in the key modulus (n) and exponent (e) | |
console.log(`PEM KEY for [${tokenHeader.kid}]\n`, pemKey); | |
console.log(`X5C KEY for [${tokenHeader.kid}]\n`, x5cKey); | |
} catch (e) { | |
console.error(e); | |
} | |
console.log("\n\nVERIFY TOKEN\n======================================================"); | |
await verifyToken(accessTokenToVerify, tokenHeader, pemKey, x5cKey); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment