Skip to content

Commit d429c36

Browse files
authored
Merge pull request #749 from azmeuk/724-jar
Implement RFC9101 JWT secured authentication requests (JAR)
2 parents 4d1f3d9 + a524d23 commit d429c36

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1285
-272
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Generic, spec-compliant implementation to build clients and providers:
5151
- [RFC8414: OAuth 2.0 Authorization Server Metadata](https://docs.authlib.org/en/latest/specs/rfc8414.html)
5252
- [RFC8628: OAuth 2.0 Device Authorization Grant](https://docs.authlib.org/en/latest/specs/rfc8628.html)
5353
- [RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://docs.authlib.org/en/latest/specs/rfc9068.html)
54+
- [RFC9101: The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)](https://docs.authlib.org/en/latest/specs/rfc9101.html)
5455
- [RFC9207: OAuth 2.0 Authorization Server Issuer Identification](https://docs.authlib.org/en/latest/specs/rfc9207.html)
5556
- [Javascript Object Signing and Encryption](https://docs.authlib.org/en/latest/jose/index.html)
5657
- [RFC7515: JSON Web Signature](https://docs.authlib.org/en/latest/jose/jws.html)

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ Specifications
3030
- RFC7521: Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants
3131
- RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
3232
- RFC7591: OAuth 2.0 Dynamic Client Registration Protocol
33+
- RFC7592: OAuth 2.0 Dynamic Client Registration Management Protocol
3334
- RFC7636: Proof Key for Code Exchange by OAuth Public Clients
3435
- RFC7638: JSON Web Key (JWK) Thumbprint
3536
- RFC7662: OAuth 2.0 Token Introspection
3637
- RFC8037: CFRG Elliptic Curve Diffie-Hellman (ECDH) and Signatures in JSON Object Signing and Encryption (JOSE)
3738
- RFC8414: OAuth 2.0 Authorization Server Metadata
3839
- RFC8628: OAuth 2.0 Device Authorization Grant
40+
- RFC9101: The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)
41+
- RFC9207: OAuth 2.0 Authorization Server Issuer Identification
3942
- OpenID Connect 1.0
4043
- OpenID Connect Discovery 1.0
4144
- draft-madden-jose-ecdh-1pu-04: Public Key Authenticated Encryption for JOSE: ECDH-1PU

authlib/deprecate.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ class AuthlibDeprecationWarning(DeprecationWarning):
88
warnings.simplefilter("always", AuthlibDeprecationWarning)
99

1010

11-
def deprecate(message, version=None, link_uid=None, link_file=None):
11+
def deprecate(message, version=None, link_uid=None, link_file=None, stacklevel=3):
1212
if version:
1313
message += f"\nIt will be compatible before version {version}."
14+
1415
if link_uid and link_file:
1516
message += f"\nRead more <https://git.io/{link_uid}#file-{link_file}-md>"
16-
warnings.warn(AuthlibDeprecationWarning(message), stacklevel=2)
17+
18+
warnings.warn(AuthlibDeprecationWarning(message), stacklevel=stacklevel)

authlib/integrations/django_oauth2/requests.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,16 @@
44
from django.utils.functional import cached_property
55

66
from authlib.common.encoding import json_loads
7+
from authlib.oauth2.rfc6749 import JsonPayload
78
from authlib.oauth2.rfc6749 import JsonRequest
9+
from authlib.oauth2.rfc6749 import OAuth2Payload
810
from authlib.oauth2.rfc6749 import OAuth2Request
911

1012

11-
class DjangoOAuth2Request(OAuth2Request):
13+
class DjangoOAuth2Payload(OAuth2Payload):
1214
def __init__(self, request: HttpRequest):
13-
super().__init__(
14-
request.method, request.build_absolute_uri(), None, request.headers
15-
)
1615
self._request = request
1716

18-
@property
19-
def args(self):
20-
return self._request.GET
21-
22-
@property
23-
def form(self):
24-
return self._request.POST
25-
2617
@cached_property
2718
def data(self):
2819
data = {}
@@ -33,20 +24,38 @@ def data(self):
3324
@cached_property
3425
def datalist(self):
3526
values = defaultdict(list)
36-
for k in self.args:
37-
values[k].extend(self.args.getlist(k))
38-
for k in self.form:
39-
values[k].extend(self.form.getlist(k))
27+
for k in self._request.GET:
28+
values[k].extend(self._request.GET.getlist(k))
29+
for k in self._request.POST:
30+
values[k].extend(self._request.POST.getlist(k))
4031
return values
4132

4233

43-
class DjangoJsonRequest(JsonRequest):
34+
class DjangoOAuth2Request(OAuth2Request):
35+
def __init__(self, request: HttpRequest):
36+
super().__init__(request.method, request.build_absolute_uri(), request.headers)
37+
self.payload = DjangoOAuth2Payload(request)
38+
self._request = request
39+
40+
@property
41+
def args(self):
42+
return self._request.GET
43+
44+
@property
45+
def form(self):
46+
return self._request.POST
47+
48+
49+
class DjangoJsonPayload(JsonPayload):
4450
def __init__(self, request: HttpRequest):
45-
super().__init__(
46-
request.method, request.build_absolute_uri(), None, request.headers
47-
)
4851
self._request = request
4952

5053
@cached_property
5154
def data(self):
5255
return json_loads(self._request.body)
56+
57+
58+
class DjangoJsonRequest(JsonRequest):
59+
def __init__(self, request: HttpRequest):
60+
super().__init__(request.method, request.build_absolute_uri(), request.headers)
61+
self.payload = DjangoJsonPayload(request)

authlib/integrations/flask_oauth2/requests.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,16 @@
33

44
from flask.wrappers import Request
55

6+
from authlib.oauth2.rfc6749 import JsonPayload
67
from authlib.oauth2.rfc6749 import JsonRequest
8+
from authlib.oauth2.rfc6749 import OAuth2Payload
79
from authlib.oauth2.rfc6749 import OAuth2Request
810

911

10-
class FlaskOAuth2Request(OAuth2Request):
12+
class FlaskOAuth2Payload(OAuth2Payload):
1113
def __init__(self, request: Request):
12-
super().__init__(request.method, request.url, None, request.headers)
1314
self._request = request
1415

15-
@property
16-
def args(self):
17-
return self._request.args
18-
19-
@property
20-
def form(self):
21-
return self._request.form
22-
2316
@property
2417
def data(self):
2518
return self._request.values
@@ -32,11 +25,31 @@ def datalist(self):
3225
return values
3326

3427

35-
class FlaskJsonRequest(JsonRequest):
28+
class FlaskOAuth2Request(OAuth2Request):
29+
def __init__(self, request: Request):
30+
super().__init__(request.method, request.url, request.headers)
31+
self._request = request
32+
self.payload = FlaskOAuth2Payload(request)
33+
34+
@property
35+
def args(self):
36+
return self._request.args
37+
38+
@property
39+
def form(self):
40+
return self._request.form
41+
42+
43+
class FlaskJsonPayload(JsonPayload):
3644
def __init__(self, request: Request):
37-
super().__init__(request.method, request.url, None, request.headers)
3845
self._request = request
3946

4047
@property
4148
def data(self):
4249
return self._request.get_json()
50+
51+
52+
class FlaskJsonRequest(JsonRequest):
53+
def __init__(self, request: Request):
54+
super().__init__(request.method, request.url, request.headers)
55+
self.payload = FlaskJsonPayload(request)

authlib/oauth2/rfc6749/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
from .models import AuthorizationCodeMixin
3737
from .models import ClientMixin
3838
from .models import TokenMixin
39+
from .requests import JsonPayload
3940
from .requests import JsonRequest
41+
from .requests import OAuth2Payload
4042
from .requests import OAuth2Request
4143
from .resource_protector import ResourceProtector
4244
from .resource_protector import TokenValidator
@@ -46,8 +48,10 @@
4648
from .wrappers import OAuth2Token
4749

4850
__all__ = [
51+
"OAuth2Payload",
4952
"OAuth2Token",
5053
"OAuth2Request",
54+
"JsonPayload",
5155
"JsonRequest",
5256
"OAuth2Error",
5357
"AccessDeniedError",

authlib/oauth2/rfc6749/authenticate_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ def authenticate_none(query_client, request):
8989
"""Authenticate public client by ``none`` method. The client
9090
does not have a client secret.
9191
"""
92-
client_id = request.client_id
93-
if client_id and not request.data.get("client_secret"):
92+
client_id = request.payload.client_id
93+
if client_id and not request.payload.data.get("client_secret"):
9494
client = _validate_client(query_client, client_id)
9595
log.debug(f'Authenticate {client_id} via "none" success')
9696
return client

authlib/oauth2/rfc6749/authorization_server.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
from authlib.common.errors import ContinueIteration
2+
from authlib.deprecate import deprecate
23

34
from .authenticate_client import ClientAuthentication
45
from .errors import InvalidScopeError
56
from .errors import OAuth2Error
67
from .errors import UnsupportedGrantTypeError
78
from .errors import UnsupportedResponseTypeError
9+
from .hooks import Hookable
10+
from .hooks import hooked
811
from .requests import JsonRequest
912
from .requests import OAuth2Request
1013
from .util import scope_to_list
1114

1215

13-
class AuthorizationServer:
16+
class AuthorizationServer(Hookable):
1417
"""Authorization server that handles Authorization Endpoint and Token
1518
Endpoint.
1619
1720
:param scopes_supported: A list of supported scopes by this authorization server.
1821
"""
1922

2023
def __init__(self, scopes_supported=None):
24+
super().__init__()
2125
self.scopes_supported = scopes_supported
2226
self._token_generators = {}
2327
self._client_auth = None
2428
self._authorization_grants = []
2529
self._token_grants = []
2630
self._endpoints = {}
31+
self._extensions = []
2732

2833
def query_client(self, client_id):
2934
"""Query OAuth client by client_id. The client model class MUST
@@ -146,6 +151,9 @@ def authenticate_client_via_custom(query_client, request):
146151

147152
self._client_auth.register(method, func)
148153

154+
def register_extension(self, extension):
155+
self._extensions.append(extension(self))
156+
149157
def get_error_uri(self, request, error):
150158
"""Return a URI for the given error, framework may implement this method."""
151159
return None
@@ -222,6 +230,7 @@ def register_endpoint(self, endpoint):
222230
endpoints = self._endpoints.setdefault(endpoint.ENDPOINT_NAME, [])
223231
endpoints.append(endpoint)
224232

233+
@hooked
225234
def get_authorization_grant(self, request):
226235
"""Find the authorization grant for current request.
227236
@@ -233,9 +242,9 @@ def get_authorization_grant(self, request):
233242
return _create_grant(grant_cls, extensions, request, self)
234243

235244
raise UnsupportedResponseTypeError(
236-
f"The response type '{request.response_type}' is not supported by the server.",
237-
request.response_type,
238-
redirect_uri=request.redirect_uri,
245+
f"The response type '{request.payload.response_type}' is not supported by the server.",
246+
request.payload.response_type,
247+
redirect_uri=request.payload.redirect_uri,
239248
)
240249

241250
def get_consent_grant(self, request=None, end_user=None):
@@ -254,7 +263,7 @@ def get_consent_grant(self, request=None, end_user=None):
254263
# REQUIRED if a "state" parameter was present in the client
255264
# authorization request. The exact value received from the
256265
# client.
257-
error.state = request.state
266+
error.state = request.payload.state
258267
raise
259268
return grant
260269

@@ -267,7 +276,7 @@ def get_token_grant(self, request):
267276
for grant_cls, extensions in self._token_grants:
268277
if grant_cls.check_token_endpoint(request):
269278
return _create_grant(grant_cls, extensions, request, self)
270-
raise UnsupportedGrantTypeError(request.grant_type)
279+
raise UnsupportedGrantTypeError(request.payload.grant_type)
271280

272281
def create_endpoint_response(self, name, request=None):
273282
"""Validate endpoint request and create endpoint response.
@@ -289,7 +298,8 @@ def create_endpoint_response(self, name, request=None):
289298
except OAuth2Error as error:
290299
return self.handle_error_response(request, error)
291300

292-
def create_authorization_response(self, request=None, grant_user=None):
301+
@hooked
302+
def create_authorization_response(self, request=None, grant_user=None, grant=None):
293303
"""Validate authorization request and create authorization response.
294304
295305
:param request: HTTP request instance.
@@ -300,18 +310,20 @@ def create_authorization_response(self, request=None, grant_user=None):
300310
if not isinstance(request, OAuth2Request):
301311
request = self.create_oauth2_request(request)
302312

303-
try:
304-
grant = self.get_authorization_grant(request)
305-
except UnsupportedResponseTypeError as error:
306-
error.state = request.state
307-
return self.handle_error_response(request, error)
313+
if not grant:
314+
deprecate("The 'grant' parameter will become mandatory.", version="1.8")
315+
try:
316+
grant = self.get_authorization_grant(request)
317+
except UnsupportedResponseTypeError as error:
318+
error.state = request.payload.state
319+
return self.handle_error_response(request, error)
308320

309321
try:
310322
redirect_uri = grant.validate_authorization_request()
311323
args = grant.create_authorization_response(redirect_uri, grant_user)
312324
response = self.handle_response(*args)
313325
except OAuth2Error as error:
314-
error.state = request.state
326+
error.state = request.payload.state
315327
response = self.handle_error_response(request, error)
316328

317329
grant.execute_hook("after_authorization_response", response)

0 commit comments

Comments
 (0)