【免费下载链接】authlib
The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS, JWE, JWK, JWA, JWT included.
本文是 Authlib 对 RFC 7523(JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants)的完整实现指南。RFC 7523 定义了两种能力:一是把 JWT 作为 OAuth 2.0 授权 Grant(assertion)直接换取访问令牌;二是把 JWT 作为客户端认证方式(client_secret_jwt/private_key_jwt)替代传统的 client secret 直传。读完本文,你将掌握在 Authlib 授权服务器上注册JWTBearerGrant、扩展客户端认证方法,以及在 OAuth2Session 客户端上签发 JWT 断言并获取令牌的完整实战方案,并理解其底层校验流程与测试验证方式。
一、RFC 7523 概览:它解决什么问题
RFC 7523 全称JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants,它把 RFC 7519 的 JWT 与 OAuth 2.0 体系打通,主要解决两类场景:
- JWT 作为授权 Grant:客户端直接向令牌端点提交一个 JWT(
assertion参数),授权服务器验签并解析 claims 后签发访问令牌。典型应用是服务器到服务器(service-to-service)的凭证交换,例如 Google 服务账号模式。 - JWT 作为客户端认证:客户端用
client_secret(HMAC)或自己的私钥(RSA/EC)对一段 JWT 签名,通过client_assertion参数向令牌端点证明身份,对应 OpenID Connect 中定义的client_secret_jwt与private_key_jwt两种方法。
在 Authlib 中,该规范由 authlib/oauth2/rfc7523/ 模块实现,对外导出以下核心 API(见 模块__all__):
- 服务端授权 Grant:
JWTBearerGrant - 服务端客户端认证:
JWTBearerClientAssertion - 客户端签名辅助:
client_secret_jwt_sign、private_key_jwt_sign、ClientSecretJWT、PrivateKeyJWT - 令牌相关:
JWTBearerToken、JWTBearerTokenGenerator、JWTBearerTokenValidator
二、使用 JWT 作为授权 Grant(JWTBearerGrant)
2.1 基类与 Grant 类型标识
JWTBearerGrant定义在 jwt_bearer.py,继承自 RFC 6749 的BaseGrant与TokenEndpointMixin,因此它的注册与使用方式与 RFC 6749 内建 Grant 完全一致——可以通过AuthorizationServer.register_grant注册(对应 authorization_server.py)。
它的 Grant 类型标识为:
JWT_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"即客户端在令牌端点提交grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer时,命中该 Grant。
2.2 完整示例:实现 JWTBearerGrant 子类
原文档给出的基类模板要求开发者必须实现若干缺失方法。以下代码来自 RFC 7523 文档 的示例,并结合源码补全注释:
from joserfc.jwk import KeySet from authlib.oauth2.rfc7523 import JWTBearerGrant as _JWTBearerGrant class JWTBearerGrant(_JWTBearerGrant): def get_audiences(self): # Per RFC 7523 Section 3, both the token endpoint URL and the # authorization server's issuer identifier are valid audience values. return ['https://example.com/oauth/token', 'https://example.com'] def resolve_issuer_client(self, issuer): # 使用 client_id 作为 issuer 时,直接按 issuer 查客户端 return Client.objects.get(client_id=issuer) def resolve_client_public_key(self, client): # 若客户端表有 jwks 列,把 JWK Set 解析为密钥集合用于验签 key_set = KeySet.import_key_set(client.jwks) return key_set def authenticate_user(self, subject): # 断言中包含 sub 时,用 sub 值定位用户(例如 sub 是 email) return User.objects.get(email=subject) def has_granted_permission(self, client, user): # 校验客户端是否拥有访问用户资源的授权 # 例如存在 UserGrant 表记录用户对客户端的授权 grant = UserGrant.objects.get(client_id=client.client_id, user_id=user.id) if grant: return grant.enabled return False # 注册到授权服务器 authorization_server.register_grant(JWTBearerGrant)创建客户端时,授权服务器会生成若干密钥对,服务器自身只需保留公钥(或 JWK Set),用于解码断言值;私钥由客户端妥善保管用于签名。
2.3 必须实现的抽象方法逐一解析
从 jwt_bearer.py 的源码可以看到,以下方法在基类中抛出NotImplementedError(),子类必须覆写:
| 方法 | 职责 | 返回说明 |
|---|---|---|
resolve_issuer_client(issuer) | 根据断言中的iss查找客户端 | 返回 Client 实例;返回None时授权服务器抛出invalid_grant("The client does not exist on this server.") |
resolve_client_public_key(client) | 解析客户端公钥用于解码断言 | 返回OctKey、RSAKey、ECKey、OKPKey或KeySet实例 |
authenticate_user(subject) | 根据断言中的sub定位用户 | 返回 User 实例;返回空值时抛出invalid_grant("Invalid 'sub' value in assertion") |
has_granted_permission(client, user) | 校验客户端是否有权访问该用户资源 | 返回布尔值 |
get_audiences() | 返回本授权服务器接受的有效aud列表 | 列表为空时跳过 aud 值校验(仅检查存在性),见 2.4 节 |
需要特别注意的是,在 v1.0.0 中这些未实现方法发生过一次变更(versionchanged:: v1.0.0),因此以当前仓库源码中的方法签名为准;此外 v1.8 起已用resolve_client_public_key取代早期的resolve_client_key(旧方法仍在兼容期,会输出 deprecation 警告,见 process_assertion_claims)。
2.4 断言校验流程与安全参数(CLAIMS_OPTIONS / LEEWAY)
基类内置了一组默认的 claims 校验选项与时钟偏移容差(见 jwt_bearer.py):
CLAIMS_OPTIONS = { "iss": {"essential": True}, "aud": {"essential": True}, "exp": {"essential": True}, } LEEWAY = 60 # 允许的时钟偏移秒数,默认 60 秒verify_claims(jwt_bearer.py)会把这些选项与get_audiences()合并成jwt.JWTClaimsRegistry并执行校验:
iss、aud、exp默认均为必需(essential);- 若
get_audiences()返回非空列表,aud会升级为{"essential": True, "values": audiences},即断言中的aud必须命中列表中的某个值——这正是 RFC 7523 Section 3 要求的"授权服务器必须拒绝任何不以其自身为受众的 JWT"; - 若返回空列表,v1.8 起会输出 deprecation 警告,提示受众校验未来将成为强制项;
- 任何
JoseError都会被转换为 OAuth 2.0 的invalid_grant错误响应。
整个请求处理流程(validate_token_request,见 jwt_bearer.py)大致为:
- 从表单取
assertion参数,缺失则抛invalid_request; extract_assertion用jws.extract_compact拆解 JWT 并解析 payload(要求 payload 必须是 JSON 对象);- 读取
iss(此时尚未验签,不能假定其存在或为字符串),据此调用resolve_issuer_client定位客户端; - 用
resolve_client_public_key得到的密钥执行jwt.decode验签; verify_claims校验iss/aud/exp等 claims;- 校验
client.check_grant_type(self.GRANT_TYPE),未授权则抛unauthorized_client; - 若断言含
sub,调用authenticate_user定位用户,并调用has_granted_permission做权限检查; - 校验请求的 scope。
请求参数格式遵循 RFC 7523 Section 2.1,示例 HTTP 请求如下(源码 docstring 中的原始示例):
POST /token.oauth2 HTTP/1.1 Host: as.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer &assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0. eyJpc3Mi[...omitted for brevity...]. J9l-ZhwP[...omitted for brevity...]校验通过后,create_token_response(jwt_bearer.py)会生成令牌并返回200响应,默认include_refresh_token=False(JWT 断言模式不签发刷新令牌)。
2.5 扩展:将访问令牌也输出为 JWT 格式
作为锦上添花的进阶方案,JWTBearerTokenGenerator(token.py)可以把 jwt-bearer grant 签发的访问令牌直接生成为 JWT 格式,并注册到授权服务器:
authorization_server.register_token_generator( "urn:ietf:params:oauth:grant-type:jwt-bearer", JWTBearerTokenGenerator(private_rsa_key), )其构造参数包括:
secret_key:私钥(bytes / JWK / JWK Set);issuer:可选的发行者标识,会写入issclaim;alg:JWT 签名算法,默认RS256。
生成的令牌 payload 包含scope、grant_type、iat、exp、client_id,可选的iss与sub(通过覆写get_sub_value定制 sub 取值)。由于 JWT 令牌短期有效,可以不落库,此时可让子类重写save_token为空操作:
class MyJWTBearerGrant(JWTBearerGrant): def save_token(self, token): pass对应地,资源服务器侧可用JWTBearerTokenValidator(validator.py)验证这类 JWT 格式的 Bearer 令牌,其构造参数为public_key、可选issuer、realm、leeway=60,默认强制校验exp、client_id、grant_type三个 claims。
三、使用 JWT 作为客户端认证
3.1 两种认证方法的由来
在 RFC 6749 中,Authlib 提供了三种内建客户端认证方法:none、client_secret_post、client_secret_basic。借助 Assertion Framework,可以扩展更多认证方法。本节新增两种:client_secret_jwt与private_key_jwt。
注意:RFC 7523 本身并未定义这两个名字,它们来自 OpenID Connect 规范的 ClientAuthentication 章节。在 Authlib 中,二者共享同一套实现逻辑——都是把 JWT 作为client_assertion提交,区别仅在签名密钥:用客户端client_secret做 HMAC 签名的叫client_secret_jwt;用客户端私钥(RSA/EC 等)签名的叫private_key_jwt。
3.2 服务端实现:JWTBearerClientAssertion
AuthorizationServer提供了register_client_auth_method(见 authorization_server.py)用于扩展客户端认证方法。服务端自定义认证逻辑时,直接继承JWTBearerClientAssertion:
class JWTClientAuth(JWTBearerClientAssertion): def get_audiences(self): # Per RFC 7523 Section 3, both the token endpoint URL and the # authorization server's issuer identifier are valid audience values. return ['https://example.com/oauth/token', 'https://example.com'] def validate_jti(self, claims, jti): # validate_jti 是 OpenID Connect 的强制要求,RFC 7523 中可选 # 用缓存校验 jti 是否被使用过,防止重放攻击 key = 'jti:{}-{}'.format(claims['sub'], jti) if cache.get(key): return False cache.set(key, 1, timeout=3600) return True def resolve_client_public_key(self, client, headers): if headers['alg'] == 'HS256': return client.client_secret if headers['alg'] == 'RS256': return client.public_key # 你可以在此支持其他 alg 值 authorization_server.register_client_auth_method( JWTClientAuth.CLIENT_AUTH_METHOD, JWTClientAuth() )关键常量与逻辑(见 client.py):
CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" CLIENT_AUTH_METHOD = "client_assertion_jwt"get_audiences返回客户端断言 JWT 中可接受的aud值列表。按 RFC 7523 Section 3,令牌端点 URL 与授权服务器 issuer 标识都是合法受众;validate_jti负责jti去重用(构造JWTClientAuth(validate_jti=False)可关闭强制校验,见 client.py);resolve_client_public_key根据 JWT header 中的alg决定用哪种密钥验签。
verify_claims(client.py)对客户端断言强制校验:iss、sub、aud、exp均为必需,且iss与sub必须相等并等于 client_id;开启 jti 校验时jti缺失或重复都会导致invalid_client。认证成功后,authenticate_client还会检查该客户端是否被允许使用client_assertion_jwt方法访问令牌端点(client.check_endpoint_auth_method)。
关于CLIENT_AUTH_METHOD的取值:你可能注意到它的值是client_assertion_jwt,而不是client_secret_jwt或private_key_jwt——因为二者的校验逻辑完全相同,client_secret_jwt与private_key_jwt的区别只体现在resolve_client_public_key中对alg的分支处理上:
alg为 MAC 类(如HS256)→ 用客户端client_secret作为验签密钥,即client_secret_jwt;alg为RS256等非对称算法 → 用客户端公钥验签,即private_key_jwt。
如果只在resolve_client_public_key中接受HS256,那么可以显式把类属性改为CLIENT_AUTH_METHOD = 'client_secret_jwt'以表达更精确的语义。
让 grant 支持该认证方法:注册客户端认证方法后,还需把它加入目标 Grant 的TOKEN_ENDPOINT_AUTH_METHODS白名单,例如授权码 Grant:
from authlib.oauth2.rfc6749 import grants class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): TOKEN_ENDPOINT_AUTH_METHODS = [ 'client_secret_basic', JWTClientAuth.CLIENT_AUTH_METHOD, ] # ...这一调用链在 tests/flask/test_oauth2/test_jwt_bearer_client_auth.py 中有完整验证:测试里把JWTBearerClientAssertion.CLIENT_AUTH_METHOD加入ClientCredentialsGrant的TOKEN_ENDPOINT_AUTH_METHODS,并通过server.register_client_auth_method注册后,用client_assertion_type/client_assertion表单参数发起令牌请求,覆盖了缺失断言、签名错误、jti 重放等异常分支。
3.3 客户端实现:在 OAuth2Session 中注册 JWT 认证
Authlib 的 RFC 7523 实现为 OAuth 2 Session 提供client_secret_jwt与private_key_jwt两种客户端认证方法。
注册client_secret_jwt:
from authlib.oauth2.rfc7523 import ClientSecretJWT from authlib.integrations.requests_client import OAuth2Session session = OAuth2Session( 'your-client-id', 'your-client-secret', token_endpoint_auth_method='client_secret_jwt' ) token_endpoint = 'https://example.com/oauth/token' session.register_client_auth_method(ClientSecretJWT(token_endpoint)) session.fetch_token(token_endpoint)注册private_key_jwt:
from authlib.oauth2.rfc7523 import PrivateKeyJWT with open('your-private-key.pem', 'rb') as f: private_key = f.read() session = OAuth2Session( 'your-client-id', private_key, token_endpoint_auth_method='private_key_jwt' # NOTICE HERE ) token_endpoint = 'https://example.com/oauth/token' session.register_client_auth_method(PrivateKeyJWT(token_endpoint)) session.fetch_token(token_endpoint)两个类的可配置项(见 auth.py):
| 类 | name | 默认 alg | 签名密钥来源 |
|---|---|---|---|
ClientSecretJWT | client_secret_jwt | HS256 | 客户端的client_secret(自动导入为OctKey) |
PrivateKeyJWT | private_key_jwt | RS256 | 客户端私钥(支持RSAKey/ECKey/OKPKey,否则自动按 RSA 导入) |
两者构造参数均为token_endpoint、claims(额外 JWT claims)、headers(额外 JWT header,如kid、jku)、alg。register_client_auth_method以auth.name为键注册(见 client.py),令牌请求时按token_endpoint_auth_method找到对应认证器,把client_assertion_type与client_assertion两个参数追加到表单体中(见 auth.py)。
底层签名函数client_secret_jwt_sign/private_key_jwt_sign(assertion.py)生成的 JWT 断言遵循以下约定:
iss与sub均为client_id;aud为令牌端点 URL(构造时传入的token_endpoint,未传则取当前请求 URI);exp默认为iat + 3600秒(可通过expires_in调整);- 自动生成 36 位随机
jti(也可在claims中显式指定),用于防重放。
这些行为在 tests/core/test_oauth2/test_rfc7523_client_secret.py 与 tests/core/test_oauth2/test_rfc7523_private_key.py 中有详尽断言,包括默认参数、自定义alg、自定义 claims/headers、自定义 jti 等场景;tests/clients/test_requests/test_oauth2_session.py 则验证了在OAuth2Session中分别以字符串密钥与 JWK 对象两种形式使用ClientSecretJWT/PrivateKeyJWT发起fetch_token的完整链路。
更简单的断言客户端:AssertionSession
如果目标是"JWT 作为授权 Grant"的客户端(而非客户端认证),可以直接使用现成的断言会话:requests体系对应authlib.integrations.requests_client.AssertionSession,httpx体系对应authlib.integrations.httpx_client.AssertionSession(及异步版AsyncAssertionSession)。它们内部通过JWTBearerGrant.sign静态方法(jwt_bearer.py)自动生成 JWT 断言并请求令牌,支持token_endpoint、issuer、subject、audience、claims、token_placement、scope等参数,见 assertion_session.py 与 httpx 版 assertion_client.py。
四、API 参考摘要
authlib.oauth2.rfc7523对外公开的 API 汇总(对应 模块__all__):
JWTBearerGrant:JWT 授权 Grant 基类,覆写resolve_issuer_client、resolve_client_public_key、authenticate_user、has_granted_permission、get_audiences后注册使用;JWTBearerClientAssertion:JWT 客户端认证实现基类,覆写get_audiences、validate_jti、resolve_client_public_key后注册使用;ClientSecretJWT/PrivateKeyJWT:OAuth2Session 客户端侧的 JWT 认证器(client_secret_jwt/private_key_jwt);client_secret_jwt_sign/private_key_jwt_sign:底层断言签名函数;JWTBearerToken/JWTBearerTokenValidator:JWT 格式 Bearer 令牌的资源服务器校验组件;JWTBearerTokenGenerator:为 jwt-bearer grant 生成 JWT 格式访问令牌的生成器。
五、更多相关资源
- RFC 6749 授权框架在 Authlib 中的完整说明见 RFC 6749 文档,
JWTBearerGrant的注册、令牌生成、错误处理机制均复用了该框架; - 客户端断言会话与 OAuth2Session 的通用用法见 OAuth 2 Session 指南;
- 服务端验证可参考 Flask 集成测试 test_jwt_bearer_client_auth.py 以及核心单元测试 test_rfc7523_client_secret.py、test_rfc7523_private_key.py;
- 注意 v1.0.0 之后未实现方法签名有调整,v1.8 起
resolve_client_public_key取代resolve_client_key、get_audiences空列表将弃用,升级时请以当前源码签名为准。
【免费下载链接】authlib
The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS, JWE, JWK, JWA, JWT included.
相关推荐
用chroma.js打造专业数据可视化配色:ColorBrewer调色板+分位数分档实战
用chroma.js打造专业数据可视化配色:ColorBrewer调色板+分位数分档实战 做数据可视化时,"颜色怎么配"往往是最头疼的环节。 chroma.js
RabbitMQ OAuth 2.0 (JWT) 认证授权后端插件实战指南:rabbitmq_auth_backend_oauth2 完整解析
RabbitMQ OAuth 2.0 JWT 认证授权后端插件实战指南:rabbitmq_auth_backend_oauth2 完整解析 本文是面向 Rabb
后端消息队列消息路由Apereo CAS OpenID Connect JWT Bearer 授权模式(JWT Authorization Grant)接入指南
Apereo CAS OpenID Connect JWT Bearer 授权模式(JWT Authorization Grant)接入指南 JWT Beare
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考