Reputation: 20163
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
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