Just finished integrating Azure ActiveDirectory OAuth2 with a Python Web API using the following authentication scenario.

Web Application to Web API diagram

The JWT token is requested through a web application and passed to the Web API for resource access. The Web API can’t just simply trust the token, it needs to verify if the issued token is valid.

Azure AD OAuth2 is using the JSON Web Key (JWK) standard to represent the certificates needed to validate a RS256 (RSA) based JWT token. If you don’t know what a JSON Web Token (JWT) is please consult jwt.io for further information.

To validate the token I used PyJWT and cryptography to support the RS256 algorithm. For fetching external information through HTTP I used requests.

pip install pyjwt cryptography requests

To validate the token we need the public key of the key pair used to sign the token. Which key was used is defined in the JWT header. For later validation we also need the App ID that you entered in the Azure portal.

import jwt

app_id = 'd31a4d20-6c4a-1a40-b74d-1a3d461bb3d8'
access_token = 'XXXX'
token_header = jwt.get_unverified_header(access_token)

The token_header looks something like this:

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "x478xyOplsM1H7NXk7Sx17x1upc",
  "kid": "x478xyOplsM1H7NXk7Sx17x1upc"
}

From that token_header we need the x5t and kid value. The kid is the “Key ID” used to match the specific key. x5t is the X.509 certificate SHA-1 fingerprint encoded in base64.

Next we need to find out where the JWK keys are located to extract the public key from the key with the specified kid. This can be looked up in the openid-configuration. The openid-configuration can be found under https://login.microsoftonline.com/common/.well-known/openid-configuration or https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration. It could also be found under the issuer URL located in the iss field of the token payload, but since this can be faked it should not be used/trusted. (Thanks for pointing this out @iksteen))

import requests

res = requests.get('https://login.microsoftonline.com/common/.well-known/openid-configuration')
jwk_uri = res.json()['jwks_uri']
{
  "authorization_endpoint": "https://login.windows.net/3825d6f3-24ae-47f4-8aa2-35d3c5891324/oauth2/authorize",
  "token_endpoint": "https://login.windows.net/3825d6f3-24ae-47f4-8aa2-35d3c5891324/oauth2/token",
  "token_endpoint_auth_methods_supported": [
    "client_secret_post",
    "private_key_jwt"
  ],
  "jwks_uri": "https://login.windows.net/common/discovery/keys",
  "response_modes_supported": [
    "query",
    "fragment",
    "form_post"
  ],
  [...]
}

I think the JWK URI with Azure is pretty much static, so you could skip the step fetching the OpenID configuration and fetch the JWK keys from https://login.windows.net/common/discovery/keys directly.

res = requests.get(jwk_uri)
jwk_keys = res.json()

x5c = None

# Iterate JWK keys and extract matching x5c chain
for key in jwk_keys['keys']:
    if key['kid'] == token_header['kid']:
        x5c = key['x5c']

The x5c (X.509 certificate chain) parameter contains a chain of one or more PKIX certificates. Azure currently only has one certificate in its x5c chain from which we will extract the public key.

from cryptography.x509 import load_pem_x509_certificate
from cryptography.hazmat.backends import default_backend

cert = ''.join([
    '-----BEGIN CERTIFICATE-----\n',
    x5c[0],
    '\n-----END CERTIFICATE-----\n',
])
public_key =  load_pem_x509_certificate(cert.encode(), default_backend()).public_key()

Notice that the x5c cert is already based64 encoded, it only needs to be wrapped in “—–BEGIN CERTIFICATE—–” and “—–END CERTIFICATE—–”.

After extracting the public key we can finally validate the JWT access token.

jwt.decode(
    access_token,
    public_key,
    algorithms=token_header['alg'],
    audience=app_id,
)

If the JWT token is successfully decoded and validated by the decode() function we ensured that the token is valid and can be trusted.