.. _azure: Dynamic keys for Azure ====================== In scenarios where you need to decode a JWT received from Azure (Microsoft), you may encounter a situation where you are unaware of the public key required for the decoding process until after the token arrives. In such cases, you will typically need to retrieve the key set dynamically from the ``iss`` (issuer) value contained within the JWT. Let's illustrate this process using a JWT token extracted from Microsoft's official documentation https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens: .. code-block:: none eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imk2bEdrM0ZaenhSY1ViMkMzbkVRN3N5SEpsWSJ9 .eyJhdWQiOiI2ZTc0MTcyYi1iZTU2LTQ4NDMtOWZmNC1lNjZhMzliYjEyZTMiLCJpc3MiOiJodHRwczovL2x vZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3Y yLjAiLCJpYXQiOjE1MzcyMzEwNDgsIm5iZiI6MTUzNzIzMTA0OCwiZXhwIjoxNTM3MjM0OTQ4LCJhaW8iOiJ BWFFBaS84SUFBQUF0QWFaTG8zQ2hNaWY2S09udHRSQjdlQnE0L0RjY1F6amNKR3hQWXkvQzNqRGFOR3hYZDZ 3TklJVkdSZ2hOUm53SjFsT2NBbk5aY2p2a295ckZ4Q3R0djMzMTQwUmlvT0ZKNGJDQ0dWdW9DYWcxdU9UVDI yMjIyZ0h3TFBZUS91Zjc5UVgrMEtJaWpkcm1wNjlSY3R6bVE9PSIsImF6cCI6IjZlNzQxNzJiLWJlNTYtNDg 0My05ZmY0LWU2NmEzOWJiMTJlMyIsImF6cGFjciI6IjAiLCJuYW1lIjoiQWJlIExpbmNvbG4iLCJvaWQiOiI 2OTAyMjJiZS1mZjFhLTRkNTYtYWJkMS03ZTRmN2QzOGU0NzQiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhYmV saUBtaWNyb3NvZnQuY29tIiwicmgiOiJJIiwic2NwIjoiYWNjZXNzX2FzX3VzZXIiLCJzdWIiOiJIS1pwZmF IeVdhZGVPb3VZbGl0anJJLUtmZlRtMjIyWDVyclYzeERxZktRIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWF mLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidXRpIjoiZnFpQnFYTFBqMGVRYTgyUy1JWUZBQSIsInZlciI6IjIuMCJ9 .pj4N-w_3Us9DrBLfpCt This token, obtained from Microsoft's official documentation, serves as an example for decoding JWTs originating from Azure. The decoded payload might look like: .. code-block:: json { "aud": "6e74172b-be56-4843-9ff4-e66a39bb12e3", "iss": "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0", "iat": 1537231048, "nbf": 1537231048, "exp": 1537234948, "aio": "AXQAi/8IAAAAtAaZLo3ChMif6KOnttRB7eBq4/DccQzjcJGxPYy/C3jDa...", "azp": "6e74172b-be56-4843-9ff4-e66a39bb12e3", "azpacr": "0", "name": "Abe Lincoln", "oid": "690222be-ff1a-4d56-abd1-7e4f7d38e474", "preferred_username": "abeli@microsoft.com", "rh": "I", "scp": "access_as_user", "sub": "HKZpfaHyWadeOouYlitjrI-KffTm222X5rrV3xDqfKQ", "tid": "72f988bf-86f1-41af-91ab-2d7cd011db47", "uti": "fqiBqXLPj0eQa82S-IYFAA", "ver": "2.0" } Steps for decoding ------------------ In order to decode JWT tokens from Azure, it is essential to retrieve the necessary information from Microsoft's OpenID configuration, including the JSON Web Key Set (JWK Set) URI. This information is crucial for verifying the tokens. OpenID Configuration Endpoint ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can obtain the OpenID configuration endpoint from Microsoft by forming a URL in the following format: .. code-block:: none https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration In the example provided, replace ``{tenant}`` with your specific Azure tenant ID or the tenant's globally unique identifier (GUID). The resulting URL will lead you to the OpenID configuration details. Then, the OpenID configuration endpoint for the above example could be: .. code-block:: none https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0/.well-known/openid-configuration JWK Set URI ~~~~~~~~~~~ Within the OpenID configuration details, you will find the JSON Web Key Set (JWK Set) URI. This URI is used to access the keys required for verifying JWT tokens. The JWK Set URI can typically be found within the configuration as follows: .. code-block:: none https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys Once again, remember to replace ``{tenant}`` with your Azure tenant ID or the appropriate identifier. In the above example, the ``jwks_uri`` could be: .. code-block:: none https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/discovery/v2.0/keys Validating JWT Tokens ~~~~~~~~~~~~~~~~~~~~~ Once you have retrieved the JSON Web Key Set (JWK Set) from the JWK Set URI provided in the OpenID configuration, you can proceed to validate JWT tokens. Using a Callable Key -------------------- In ``joserfc``, a callable key is a powerful feature that allows you to dynamically retrieve and use the appropriate JSON Web Key (JWK) for token decoding. In the context of Azure tokens, you can implement a callable key to fetch the JWKs from the JWK Set URI and select the correct key based on the kid (Key ID) in the token's header. .. code-block:: python import json import requests from joserfc.jws import CompactSignature from joserfc.jwk import KeySet from joserfc import jwt def load_key(obj: CompactSignature): claims = json.loads(obj.payload) issuer_url = claims['iss'] # retrieve OpenID Configuration Endpoint openid_configuration_endpoint = f'{issuer_url}/.well-known/openid-configuration' resp = requests.get(openid_configuration_endpoint) # retrieve JWK Set URI jwks_uri = resp.json()['jwks_uri'] resp = requests.get(jwks_uri) key_set = KeySet.import_key_set(resp.json()) return key_set # pass load_key as a callable key to `jwt.decode` method jwt.decode(token_string, load_key) When using the callable key method in ``joserfc`` to decode the tokens, it retrieves the key dynamically on each token decoding request. However, you may encounter performance issues due to the repeated retrieval of keys. In such cases, it's advisable to optimize the callable key by implementing key set caching based on the issuer. Let's enhance the callable key method to improve its efficiency. .. code-block:: python import functools @functools.cache def fetch_key_set(issuer: str): openid_configuration_endpoint = f'{issuer}/.well-known/openid-configuration' resp = requests.get(openid_configuration_endpoint) jwks_uri = resp.json()['jwks_uri'] resp = requests.get(jwks_uri) return KeySet.import_key_set(resp.json()) def load_key(obj: CompactSignature): claims = json.loads(obj.payload) key_set = fetch_key_set(claims['iss']) return key_set In this enhanced callable key, an LRU (Least Recently Used) cache is used to store JWK Sets for different issuers. When decoding a token, the callable key function first checks if the JWK Set for the specific issuer is available in the cache. If it's not, it fetches the JWK Set for the issuer, caches it, and then selects the appropriate JWK based on the kid. This caching mechanism significantly reduces the network requests for JWK Sets and improves the efficiency of token decoding. Manual Token Decoding --------------------- If you prefer a more hands-on approach and want to decode the token step by step, you can opt for a manual decoding process. This method allows you to extract the token string and work with it directly. Since the token is a JWT in JWS format, you can utilize the ``extract_compact`` method from the JWS module to obtain the necessary information. The result of this extraction is an object of type :class:`~joserfc.jws.CompactSignature`. .. code-block:: python from joserfc.jws import extract_compact, CompactSignature obj: CompactSignature = extract_compact(token_string) Similar to the approach detailed in the "Using a Callable Key" section, you can retrieve the key set based on the issuer (``iss``) claim. This method allows you to access the necessary keys for token verification. .. code-block:: python @functools.cache def fetch_key_set(issuer: str): openid_configuration_endpoint = f'{issuer}/.well-known/openid-configuration' resp = requests.get(openid_configuration_endpoint) jwks_uri = resp.json()['jwks_uri'] resp = requests.get(jwks_uri) return KeySet.import_key_set(resp.json()) claims = json.loads(obj.payload) key_set = fetch_key_set(claims['iss']) Once you have obtained the key set based on the issuer (``iss``) claim, you can use this set of keys to decode the token. .. code-block:: python from joserfc import jwt token = jwt.decode(token_string, key_set)