如何验证由Azure AD生成的JWT?

10

问题

当我在Python中收到来自Azure AD的JWK时,我想要验证并解码它。然而,我一直收到"签名验证失败"的错误。

我的设置

我的设置如下:

  1. Azure设置
    在Azure中,我创建了一个应用程序注册,并选择了"仅适用于个人Microsoft帐户"的设置。
  2. Python设置
    在Python中,我使用MSAL软件包接收令牌。我使用来自Azure的公钥来验证令牌。

代码

使用来自Azure门户的凭据,我设置了一个客户端以获取令牌。

import msal
ad_auth_client = msal.ConfidentialClientApplication(
    client_id = client_id,
    client_credential = client_secret,
    authority = "https://login.microsoftonline.com/consumers"
)
my_token = ad_auth_client.acquire_token_for_client(scopes=['https://graph.microsoft.com/.default'])

如果我将令牌抛到像https://jwt.io/这样的网站上,一切看起来都很好。接下来,我需要从Azure获取公钥以验证令牌。
import requests
response = requests.get("https://login.microsoftonline.com/common/discovery/keys")
keys = response.json()['keys']

为了将公钥与令牌匹配,我使用令牌头中的“kid”。我还获取了用于加密的算法。
import jwt
token_headers = jwt.get_unverified_header(my_token['access_token'])
token_alg = token_headers['alg']
token_kid = token_headers['kid']
public_key = None
for key in keys:
    if key['kid'] == token_kid:
        public_key = key

现在我已经从Azure获取了正确的公钥来验证我的令牌,但问题是它是一个JWT密钥。在我能用它进行解码之前,需要将其转换为RSA PEM密钥。

from cryptography.hazmat.primitives import serialization
rsa_pem_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(public_key))
rsa_pem_key_bytes = rsa_pem_key.public_bytes(
    encoding=serialization.Encoding.PEM, 
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)
    

这是 Azure 公钥的外观:
b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyr3v1uETrFfT17zvOiy0\n1w8nO+1t67cmiZLZxq2ISDdte9dw+IxCR7lPV2wezczIRgcWmYgFnsk2j6m10H4t\nKzcqZM0JJ/NigY29pFimxlL7/qXMB1PorFJdlAKvp5SgjSTwLrXjkr1AqWwbpzG2\nyZUNN3GE8GvmTeo4yweQbNCd+yO/Zpozx0J34wHBEMuaw+ZfCUk7mdKKsg+EcE4Z\nv0Xgl9wP2MpKPx0V8gLazxe6UQ9ShzNuruSOncpLYJN/oQ4aKf5ptOp1rsfDY2IK\n9frtmRTKOdQ+MEmSdjGL/88IQcvCs7jqVz53XKoXRlXB8tMIGOcg+ICer6yxe2it\nIQIDAQAB\n-----END PUBLIC KEY-----\n'

我需要做的最后一件事是使用公钥验证令牌。

decoded_token = jwt.decode(
    my_token['access_token'], 
    key=rsa_pem_key_bytes,
    verify=True,
    algorithms=[token_alg],
    audience=[client_id],
    issuer="https://login.microsoftonline.com/consumers"
)

我收到的结果是:
jwt.exceptions.InvalidSignatureError: Signature verification failed

我也尝试过的方法

我还尝试了这个流行指南:如何验证由MS Azure AD生成的JWT id_token? 将x5c放入证书前后缀中会产生无效格式错误。

接下来是什么?

你们能看到明显的错误吗?我主要猜测是受众发行者出了问题,但我无法确定是什么问题,而微软的文档像往常一样很差。此外,在Azure的应用注册中有一个秘密密钥,但它似乎也不起作用。

更新

结果发现我的验证代码是正确的,但我试图验证错误的令牌。稍作修改后,我现在收到了一个可以解码和验证的id_token。


我的猜测是受众或发行者出了问题,错误的受众会导致 jwt.InvalidAudienceError。但是受众参数是可选的,因此在测试时将其删除并查看结果。 - jps
你知道发行者是否也是可选的吗?如果我只输入了密钥、秘密和算法,我会得到以下错误信息:[_OpenSSLErrorWithText(code=151584876, lib=9, reason=108, reason_text=b'error:0909006C:PEM routines:get_name:no start line')] - Esben Eickhardt
这是PyJWT,对吧?那么是的,issuer也是可选的。https://pyjwt.readthedocs.io/en/stable/usage.html#issuer-claim-iss。错误似乎与密钥有关。尝试像这里显示的方式[here](https://stackoverflow.com/a/64598545/7329832)。 - jps
是的,它就是 PyJWT。 - Esben Eickhardt
5
嗨@EsbenEickhardt,你能展示一下你对id_token的“轻微修改”吗? - Elliot13
4个回答

2
你已经获取了范围为https://graph.microsoft.com/.default的令牌,这意味着访问令牌是用于MS Graph API的。 你不应该验证它,这是它所针对的API的工作。 Graph API也很特殊,使用与其他API不同的签名方法。

1
那么我收到的令牌不需要进行验证吗?我想使用Azure AD进行访问控制,以便只有属于某个AD组的用户才能访问我的应用程序。MSAL文档仅提供了委派访问Graph API的示例。所以我的想法是,如果只有属于某个AD组的用户可以获取令牌,并且我可以验证这些令牌,那么我就可以获得访问权限。你有其他建议吗? :) - Esben Eickhardt
1
是的,我可以给你一些指针 :) 你能告诉我这是什么类型的应用程序吗?例如,这是一个Web应用程序吗?通常,一个应用程序将:验证用户身份,验证应用程序的ID令牌/访问令牌,(可选)检查令牌中的用户角色,(可选)检查令牌中的应用程序权限,(可选)通过令牌或MS Graph API检查用户组。 - juunas
1
听起来你的应用程序是一个没有单页应用程序(React、Angular等)前端的后端Web应用程序。这些应用程序通常使用授权代码OAuth流程来获取令牌。不幸的是,我没有使用过Python,所以我无法真正说出要使用哪些库。 - juunas
@EsbenEickhardt,我们希望您能提供一个已接受的答案或者您自己解决方案的一些细节。 - undefined
结果证明我的代码是正确的,只是我验证的是错误的键。 - undefined
显示剩余2条评论

0

尝试更改您的

scopes=['https://graph.microsoft.com/.default']

scopes:['[YOUR_CLIENT_ID]/.default']

0
至少有两种方法可以解码Microsoft Azure AD ID令牌:
选项1:使用jwt OP提供的代码给我抛出了InvalidIssuerError异常。即使将issuer参数替换为https://login.microsoftonline.com/{your-tenant-id},对我来说也没有起作用。然而,完全省略这个参数允许我解码ID令牌。以下是完整的代码:
# Get the public keys from Microsoft
import requests
response = requests.get("https://login.microsoftonline.com/common/discovery/keys")
keys = response.json()['keys']

# Format keys as PEM
from cryptography.hazmat.primitives import serialization
rsa_pem_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(public_key))
rsa_pem_key_bytes = rsa_pem_key.public_bytes(
    encoding=serialization.Encoding.PEM, 
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Get algorithm from token header
alg = jwt.get_unverified_header(your-id-token)['alg']

# Decode token
jwt.decode(
    your-id-token,
    key=rsa_pem_key_bytes,
    algorithms=[alg],
    verify=True,
    audience=[your-client-id],
    options={"verify_signature": True}
)

选项2:使用msaldecode_id_token

微软的msal包提供了一个解码身份令牌的函数。代码简单地变成:

from msal.oauth2cli.oidc import decode_id_token 
decode_id_token(id_token=your-token-id, client_id=your-client-id)

0

问题似乎与您生成的JWT与MS Graph API相关,结果JWT头中会包含“nonce”,并且不应该进行验证。

解决方法是使用范围不等于https://graph.microsoft.com/.default的JWT生成,而是以“scope”形式为“api:// client-id / .default”。

之后,您可以验证JWT头不再具有JWT头中的“nonce”,并且像上面的代码一样进行正常验证。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接