Skip to content

Commit

Permalink
Merge pull request #586 from azmeuk/rfc9068
Browse files Browse the repository at this point in the history
RFC9068 implementation
  • Loading branch information
lepture authored Nov 21, 2023
2 parents eea8c61 + d589d4f commit 701113f
Show file tree
Hide file tree
Showing 20 changed files with 1,610 additions and 28 deletions.
6 changes: 6 additions & 0 deletions BACKERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,11 @@ Jeff Heaton
</a><br>
Birk Jernström
</td>
<td align="center">
<a href="https://github.com/yaal-coop">
<img src="https://avatars.githubusercontent.com/u/7792703?v=4" alt="Yaal Coop" width="48" height="48">
</a><br>
Yaal Coop
</td>
</tr>
</table>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Generic, spec-compliant implementation to build clients and providers:
- [RFC7662: OAuth 2.0 Token Introspection](https://docs.authlib.org/en/latest/specs/rfc7662.html)
- [RFC8414: OAuth 2.0 Authorization Server Metadata](https://docs.authlib.org/en/latest/specs/rfc8414.html)
- [RFC8628: OAuth 2.0 Device Authorization Grant](https://docs.authlib.org/en/latest/specs/rfc8628.html)
- [RFC9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens](https://docs.authlib.org/en/latest/specs/rfc9068.html)
- [Javascript Object Signing and Encryption](https://docs.authlib.org/en/latest/jose/index.html)
- [RFC7515: JSON Web Signature](https://docs.authlib.org/en/latest/jose/jws.html)
- [RFC7516: JSON Web Encryption](https://docs.authlib.org/en/latest/jose/jwe.html)
Expand Down
18 changes: 12 additions & 6 deletions authlib/integrations/django_oauth2/resource_protector.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,32 @@


class ResourceProtector(_ResourceProtector):
def acquire_token(self, request, scopes=None):
def acquire_token(self, request, scopes=None, **kwargs):
"""A method to acquire current valid token with the given scope.
:param request: Django HTTP request instance
:param scopes: a list of scope values
:return: token object
"""
req = DjangoJsonRequest(request)
if isinstance(scopes, str):
scopes = [scopes]
token = self.validate_request(scopes, req)
# backward compatibility
kwargs['scopes'] = scopes
for claim in kwargs:
if isinstance(kwargs[claim], str):
kwargs[claim] = [kwargs[claim]]
token = self.validate_request(request=req, **kwargs)
token_authenticated.send(sender=self.__class__, token=token)
return token

def __call__(self, scopes=None, optional=False):
def __call__(self, scopes=None, optional=False, **kwargs):
claims = kwargs
# backward compatibility
claims['scopes'] = scopes
def wrapper(f):
@functools.wraps(f)
def decorated(request, *args, **kwargs):
try:
token = self.acquire_token(request, scopes)
token = self.acquire_token(request, **claims)
request.oauth_token = token
except MissingAuthorizationError as error:
if optional:
Expand Down
19 changes: 12 additions & 7 deletions authlib/integrations/flask_oauth2/resource_protector.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,19 @@ def raise_error_response(self, error):
headers = error.get_headers()
raise_http_exception(status, body, headers)

def acquire_token(self, scopes=None):
def acquire_token(self, scopes=None, **kwargs):
"""A method to acquire current valid token with the given scope.
:param scopes: a list of scope values
:return: token object
"""
request = FlaskJsonRequest(_req)
# backward compatible
if isinstance(scopes, str):
scopes = [scopes]
token = self.validate_request(scopes, request)
# backward compatibility
kwargs['scopes'] = scopes
for claim in kwargs:
if isinstance(kwargs[claim], str):
kwargs[claim] = [kwargs[claim]]
token = self.validate_request(request=request, **kwargs)
token_authenticated.send(self, token=token)
g.authlib_server_oauth2_token = token
return token
Expand All @@ -85,12 +87,15 @@ def user_api():
except OAuth2Error as error:
self.raise_error_response(error)

def __call__(self, scopes=None, optional=False):
def __call__(self, scopes=None, optional=False, **kwargs):
claims = kwargs
# backward compatibility
claims['scopes'] = scopes
def wrapper(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
try:
self.acquire_token(scopes)
self.acquire_token(**claims)
except MissingAuthorizationError as error:
if optional:
return f(*args, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions authlib/jose/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class InvalidClaimError(JoseError):
error = 'invalid_claim'

def __init__(self, claim):
self.claim_name = claim
description = f'Invalid claim "{claim}"'
super().__init__(description=description)

Expand Down
2 changes: 1 addition & 1 deletion authlib/jose/rfc7519/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def encode(self, header, payload, key, check=True):
:param check: check if sensitive data in payload
:return: bytes
"""
header['typ'] = 'JWT'
header.setdefault('typ', 'JWT')

for k in ['exp', 'iat', 'nbf']:
# convert datetime into timestamp
Expand Down
13 changes: 9 additions & 4 deletions authlib/oauth2/rfc6749/authorization_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,21 @@ def authenticate_user(self, credential):
if hasattr(grant_cls, 'check_token_endpoint'):
self._token_grants.append((grant_cls, extensions))

def register_endpoint(self, endpoint_cls):
def register_endpoint(self, endpoint):
"""Add extra endpoint to authorization server. e.g.
RevocationEndpoint::
authorization_server.register_endpoint(RevocationEndpoint)
:param endpoint_cls: A endpoint class
:param endpoint_cls: A endpoint class or instance.
"""
endpoints = self._endpoints.setdefault(endpoint_cls.ENDPOINT_NAME, [])
endpoints.append(endpoint_cls(self))
if isinstance(endpoint, type):
endpoint = endpoint(self)
else:
endpoint.server = self

endpoints = self._endpoints.setdefault(endpoint.ENDPOINT_NAME, [])
endpoints.append(endpoint)

def get_authorization_grant(self, request):
"""Find the authorization grant for current request.
Expand Down
4 changes: 2 additions & 2 deletions authlib/oauth2/rfc6749/resource_protector.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ def parse_request_authorization(self, request):
validator = self.get_token_validator(token_type)
return validator, token_string

def validate_request(self, scopes, request):
def validate_request(self, scopes, request, **kwargs):
"""Validate the request and return a token."""
validator, token_string = self.parse_request_authorization(request)
validator.validate_request(request)
token = validator.authenticate_token(token_string)
validator.validate_token(token, scopes, request)
validator.validate_token(token, scopes, request, **kwargs)
return token
10 changes: 6 additions & 4 deletions authlib/oauth2/rfc7009/revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,19 @@ def authenticate_token(self, request, client):
OPTIONAL. A hint about the type of the token submitted for
revocation.
"""
self.check_params(request, client)
token = self.query_token(request.form['token'], request.form.get('token_type_hint'))
if token and token.check_client(client):
return token

def check_params(self, request, client):
if 'token' not in request.form:
raise InvalidRequestError()

hint = request.form.get('token_type_hint')
if hint and hint not in self.SUPPORTED_TOKEN_TYPES:
raise UnsupportedTokenTypeError()

token = self.query_token(request.form['token'], hint)
if token and token.check_client(client):
return token

def create_endpoint_response(self, request):
"""Validate revocation request and create the response for revocation.
For example, a client may request the revocation of a refresh token
Expand Down
11 changes: 7 additions & 4 deletions authlib/oauth2/rfc7662/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ def authenticate_token(self, request, client):
**OPTIONAL** A hint about the type of the token submitted for
introspection.
"""

self.check_params(request, client)
token = self.query_token(request.form['token'], request.form.get('token_type_hint'))
if token and self.check_permission(token, client, request):
return token

def check_params(self, request, client):
params = request.form
if 'token' not in params:
raise InvalidRequestError()
Expand All @@ -42,10 +49,6 @@ def authenticate_token(self, request, client):
if hint and hint not in self.SUPPORTED_TOKEN_TYPES:
raise UnsupportedTokenTypeError()

token = self.query_token(params['token'], hint)
if token and self.check_permission(token, client, request):
return token

def create_endpoint_response(self, request):
"""Validate introspection request and create the response.
Expand Down
11 changes: 11 additions & 0 deletions authlib/oauth2/rfc9068/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .introspection import JWTIntrospectionEndpoint
from .revocation import JWTRevocationEndpoint
from .token import JWTBearerTokenGenerator
from .token_validator import JWTBearerTokenValidator

__all__ = [
'JWTBearerTokenGenerator',
'JWTBearerTokenValidator',
'JWTIntrospectionEndpoint',
'JWTRevocationEndpoint',
]
62 changes: 62 additions & 0 deletions authlib/oauth2/rfc9068/claims.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from authlib.jose.errors import InvalidClaimError
from authlib.jose.rfc7519 import JWTClaims


class JWTAccessTokenClaims(JWTClaims):
REGISTERED_CLAIMS = JWTClaims.REGISTERED_CLAIMS + [
'client_id',
'auth_time',
'acr',
'amr',
'scope',
'groups',
'roles',
'entitlements',
]

def validate(self, **kwargs):
self.validate_typ()

super().validate(**kwargs)
self.validate_client_id()
self.validate_auth_time()
self.validate_acr()
self.validate_amr()
self.validate_scope()
self.validate_groups()
self.validate_roles()
self.validate_entitlements()

def validate_typ(self):
# The resource server MUST verify that the 'typ' header value is 'at+jwt'
# or 'application/at+jwt' and reject tokens carrying any other value.
if self.header['typ'].lower() not in ('at+jwt', 'application/at+jwt'):
raise InvalidClaimError('typ')

def validate_client_id(self):
return self._validate_claim_value('client_id')

def validate_auth_time(self):
auth_time = self.get('auth_time')
if auth_time and not isinstance(auth_time, (int, float)):
raise InvalidClaimError('auth_time')

def validate_acr(self):
return self._validate_claim_value('acr')

def validate_amr(self):
amr = self.get('amr')
if amr and not isinstance(self['amr'], list):
raise InvalidClaimError('amr')

def validate_scope(self):
return self._validate_claim_value('scope')

def validate_groups(self):
return self._validate_claim_value('groups')

def validate_roles(self):
return self._validate_claim_value('roles')

def validate_entitlements(self):
return self._validate_claim_value('entitlements')
Loading

0 comments on commit 701113f

Please sign in to comment.