Jorge Leitao
Jorge Leitao

Reputation: 20163

How do I validate a Microsoft jwt id_token?

I am working with jwt tokens coming from Microsoft to a client to authenticate requests from it to an web API (server). I have control over the code of both the client (js) and the server (Python).

At the client, I am using the following request to get the token (which the user claims through password/2FA on the tenant):

`https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/authorize
            ?response_type=id_token+token
            &client_id=${CLIENT_ID}
            &redirect_uri=${redirect_uri}
            &scope=openid+email+profile
            &state=${guid()}
            &nonce=${guid()}`

here guid is a unique value, TENANT_ID is the tenant, and CLIENT_ID the client.

After I get this token, I send it as an authorization header, like this:

init = {
    headers: {
       'Authorization': `Bearer ${token}`,
    }
}

return fetch(url, init).then(response => {
    return response.json()
})

On the server, I then retrieve the token and validate it:

if 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '):
    token = request.headers['Authorization'][len('Bearer '):]
    from authlib.jose import jwt
    claims = jwt.decode(token, jwk)

where jwk is the contents of https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys.

The whole flow works until the validation, which fails with the following error:

authlib.jose.errors.InvalidHeaderParameterName: invalid_header_parameter_name: Invalid Header Parameter Names: nonce

which indicates that the header of the token contains a key nonce (I validated it).

Looking at the Microsoft's documentation on this, here, there is no reference to a nonce on the header -- just on the payload.

Q1: what am I doing wrong here?

Q2: assuming that Microsoft is the one putting the nonce in the wrong place (header instead of payload), is it possible to just remove the nonce from the header (on the server side) before passing it to the jose's authentication library? Is this safe to do?

Upvotes: 2

Views: 3696

Answers (1)

zaki benz
zaki benz

Reputation: 702

see : https://robertoprevato.github.io/Validating-JWT-Bearer-tokens-from-Azure-AD-in-Python/

Here is how I validate Azure AD Id_tokens in my API:

import base64
import jwt
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization


def ensure_bytes(key):
    if isinstance(key, str):
        key = key.encode('utf-8')
    return key


def decode_value(val):
    decoded = base64.urlsafe_b64decode(ensure_bytes(val) + b'==')
    return int.from_bytes(decoded, 'big')


def rsa_pem_from_jwk(jwk):
    return RSAPublicNumbers(
        n=decode_value(jwk['n']),
        e=decode_value(jwk['e'])
    ).public_key(default_backend()).public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )


# obtain jwks as you wish: configuration file, HTTP GET request to the endpoint returning them;
jwks = {
    "keys": [
        {
            "kty": "RSA",
            "use": "sig",
            "kid": "piVlloQDSMKx...",
            "x5t": "piVlloQDSMKx...",
            "n": "0XhhwpmEpN-jDBapnzhF...",
            "e": "AQAB",
            "x5c": [
                "MIIDBTCCAe2gAwIBAgIQMCJcg...."
            ],
            "issuer": "https://login.microsoftonline.com/{tenant}/v2.0"
        }
    ]
}

# configuration, these can be seen in valid JWTs from Azure B2C:
valid_audiences = ['dd050a67-ebfd-xxx-xxxx-xxxxxxxx'] # id of the application prepared previously

class InvalidAuthorizationToken(Exception):
    def __init__(self, details):
        super().__init__('Invalid authorization token: ' + details)


def get_kid(token):
    headers = jwt.get_unverified_header(token)
    if not headers:
        raise InvalidAuthorizationToken('missing headers')
    try:
        return headers['kid']
    except KeyError:
        raise InvalidAuthorizationToken('missing kid')



def get_jwk(kid):
    for jwk in jwks.get('keys'):
        if jwk.get('kid') == kid:
            return jwk
    raise InvalidAuthorizationToken('kid not recognized')

def get_issuer(kid):
    for jwk in jwks.get('keys'):
        if jwk.get('kid') == kid:
            return jwk.get('issuer')
    raise InvalidAuthorizationToken('kid not recognized')

def get_public_key(token):
    return rsa_pem_from_jwk(get_jwk(get_kid(token)))


def validate_jwt(jwt_to_validate):
    try:
        public_key = get_public_key(jwt_to_validate)
    issuer = get_issuer(kid)


        options = {
            'verify_signature': True,
            'verify_exp': True,  # Skipping expiration date check
            'verify_nbf': False,
            'verify_iat': False,
            'verify_aud': True  # Skipping audience check
        }

        decoded = jwt.decode(jwt_to_validate,
                             public_key,
                             options=options,
                             algorithms=['RS256'],
                             audience=valid_audiences,
                             issuer=issuer)

        # do what you wish with decoded token:
        # if we get here, the JWT is validated
        print(decoded)

    except Exception as ex:
        print('The JWT is not valid!')
        return False
    else:
        return decoded
        print('The JWT is valid!')

Upvotes: 6

Related Questions