From feebf4186db50eaaf14306ea35d073da6a1db1f2 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 26 Jun 2018 19:28:02 -0400 Subject: [PATCH 1/9] work around race condition in which two threads both try to create the same AccessToken --- oauth2_provider/oauth2_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 450a04fb5..18ffea8eb 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -335,7 +335,7 @@ def _get_token_from_authentication_server( try: access_token = AccessToken.objects.select_related("application", "user").get(token=token) except AccessToken.DoesNotExist: - access_token = AccessToken.objects.create( + access_token, _created = AccessToken.objects.get_or_create( token=token, user=user, application=None, From d68d5943c636f7a55bbf13fff673a1932caf5b1d Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Tue, 26 Jun 2018 19:58:01 -0400 Subject: [PATCH 2/9] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afbf34f3c..d98912479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 1.2.x [unrealeased] + +* Fix a race condition in creation of AccessToken with external oauth2 server. + ### 1.2.0 [2018-06-03] * **Compatibility**: Python 3.4 is the new minimum required version. From 4a86d7c055ec90f82c21cb25d5ad48526e2adcda Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Fri, 27 Jul 2018 11:23:54 -0400 Subject: [PATCH 3/9] change to prevent race condition as suggested by @jdelic --- oauth2_provider/oauth2_validators.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 18ffea8eb..5db6d648d 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -332,21 +332,9 @@ def _get_token_from_authentication_server( scope = content.get("scope", "") expires = make_aware(expires) - try: - access_token = AccessToken.objects.select_related("application", "user").get(token=token) - except AccessToken.DoesNotExist: - access_token, _created = AccessToken.objects.get_or_create( - token=token, - user=user, - application=None, - scope=scope, - expires=expires - ) - else: - access_token.expires = expires - access_token.scope = scope - access_token.save() - + access_token = AccessToken.objects.select_related().get_or_create(token=token, user=user, + application=None, scope=scope, + expires=expires) return access_token def validate_bearer_token(self, token, scopes, request): From 86ca3799fde7fd499352145c0d4fdad7bcf32dde Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 28 Jul 2018 16:21:45 -0400 Subject: [PATCH 4/9] Revert "change to prevent race condition as suggested by @jdelic" This reverts commit 4a86d7c055ec90f82c21cb25d5ad48526e2adcda. --- oauth2_provider/oauth2_validators.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 5db6d648d..18ffea8eb 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -332,9 +332,21 @@ def _get_token_from_authentication_server( scope = content.get("scope", "") expires = make_aware(expires) - access_token = AccessToken.objects.select_related().get_or_create(token=token, user=user, - application=None, scope=scope, - expires=expires) + try: + access_token = AccessToken.objects.select_related("application", "user").get(token=token) + except AccessToken.DoesNotExist: + access_token, _created = AccessToken.objects.get_or_create( + token=token, + user=user, + application=None, + scope=scope, + expires=expires + ) + else: + access_token.expires = expires + access_token.scope = scope + access_token.save() + return access_token def validate_bearer_token(self, token, scopes, request): From ee55818ccb85e0ab3e2ef8ab79d0db57a9e0ca11 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 28 Jul 2018 16:22:23 -0400 Subject: [PATCH 5/9] Revert "work around race condition in which two threads both try to create the same AccessToken" This reverts commit feebf4186db50eaaf14306ea35d073da6a1db1f2. --- oauth2_provider/oauth2_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 18ffea8eb..450a04fb5 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -335,7 +335,7 @@ def _get_token_from_authentication_server( try: access_token = AccessToken.objects.select_related("application", "user").get(token=token) except AccessToken.DoesNotExist: - access_token, _created = AccessToken.objects.get_or_create( + access_token = AccessToken.objects.create( token=token, user=user, application=None, From 1da2a0a889c0d75fa00d23c1c36fcd06ddb0786e Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sat, 28 Jul 2018 17:37:43 -0400 Subject: [PATCH 6/9] corrected get_or_create --- oauth2_provider/oauth2_validators.py | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 450a04fb5..b4c2e968b 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -332,20 +332,21 @@ def _get_token_from_authentication_server( scope = content.get("scope", "") expires = make_aware(expires) - try: - access_token = AccessToken.objects.select_related("application", "user").get(token=token) - except AccessToken.DoesNotExist: - access_token = AccessToken.objects.create( - token=token, - user=user, - application=None, - scope=scope, - expires=expires - ) - else: - access_token.expires = expires - access_token.scope = scope - access_token.save() + with transaction.atomic(): + access_token, _created = AccessToken\ + .objects.select_related("application", "user")\ + .select_for_update()\ + .get_or_create(token=token, + defaults={ + "user": user, + "application": None, + "scope": scope, + "expires": expires, + }) + if not _created: + access_token.scope = scope + access_token.expires = expires + access_token.save() return access_token From 5d466dd9040fee1cd56718b4751cc7bfe34f7225 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sun, 29 Jul 2018 13:40:34 -0400 Subject: [PATCH 7/9] update_or_create instead of two steps go get_or_create then update + save --- oauth2_provider/oauth2_validators.orig | 629 +++++++++++++++++++++++++ oauth2_provider/oauth2_validators.py | 69 +-- 2 files changed, 654 insertions(+), 44 deletions(-) create mode 100644 oauth2_provider/oauth2_validators.orig diff --git a/oauth2_provider/oauth2_validators.orig b/oauth2_provider/oauth2_validators.orig new file mode 100644 index 000000000..450a04fb5 --- /dev/null +++ b/oauth2_provider/oauth2_validators.orig @@ -0,0 +1,629 @@ +import base64 +import binascii +import logging +from collections import OrderedDict +from datetime import datetime, timedelta +from urllib.parse import unquote_plus + +import requests +from django.conf import settings +from django.contrib.auth import authenticate, get_user_model +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.db.models import Q +from django.utils import timezone +from django.utils.timezone import make_aware +from django.utils.translation import ugettext_lazy as _ +from oauthlib.oauth2 import RequestValidator + +from .exceptions import FatalClientError +from .models import ( + AbstractApplication, get_access_token_model, + get_application_model, get_grant_model, get_refresh_token_model +) +from .scopes import get_scopes_backend +from .settings import oauth2_settings + + +log = logging.getLogger("oauth2_provider") + +GRANT_TYPE_MAPPING = { + "authorization_code": (AbstractApplication.GRANT_AUTHORIZATION_CODE, ), + "password": (AbstractApplication.GRANT_PASSWORD, ), + "client_credentials": (AbstractApplication.GRANT_CLIENT_CREDENTIALS, ), + "refresh_token": ( + AbstractApplication.GRANT_AUTHORIZATION_CODE, + AbstractApplication.GRANT_PASSWORD, + AbstractApplication.GRANT_CLIENT_CREDENTIALS, + ) +} + +Application = get_application_model() +AccessToken = get_access_token_model() +Grant = get_grant_model() +RefreshToken = get_refresh_token_model() +UserModel = get_user_model() + + +class OAuth2Validator(RequestValidator): + def _extract_basic_auth(self, request): + """ + Return authentication string if request contains basic auth credentials, + otherwise return None + """ + auth = request.headers.get("HTTP_AUTHORIZATION", None) + if not auth: + return None + + splitted = auth.split(" ", 1) + if len(splitted) != 2: + return None + auth_type, auth_string = splitted + + if auth_type != "Basic": + return None + + return auth_string + + def _authenticate_basic_auth(self, request): + """ + Authenticates with HTTP Basic Auth. + + Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with + "application/x-www-form-urlencoded" encoding algorithm. + """ + auth_string = self._extract_basic_auth(request) + if not auth_string: + return False + + try: + encoding = request.encoding or settings.DEFAULT_CHARSET or "utf-8" + except AttributeError: + encoding = "utf-8" + + try: + b64_decoded = base64.b64decode(auth_string) + except (TypeError, binascii.Error): + log.debug("Failed basic auth: %r can't be decoded as base64", auth_string) + return False + + try: + auth_string_decoded = b64_decoded.decode(encoding) + except UnicodeDecodeError: + log.debug( + "Failed basic auth: %r can't be decoded as unicode by %r", + auth_string, encoding + ) + return False + + try: + client_id, client_secret = map(unquote_plus, auth_string_decoded.split(":", 1)) + except ValueError: + log.debug("Failed basic auth, Invalid base64 encoding.") + return False + + if self._load_application(client_id, request) is None: + log.debug("Failed basic auth: Application %s does not exist" % client_id) + return False + elif request.client.client_id != client_id: + log.debug("Failed basic auth: wrong client id %s" % client_id) + return False + elif request.client.client_secret != client_secret: + log.debug("Failed basic auth: wrong client secret %s" % client_secret) + return False + else: + return True + + def _authenticate_request_body(self, request): + """ + Try to authenticate the client using client_id and client_secret + parameters included in body. + + Remember that this method is NOT RECOMMENDED and SHOULD be limited to + clients unable to directly utilize the HTTP Basic authentication scheme. + See rfc:`2.3.1` for more details. + """ + # TODO: check if oauthlib has already unquoted client_id and client_secret + try: + client_id = request.client_id + client_secret = request.client_secret + except AttributeError: + return False + + if self._load_application(client_id, request) is None: + log.debug("Failed body auth: Application %s does not exists" % client_id) + return False + elif request.client.client_secret != client_secret: + log.debug("Failed body auth: wrong client secret %s" % client_secret) + return False + else: + return True + + def _load_application(self, client_id, request): + """ + If request.client was not set, load application instance for given + client_id and store it in request.client + """ + + # we want to be sure that request has the client attribute! + assert hasattr(request, "client"), '"request" instance has no "client" attribute' + + try: + request.client = request.client or Application.objects.get(client_id=client_id) + # Check that the application can be used (defaults to always True) + if not request.client.is_usable(request): + log.debug("Failed body authentication: Application %r is disabled" % (client_id)) + return None + return request.client + except Application.DoesNotExist: + log.debug("Failed body authentication: Application %r does not exist" % (client_id)) + return None + + def _set_oauth2_error_on_request(self, request, access_token, scopes): + if access_token is None: + error = OrderedDict([ + ("error", "invalid_token", ), + ("error_description", _("The access token is invalid."), ), + ]) + elif access_token.is_expired(): + error = OrderedDict([ + ("error", "invalid_token", ), + ("error_description", _("The access token has expired."), ), + ]) + elif not access_token.allow_scopes(scopes): + error = OrderedDict([ + ("error", "insufficient_scope", ), + ("error_description", _("The access token is valid but does not have enough scope."), ), + ]) + else: + log.warning("OAuth2 access token is invalid for an unknown reason.") + error = OrderedDict([ + ("error", "invalid_token", ), + ]) + request.oauth2_error = error + return request + + def client_authentication_required(self, request, *args, **kwargs): + """ + Determine if the client has to be authenticated + + This method is called only for grant types that supports client authentication: + * Authorization code grant + * Resource owner password grant + * Refresh token grant + + If the request contains authorization headers, always authenticate the client + no matter the grant type. + + If the request does not contain authorization headers, proceed with authentication + only if the client is of type `Confidential`. + + If something goes wrong, call oauthlib implementation of the method. + """ + if self._extract_basic_auth(request): + return True + + try: + if request.client_id and request.client_secret: + return True + except AttributeError: + log.debug("Client ID or client secret not provided...") + pass + + self._load_application(request.client_id, request) + if request.client: + return request.client.client_type == AbstractApplication.CLIENT_CONFIDENTIAL + + return super().client_authentication_required(request, *args, **kwargs) + + def authenticate_client(self, request, *args, **kwargs): + """ + Check if client exists and is authenticating itself as in rfc:`3.2.1` + + First we try to authenticate with HTTP Basic Auth, and that is the PREFERRED + authentication method. + Whether this fails we support including the client credentials in the request-body, + but this method is NOT RECOMMENDED and SHOULD be limited to clients unable to + directly utilize the HTTP Basic authentication scheme. + See rfc:`2.3.1` for more details + """ + authenticated = self._authenticate_basic_auth(request) + + if not authenticated: + authenticated = self._authenticate_request_body(request) + + return authenticated + + def authenticate_client_id(self, client_id, request, *args, **kwargs): + """ + If we are here, the client did not authenticate itself as in rfc:`3.2.1` and we can + proceed only if the client exists and is not of type "Confidential". + """ + if self._load_application(client_id, request) is not None: + log.debug("Application %r has type %r" % (client_id, request.client.client_type)) + return request.client.client_type != AbstractApplication.CLIENT_CONFIDENTIAL + return False + + def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): + """ + Ensure the redirect_uri is listed in the Application instance redirect_uris field + """ + grant = Grant.objects.get(code=code, application=client) + return grant.redirect_uri_allowed(redirect_uri) + + def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): + """ + Remove the temporary grant used to swap the authorization token + """ + grant = Grant.objects.get(code=code, application=request.client) + grant.delete() + + def validate_client_id(self, client_id, request, *args, **kwargs): + """ + Ensure an Application exists with given client_id. + If it exists, it's assigned to request.client. + """ + return self._load_application(client_id, request) is not None + + def get_default_redirect_uri(self, client_id, request, *args, **kwargs): + return request.client.default_redirect_uri + + def _get_token_from_authentication_server( + self, token, introspection_url, introspection_token, introspection_credentials + ): + """Use external introspection endpoint to "crack open" the token. + :param introspection_url: introspection endpoint URL + :param introspection_token: Bearer token + :param introspection_credentials: Basic Auth credentials (id,secret) + :return: :class:`models.AccessToken` + + Some RFC 7662 implementations (including this one) use a Bearer token while others use Basic + Auth. Depending on the external AS's implementation, provide either the introspection_token + or the introspection_credentials. + + If the resulting access_token identifies a username (e.g. Authorization Code grant), add + that user to the UserModel. Also cache the access_token up until its expiry time or a + configured maximum time. + + """ + headers = None + if introspection_token: + headers = {"Authorization": "Bearer {}".format(introspection_token)} + elif introspection_credentials: + client_id = introspection_credentials[0].encode("utf-8") + client_secret = introspection_credentials[1].encode("utf-8") + basic_auth = base64.b64encode(client_id + b":" + client_secret) + headers = {"Authorization": "Basic {}".format(basic_auth.decode("utf-8"))} + + try: + response = requests.post( + introspection_url, + data={"token": token}, headers=headers + ) + except requests.exceptions.RequestException: + log.exception("Introspection: Failed POST to %r in token lookup", introspection_url) + return None + + try: + content = response.json() + except ValueError: + log.exception("Introspection: Failed to parse response as json") + return None + + if "active" in content and content["active"] is True: + if "username" in content: + user, _created = UserModel.objects.get_or_create( + **{UserModel.USERNAME_FIELD: content["username"]} + ) + else: + user = None + + max_caching_time = datetime.now() + timedelta( + seconds=oauth2_settings.RESOURCE_SERVER_TOKEN_CACHING_SECONDS + ) + + if "exp" in content: + expires = datetime.utcfromtimestamp(content["exp"]) + if expires > max_caching_time: + expires = max_caching_time + else: + expires = max_caching_time + + scope = content.get("scope", "") + expires = make_aware(expires) + + try: + access_token = AccessToken.objects.select_related("application", "user").get(token=token) + except AccessToken.DoesNotExist: + access_token = AccessToken.objects.create( + token=token, + user=user, + application=None, + scope=scope, + expires=expires + ) + else: + access_token.expires = expires + access_token.scope = scope + access_token.save() + + return access_token + + def validate_bearer_token(self, token, scopes, request): + """ + When users try to access resources, check that provided token is valid + """ + if not token: + return False + + introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL + introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN + introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS + + try: + access_token = AccessToken.objects.select_related("application", "user").get(token=token) + # if there is a token but invalid then look up the token + if introspection_url and (introspection_token or introspection_credentials): + if not access_token.is_valid(scopes): + access_token = self._get_token_from_authentication_server( + token, + introspection_url, + introspection_token, + introspection_credentials + ) + if access_token and access_token.is_valid(scopes): + request.client = access_token.application + request.user = access_token.user + request.scopes = scopes + + # this is needed by django rest framework + request.access_token = access_token + return True + self._set_oauth2_error_on_request(request, access_token, scopes) + return False + except AccessToken.DoesNotExist: + # there is no initial token, look up the token + if introspection_url and (introspection_token or introspection_credentials): + access_token = self._get_token_from_authentication_server( + token, + introspection_url, + introspection_token, + introspection_credentials + ) + if access_token and access_token.is_valid(scopes): + request.client = access_token.application + request.user = access_token.user + request.scopes = scopes + + # this is needed by django rest framework + request.access_token = access_token + return True + self._set_oauth2_error_on_request(request, None, scopes) + return False + + def validate_code(self, client_id, code, client, request, *args, **kwargs): + try: + grant = Grant.objects.get(code=code, application=client) + if not grant.is_expired(): + request.scopes = grant.scope.split(" ") + request.user = grant.user + return True + return False + + except Grant.DoesNotExist: + return False + + def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): + """ + Validate both grant_type is a valid string and grant_type is allowed for current workflow + """ + assert(grant_type in GRANT_TYPE_MAPPING) # mapping misconfiguration + return request.client.allows_grant_type(*GRANT_TYPE_MAPPING[grant_type]) + + def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): + """ + We currently do not support the Authorization Endpoint Response Types registry as in + rfc:`8.4`, so validate the response_type only if it matches "code" or "token" + """ + if response_type == "code": + return client.allows_grant_type(AbstractApplication.GRANT_AUTHORIZATION_CODE) + elif response_type == "token": + return client.allows_grant_type(AbstractApplication.GRANT_IMPLICIT) + else: + return False + + def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): + """ + Ensure required scopes are permitted (as specified in the settings file) + """ + available_scopes = get_scopes_backend().get_available_scopes(application=client, request=request) + return set(scopes).issubset(set(available_scopes)) + + def get_default_scopes(self, client_id, request, *args, **kwargs): + default_scopes = get_scopes_backend().get_default_scopes(application=request.client, request=request) + return default_scopes + + def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): + return request.client.redirect_uri_allowed(redirect_uri) + + def save_authorization_code(self, client_id, code, request, *args, **kwargs): + expires = timezone.now() + timedelta( + seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS) + g = Grant(application=request.client, user=request.user, code=code["code"], + expires=expires, redirect_uri=request.redirect_uri, + scope=" ".join(request.scopes)) + g.save() + + def rotate_refresh_token(self, request): + """ + Checks if rotate refresh token is enabled + """ + return oauth2_settings.ROTATE_REFRESH_TOKEN + + @transaction.atomic + def save_bearer_token(self, token, request, *args, **kwargs): + """ + Save access and refresh token, If refresh token is issued, remove or + reuse old refresh token as in rfc:`6` + + @see: https://tools.ietf.org/html/draft-ietf-oauth-v2-31#page-43 + """ + + if "scope" not in token: + raise FatalClientError("Failed to renew access token: missing scope") + + expires = timezone.now() + timedelta(seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) + + if request.grant_type == "client_credentials": + request.user = None + + # This comes from OAuthLib: + # https://github.com/idan/oauthlib/blob/1.0.3/oauthlib/oauth2/rfc6749/tokens.py#L267 + # Its value is either a new random code; or if we are reusing + # refresh tokens, then it is the same value that the request passed in + # (stored in `request.refresh_token`) + refresh_token_code = token.get("refresh_token", None) + + if refresh_token_code: + # an instance of `RefreshToken` that matches the old refresh code. + # Set on the request in `validate_refresh_token` + refresh_token_instance = getattr(request, "refresh_token_instance", None) + + # If we are to reuse tokens, and we can: do so + if not self.rotate_refresh_token(request) and \ + isinstance(refresh_token_instance, RefreshToken) and \ + refresh_token_instance.access_token: + + access_token = AccessToken.objects.select_for_update().get( + pk=refresh_token_instance.access_token.pk + ) + access_token.user = request.user + access_token.scope = token["scope"] + access_token.expires = expires + access_token.token = token["access_token"] + access_token.application = request.client + access_token.save() + + # else create fresh with access & refresh tokens + else: + # revoke existing tokens if possible to allow reuse of grant + if isinstance(refresh_token_instance, RefreshToken): + previous_access_token = AccessToken.objects.filter( + source_refresh_token=refresh_token_instance + ).first() + try: + refresh_token_instance.revoke() + except (AccessToken.DoesNotExist, RefreshToken.DoesNotExist): + pass + else: + setattr(request, "refresh_token_instance", None) + else: + previous_access_token = None + + # If the refresh token has already been used to create an + # access token (ie it's within the grace period), return that + # access token + if not previous_access_token: + access_token = self._create_access_token( + expires, + request, + token, + source_refresh_token=refresh_token_instance, + ) + + refresh_token = RefreshToken( + user=request.user, + token=refresh_token_code, + application=request.client, + access_token=access_token + ) + refresh_token.save() + else: + # make sure that the token data we're returning matches + # the existing token + token["access_token"] = previous_access_token.token + token["scope"] = previous_access_token.scope + + # No refresh token should be created, just access token + else: + self._create_access_token(expires, request, token) + + # TODO: check out a more reliable way to communicate expire time to oauthlib + token["expires_in"] = oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS + + def _create_access_token(self, expires, request, token, source_refresh_token=None): + access_token = AccessToken( + user=request.user, + scope=token["scope"], + expires=expires, + token=token["access_token"], + application=request.client, + source_refresh_token=source_refresh_token, + ) + access_token.save() + return access_token + + def revoke_token(self, token, token_type_hint, request, *args, **kwargs): + """ + Revoke an access or refresh token. + + :param token: The token string. + :param token_type_hint: access_token or refresh_token. + :param request: The HTTP Request (oauthlib.common.Request) + """ + if token_type_hint not in ["access_token", "refresh_token"]: + token_type_hint = None + + token_types = { + "access_token": AccessToken, + "refresh_token": RefreshToken, + } + + token_type = token_types.get(token_type_hint, AccessToken) + try: + token_type.objects.get(token=token).revoke() + except ObjectDoesNotExist: + for other_type in [_t for _t in token_types.values() if _t != token_type]: + # slightly inefficient on Python2, but the queryset contains only one instance + list(map(lambda t: t.revoke(), other_type.objects.filter(token=token))) + + def validate_user(self, username, password, client, request, *args, **kwargs): + """ + Check username and password correspond to a valid and active User + """ + u = authenticate(username=username, password=password) + if u is not None and u.is_active: + request.user = u + return True + return False + + def get_original_scopes(self, refresh_token, request, *args, **kwargs): + # Avoid second query for RefreshToken since this method is invoked *after* + # validate_refresh_token. + rt = request.refresh_token_instance + if not rt.access_token_id: + return AccessToken.objects.get(source_refresh_token_id=rt.id).scope + + return rt.access_token.scope + + def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): + """ + Check refresh_token exists and refers to the right client. + Also attach User instance to the request object + """ + + null_or_recent = Q(revoked__isnull=True) | Q( + revoked__gt=timezone.now() - timedelta( + seconds=oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS + ) + ) + rt = RefreshToken.objects.filter(null_or_recent, token=refresh_token).first() + + if not rt: + return False + + request.user = rt.user + request.refresh_token = rt.token + # Temporary store RefreshToken instance to be reused by get_original_scopes and save_bearer_token. + request.refresh_token_instance = rt + return rt.application == client diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index b4c2e968b..87883e7ea 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -332,21 +332,15 @@ def _get_token_from_authentication_server( scope = content.get("scope", "") expires = make_aware(expires) - with transaction.atomic(): - access_token, _created = AccessToken\ - .objects.select_related("application", "user")\ - .select_for_update()\ - .get_or_create(token=token, - defaults={ - "user": user, - "application": None, - "scope": scope, - "expires": expires, - }) - if not _created: - access_token.scope = scope - access_token.expires = expires - access_token.save() + access_token, _created = AccessToken\ + .objects.select_related("application", "user")\ + .update_or_create(token=token, + defaults={ + "user": user, + "application": None, + "scope": scope, + "expires": expires, + }) return access_token @@ -363,27 +357,11 @@ def validate_bearer_token(self, token, scopes, request): try: access_token = AccessToken.objects.select_related("application", "user").get(token=token) - # if there is a token but invalid then look up the token - if introspection_url and (introspection_token or introspection_credentials): - if not access_token.is_valid(scopes): - access_token = self._get_token_from_authentication_server( - token, - introspection_url, - introspection_token, - introspection_credentials - ) - if access_token and access_token.is_valid(scopes): - request.client = access_token.application - request.user = access_token.user - request.scopes = scopes - - # this is needed by django rest framework - request.access_token = access_token - return True - self._set_oauth2_error_on_request(request, access_token, scopes) - return False except AccessToken.DoesNotExist: - # there is no initial token, look up the token + access_token = None + + # if there is no token or it's invalid then introspect the token if there's an external OAuth server + if not access_token or not access_token.is_valid(scopes): if introspection_url and (introspection_token or introspection_credentials): access_token = self._get_token_from_authentication_server( token, @@ -391,17 +369,20 @@ def validate_bearer_token(self, token, scopes, request): introspection_token, introspection_credentials ) - if access_token and access_token.is_valid(scopes): - request.client = access_token.application - request.user = access_token.user - request.scopes = scopes - - # this is needed by django rest framework - request.access_token = access_token - return True - self._set_oauth2_error_on_request(request, None, scopes) + + if access_token and access_token.is_valid(scopes): + request.client = access_token.application + request.user = access_token.user + request.scopes = scopes + + # this is needed by django rest framework + request.access_token = access_token + return True + else: + self._set_oauth2_error_on_request(request, access_token, scopes) return False + def validate_code(self, client_id, code, client, request, *args, **kwargs): try: grant = Grant.objects.get(code=code, application=client) From e6267576f00da62ba2db11e573a40e4f802c6078 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Sun, 29 Jul 2018 14:08:25 -0400 Subject: [PATCH 8/9] flake8 --- oauth2_provider/oauth2_validators.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 87883e7ea..2385be055 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -335,12 +335,12 @@ def _get_token_from_authentication_server( access_token, _created = AccessToken\ .objects.select_related("application", "user")\ .update_or_create(token=token, - defaults={ - "user": user, - "application": None, - "scope": scope, - "expires": expires, - }) + defaults={ + "user": user, + "application": None, + "scope": scope, + "expires": expires, + }) return access_token @@ -382,7 +382,6 @@ def validate_bearer_token(self, token, scopes, request): self._set_oauth2_error_on_request(request, access_token, scopes) return False - def validate_code(self, client_id, code, client, request, *args, **kwargs): try: grant = Grant.objects.get(code=code, application=client) From 594e2b496e100644a6cdc94522f518698b68ee76 Mon Sep 17 00:00:00 2001 From: Alan Crosswell Date: Mon, 30 Jul 2018 14:08:29 -0400 Subject: [PATCH 9/9] inadvertent commit. --- oauth2_provider/oauth2_validators.orig | 629 ------------------------- 1 file changed, 629 deletions(-) delete mode 100644 oauth2_provider/oauth2_validators.orig diff --git a/oauth2_provider/oauth2_validators.orig b/oauth2_provider/oauth2_validators.orig deleted file mode 100644 index 450a04fb5..000000000 --- a/oauth2_provider/oauth2_validators.orig +++ /dev/null @@ -1,629 +0,0 @@ -import base64 -import binascii -import logging -from collections import OrderedDict -from datetime import datetime, timedelta -from urllib.parse import unquote_plus - -import requests -from django.conf import settings -from django.contrib.auth import authenticate, get_user_model -from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction -from django.db.models import Q -from django.utils import timezone -from django.utils.timezone import make_aware -from django.utils.translation import ugettext_lazy as _ -from oauthlib.oauth2 import RequestValidator - -from .exceptions import FatalClientError -from .models import ( - AbstractApplication, get_access_token_model, - get_application_model, get_grant_model, get_refresh_token_model -) -from .scopes import get_scopes_backend -from .settings import oauth2_settings - - -log = logging.getLogger("oauth2_provider") - -GRANT_TYPE_MAPPING = { - "authorization_code": (AbstractApplication.GRANT_AUTHORIZATION_CODE, ), - "password": (AbstractApplication.GRANT_PASSWORD, ), - "client_credentials": (AbstractApplication.GRANT_CLIENT_CREDENTIALS, ), - "refresh_token": ( - AbstractApplication.GRANT_AUTHORIZATION_CODE, - AbstractApplication.GRANT_PASSWORD, - AbstractApplication.GRANT_CLIENT_CREDENTIALS, - ) -} - -Application = get_application_model() -AccessToken = get_access_token_model() -Grant = get_grant_model() -RefreshToken = get_refresh_token_model() -UserModel = get_user_model() - - -class OAuth2Validator(RequestValidator): - def _extract_basic_auth(self, request): - """ - Return authentication string if request contains basic auth credentials, - otherwise return None - """ - auth = request.headers.get("HTTP_AUTHORIZATION", None) - if not auth: - return None - - splitted = auth.split(" ", 1) - if len(splitted) != 2: - return None - auth_type, auth_string = splitted - - if auth_type != "Basic": - return None - - return auth_string - - def _authenticate_basic_auth(self, request): - """ - Authenticates with HTTP Basic Auth. - - Note: as stated in rfc:`2.3.1`, client_id and client_secret must be encoded with - "application/x-www-form-urlencoded" encoding algorithm. - """ - auth_string = self._extract_basic_auth(request) - if not auth_string: - return False - - try: - encoding = request.encoding or settings.DEFAULT_CHARSET or "utf-8" - except AttributeError: - encoding = "utf-8" - - try: - b64_decoded = base64.b64decode(auth_string) - except (TypeError, binascii.Error): - log.debug("Failed basic auth: %r can't be decoded as base64", auth_string) - return False - - try: - auth_string_decoded = b64_decoded.decode(encoding) - except UnicodeDecodeError: - log.debug( - "Failed basic auth: %r can't be decoded as unicode by %r", - auth_string, encoding - ) - return False - - try: - client_id, client_secret = map(unquote_plus, auth_string_decoded.split(":", 1)) - except ValueError: - log.debug("Failed basic auth, Invalid base64 encoding.") - return False - - if self._load_application(client_id, request) is None: - log.debug("Failed basic auth: Application %s does not exist" % client_id) - return False - elif request.client.client_id != client_id: - log.debug("Failed basic auth: wrong client id %s" % client_id) - return False - elif request.client.client_secret != client_secret: - log.debug("Failed basic auth: wrong client secret %s" % client_secret) - return False - else: - return True - - def _authenticate_request_body(self, request): - """ - Try to authenticate the client using client_id and client_secret - parameters included in body. - - Remember that this method is NOT RECOMMENDED and SHOULD be limited to - clients unable to directly utilize the HTTP Basic authentication scheme. - See rfc:`2.3.1` for more details. - """ - # TODO: check if oauthlib has already unquoted client_id and client_secret - try: - client_id = request.client_id - client_secret = request.client_secret - except AttributeError: - return False - - if self._load_application(client_id, request) is None: - log.debug("Failed body auth: Application %s does not exists" % client_id) - return False - elif request.client.client_secret != client_secret: - log.debug("Failed body auth: wrong client secret %s" % client_secret) - return False - else: - return True - - def _load_application(self, client_id, request): - """ - If request.client was not set, load application instance for given - client_id and store it in request.client - """ - - # we want to be sure that request has the client attribute! - assert hasattr(request, "client"), '"request" instance has no "client" attribute' - - try: - request.client = request.client or Application.objects.get(client_id=client_id) - # Check that the application can be used (defaults to always True) - if not request.client.is_usable(request): - log.debug("Failed body authentication: Application %r is disabled" % (client_id)) - return None - return request.client - except Application.DoesNotExist: - log.debug("Failed body authentication: Application %r does not exist" % (client_id)) - return None - - def _set_oauth2_error_on_request(self, request, access_token, scopes): - if access_token is None: - error = OrderedDict([ - ("error", "invalid_token", ), - ("error_description", _("The access token is invalid."), ), - ]) - elif access_token.is_expired(): - error = OrderedDict([ - ("error", "invalid_token", ), - ("error_description", _("The access token has expired."), ), - ]) - elif not access_token.allow_scopes(scopes): - error = OrderedDict([ - ("error", "insufficient_scope", ), - ("error_description", _("The access token is valid but does not have enough scope."), ), - ]) - else: - log.warning("OAuth2 access token is invalid for an unknown reason.") - error = OrderedDict([ - ("error", "invalid_token", ), - ]) - request.oauth2_error = error - return request - - def client_authentication_required(self, request, *args, **kwargs): - """ - Determine if the client has to be authenticated - - This method is called only for grant types that supports client authentication: - * Authorization code grant - * Resource owner password grant - * Refresh token grant - - If the request contains authorization headers, always authenticate the client - no matter the grant type. - - If the request does not contain authorization headers, proceed with authentication - only if the client is of type `Confidential`. - - If something goes wrong, call oauthlib implementation of the method. - """ - if self._extract_basic_auth(request): - return True - - try: - if request.client_id and request.client_secret: - return True - except AttributeError: - log.debug("Client ID or client secret not provided...") - pass - - self._load_application(request.client_id, request) - if request.client: - return request.client.client_type == AbstractApplication.CLIENT_CONFIDENTIAL - - return super().client_authentication_required(request, *args, **kwargs) - - def authenticate_client(self, request, *args, **kwargs): - """ - Check if client exists and is authenticating itself as in rfc:`3.2.1` - - First we try to authenticate with HTTP Basic Auth, and that is the PREFERRED - authentication method. - Whether this fails we support including the client credentials in the request-body, - but this method is NOT RECOMMENDED and SHOULD be limited to clients unable to - directly utilize the HTTP Basic authentication scheme. - See rfc:`2.3.1` for more details - """ - authenticated = self._authenticate_basic_auth(request) - - if not authenticated: - authenticated = self._authenticate_request_body(request) - - return authenticated - - def authenticate_client_id(self, client_id, request, *args, **kwargs): - """ - If we are here, the client did not authenticate itself as in rfc:`3.2.1` and we can - proceed only if the client exists and is not of type "Confidential". - """ - if self._load_application(client_id, request) is not None: - log.debug("Application %r has type %r" % (client_id, request.client.client_type)) - return request.client.client_type != AbstractApplication.CLIENT_CONFIDENTIAL - return False - - def confirm_redirect_uri(self, client_id, code, redirect_uri, client, *args, **kwargs): - """ - Ensure the redirect_uri is listed in the Application instance redirect_uris field - """ - grant = Grant.objects.get(code=code, application=client) - return grant.redirect_uri_allowed(redirect_uri) - - def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): - """ - Remove the temporary grant used to swap the authorization token - """ - grant = Grant.objects.get(code=code, application=request.client) - grant.delete() - - def validate_client_id(self, client_id, request, *args, **kwargs): - """ - Ensure an Application exists with given client_id. - If it exists, it's assigned to request.client. - """ - return self._load_application(client_id, request) is not None - - def get_default_redirect_uri(self, client_id, request, *args, **kwargs): - return request.client.default_redirect_uri - - def _get_token_from_authentication_server( - self, token, introspection_url, introspection_token, introspection_credentials - ): - """Use external introspection endpoint to "crack open" the token. - :param introspection_url: introspection endpoint URL - :param introspection_token: Bearer token - :param introspection_credentials: Basic Auth credentials (id,secret) - :return: :class:`models.AccessToken` - - Some RFC 7662 implementations (including this one) use a Bearer token while others use Basic - Auth. Depending on the external AS's implementation, provide either the introspection_token - or the introspection_credentials. - - If the resulting access_token identifies a username (e.g. Authorization Code grant), add - that user to the UserModel. Also cache the access_token up until its expiry time or a - configured maximum time. - - """ - headers = None - if introspection_token: - headers = {"Authorization": "Bearer {}".format(introspection_token)} - elif introspection_credentials: - client_id = introspection_credentials[0].encode("utf-8") - client_secret = introspection_credentials[1].encode("utf-8") - basic_auth = base64.b64encode(client_id + b":" + client_secret) - headers = {"Authorization": "Basic {}".format(basic_auth.decode("utf-8"))} - - try: - response = requests.post( - introspection_url, - data={"token": token}, headers=headers - ) - except requests.exceptions.RequestException: - log.exception("Introspection: Failed POST to %r in token lookup", introspection_url) - return None - - try: - content = response.json() - except ValueError: - log.exception("Introspection: Failed to parse response as json") - return None - - if "active" in content and content["active"] is True: - if "username" in content: - user, _created = UserModel.objects.get_or_create( - **{UserModel.USERNAME_FIELD: content["username"]} - ) - else: - user = None - - max_caching_time = datetime.now() + timedelta( - seconds=oauth2_settings.RESOURCE_SERVER_TOKEN_CACHING_SECONDS - ) - - if "exp" in content: - expires = datetime.utcfromtimestamp(content["exp"]) - if expires > max_caching_time: - expires = max_caching_time - else: - expires = max_caching_time - - scope = content.get("scope", "") - expires = make_aware(expires) - - try: - access_token = AccessToken.objects.select_related("application", "user").get(token=token) - except AccessToken.DoesNotExist: - access_token = AccessToken.objects.create( - token=token, - user=user, - application=None, - scope=scope, - expires=expires - ) - else: - access_token.expires = expires - access_token.scope = scope - access_token.save() - - return access_token - - def validate_bearer_token(self, token, scopes, request): - """ - When users try to access resources, check that provided token is valid - """ - if not token: - return False - - introspection_url = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_URL - introspection_token = oauth2_settings.RESOURCE_SERVER_AUTH_TOKEN - introspection_credentials = oauth2_settings.RESOURCE_SERVER_INTROSPECTION_CREDENTIALS - - try: - access_token = AccessToken.objects.select_related("application", "user").get(token=token) - # if there is a token but invalid then look up the token - if introspection_url and (introspection_token or introspection_credentials): - if not access_token.is_valid(scopes): - access_token = self._get_token_from_authentication_server( - token, - introspection_url, - introspection_token, - introspection_credentials - ) - if access_token and access_token.is_valid(scopes): - request.client = access_token.application - request.user = access_token.user - request.scopes = scopes - - # this is needed by django rest framework - request.access_token = access_token - return True - self._set_oauth2_error_on_request(request, access_token, scopes) - return False - except AccessToken.DoesNotExist: - # there is no initial token, look up the token - if introspection_url and (introspection_token or introspection_credentials): - access_token = self._get_token_from_authentication_server( - token, - introspection_url, - introspection_token, - introspection_credentials - ) - if access_token and access_token.is_valid(scopes): - request.client = access_token.application - request.user = access_token.user - request.scopes = scopes - - # this is needed by django rest framework - request.access_token = access_token - return True - self._set_oauth2_error_on_request(request, None, scopes) - return False - - def validate_code(self, client_id, code, client, request, *args, **kwargs): - try: - grant = Grant.objects.get(code=code, application=client) - if not grant.is_expired(): - request.scopes = grant.scope.split(" ") - request.user = grant.user - return True - return False - - except Grant.DoesNotExist: - return False - - def validate_grant_type(self, client_id, grant_type, client, request, *args, **kwargs): - """ - Validate both grant_type is a valid string and grant_type is allowed for current workflow - """ - assert(grant_type in GRANT_TYPE_MAPPING) # mapping misconfiguration - return request.client.allows_grant_type(*GRANT_TYPE_MAPPING[grant_type]) - - def validate_response_type(self, client_id, response_type, client, request, *args, **kwargs): - """ - We currently do not support the Authorization Endpoint Response Types registry as in - rfc:`8.4`, so validate the response_type only if it matches "code" or "token" - """ - if response_type == "code": - return client.allows_grant_type(AbstractApplication.GRANT_AUTHORIZATION_CODE) - elif response_type == "token": - return client.allows_grant_type(AbstractApplication.GRANT_IMPLICIT) - else: - return False - - def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): - """ - Ensure required scopes are permitted (as specified in the settings file) - """ - available_scopes = get_scopes_backend().get_available_scopes(application=client, request=request) - return set(scopes).issubset(set(available_scopes)) - - def get_default_scopes(self, client_id, request, *args, **kwargs): - default_scopes = get_scopes_backend().get_default_scopes(application=request.client, request=request) - return default_scopes - - def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): - return request.client.redirect_uri_allowed(redirect_uri) - - def save_authorization_code(self, client_id, code, request, *args, **kwargs): - expires = timezone.now() + timedelta( - seconds=oauth2_settings.AUTHORIZATION_CODE_EXPIRE_SECONDS) - g = Grant(application=request.client, user=request.user, code=code["code"], - expires=expires, redirect_uri=request.redirect_uri, - scope=" ".join(request.scopes)) - g.save() - - def rotate_refresh_token(self, request): - """ - Checks if rotate refresh token is enabled - """ - return oauth2_settings.ROTATE_REFRESH_TOKEN - - @transaction.atomic - def save_bearer_token(self, token, request, *args, **kwargs): - """ - Save access and refresh token, If refresh token is issued, remove or - reuse old refresh token as in rfc:`6` - - @see: https://tools.ietf.org/html/draft-ietf-oauth-v2-31#page-43 - """ - - if "scope" not in token: - raise FatalClientError("Failed to renew access token: missing scope") - - expires = timezone.now() + timedelta(seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS) - - if request.grant_type == "client_credentials": - request.user = None - - # This comes from OAuthLib: - # https://github.com/idan/oauthlib/blob/1.0.3/oauthlib/oauth2/rfc6749/tokens.py#L267 - # Its value is either a new random code; or if we are reusing - # refresh tokens, then it is the same value that the request passed in - # (stored in `request.refresh_token`) - refresh_token_code = token.get("refresh_token", None) - - if refresh_token_code: - # an instance of `RefreshToken` that matches the old refresh code. - # Set on the request in `validate_refresh_token` - refresh_token_instance = getattr(request, "refresh_token_instance", None) - - # If we are to reuse tokens, and we can: do so - if not self.rotate_refresh_token(request) and \ - isinstance(refresh_token_instance, RefreshToken) and \ - refresh_token_instance.access_token: - - access_token = AccessToken.objects.select_for_update().get( - pk=refresh_token_instance.access_token.pk - ) - access_token.user = request.user - access_token.scope = token["scope"] - access_token.expires = expires - access_token.token = token["access_token"] - access_token.application = request.client - access_token.save() - - # else create fresh with access & refresh tokens - else: - # revoke existing tokens if possible to allow reuse of grant - if isinstance(refresh_token_instance, RefreshToken): - previous_access_token = AccessToken.objects.filter( - source_refresh_token=refresh_token_instance - ).first() - try: - refresh_token_instance.revoke() - except (AccessToken.DoesNotExist, RefreshToken.DoesNotExist): - pass - else: - setattr(request, "refresh_token_instance", None) - else: - previous_access_token = None - - # If the refresh token has already been used to create an - # access token (ie it's within the grace period), return that - # access token - if not previous_access_token: - access_token = self._create_access_token( - expires, - request, - token, - source_refresh_token=refresh_token_instance, - ) - - refresh_token = RefreshToken( - user=request.user, - token=refresh_token_code, - application=request.client, - access_token=access_token - ) - refresh_token.save() - else: - # make sure that the token data we're returning matches - # the existing token - token["access_token"] = previous_access_token.token - token["scope"] = previous_access_token.scope - - # No refresh token should be created, just access token - else: - self._create_access_token(expires, request, token) - - # TODO: check out a more reliable way to communicate expire time to oauthlib - token["expires_in"] = oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS - - def _create_access_token(self, expires, request, token, source_refresh_token=None): - access_token = AccessToken( - user=request.user, - scope=token["scope"], - expires=expires, - token=token["access_token"], - application=request.client, - source_refresh_token=source_refresh_token, - ) - access_token.save() - return access_token - - def revoke_token(self, token, token_type_hint, request, *args, **kwargs): - """ - Revoke an access or refresh token. - - :param token: The token string. - :param token_type_hint: access_token or refresh_token. - :param request: The HTTP Request (oauthlib.common.Request) - """ - if token_type_hint not in ["access_token", "refresh_token"]: - token_type_hint = None - - token_types = { - "access_token": AccessToken, - "refresh_token": RefreshToken, - } - - token_type = token_types.get(token_type_hint, AccessToken) - try: - token_type.objects.get(token=token).revoke() - except ObjectDoesNotExist: - for other_type in [_t for _t in token_types.values() if _t != token_type]: - # slightly inefficient on Python2, but the queryset contains only one instance - list(map(lambda t: t.revoke(), other_type.objects.filter(token=token))) - - def validate_user(self, username, password, client, request, *args, **kwargs): - """ - Check username and password correspond to a valid and active User - """ - u = authenticate(username=username, password=password) - if u is not None and u.is_active: - request.user = u - return True - return False - - def get_original_scopes(self, refresh_token, request, *args, **kwargs): - # Avoid second query for RefreshToken since this method is invoked *after* - # validate_refresh_token. - rt = request.refresh_token_instance - if not rt.access_token_id: - return AccessToken.objects.get(source_refresh_token_id=rt.id).scope - - return rt.access_token.scope - - def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs): - """ - Check refresh_token exists and refers to the right client. - Also attach User instance to the request object - """ - - null_or_recent = Q(revoked__isnull=True) | Q( - revoked__gt=timezone.now() - timedelta( - seconds=oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS - ) - ) - rt = RefreshToken.objects.filter(null_or_recent, token=refresh_token).first() - - if not rt: - return False - - request.user = rt.user - request.refresh_token = rt.token - # Temporary store RefreshToken instance to be reused by get_original_scopes and save_bearer_token. - request.refresh_token_instance = rt - return rt.application == client