jwt - 如何验证 Microsoft jwt id_token?
问题描述
我正在使用从 Microsoft 到客户端的 jwt 令牌来验证从它到 Web API(服务器)的请求。我可以控制客户端(js)和服务器(Python)的代码。
在客户端,我使用以下请求获取令牌(用户通过租户上的密码/2FA 声明):
`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()}`
这里guid
有一个独特的价值,TENANT_ID
是租户,CLIENT_ID
也是客户。
获得此令牌后,我将其作为授权标头发送,如下所示:
init = {
headers: {
'Authorization': `Bearer ${token}`,
}
}
return fetch(url, init).then(response => {
return response.json()
})
然后在服务器上检索令牌并验证它:
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)
jwk
的内容在哪里https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys
。
整个流程一直有效,直到验证失败,并出现以下错误:
authlib.jose.errors.InvalidHeaderParameterName: invalid_header_parameter_name: Invalid Header Parameter Names: nonce
这表明令牌的标头包含一个密钥nonce
(我验证了它)。
在此处查看Microsoft 的文档,标头上没有对 anonce
的引用——只是在有效负载上。
Q1:我在这里做错了什么?
Q2:假设微软是把 nonce 放在错误的地方(头而不是有效负载)的人,是否可以在将 nonce 传递给 jose 的身份验证库之前从头(在服务器端)中删除它?这样做安全吗?
解决方案
请参阅:https ://robertoprevato.github.io/Validating-JWT-Bearer-tokens-from-Azure-AD-in-Python/
以下是我在 API 中验证 Azure AD Id_tokens 的方法:
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!')
推荐阅读
- javascript - 为什么我的 `lyrics` 变量在 async/await 函数之外返回一个空字符串
- html - 执行 Thymeleaf :从变量插入
- distribution - 如何在时间表之外获得可变的乘客到达率?
- javascript - 为什么当我运行 javascript 函数时我的 RAM 会过载?
- php - WordPress:保护/转义文件内容的输出 - 在 PHP 中通过 @fread() 读取
- typescript - 如何将 Javascript 对象转换为 typescript 接口
- mpi - Openmpi 4.0.5 无法将任务分发到超过 1 个节点
- r - 查找向量的统计模式:当有多个模式时 - 返回最后一个模式
- discord.js - 信号:分段错误(核心转储)
- asp.net - 为 ASP.Net Web 应用程序自定义 azure 403 Forbidden 错误页面