diff --git a/oauth2_provider/admin.py b/oauth2_provider/admin.py index dd636184c..99c6115f1 100644 --- a/oauth2_provider/admin.py +++ b/oauth2_provider/admin.py @@ -19,7 +19,15 @@ class ApplicationAdmin(admin.ModelAdmin): - list_display = ("pk", "name", "user", "client_type", "authorization_grant_type") + list_display = ( + "pk", + "name", + "user", + "access_token_expire_seconds", + "refresh_token_expire_seconds", + "client_type", + "authorization_grant_type", + ) list_filter = ("client_type", "authorization_grant_type", "skip_authorization") radio_fields = { "client_type": admin.HORIZONTAL, diff --git a/oauth2_provider/migrations/0001_initial.py b/oauth2_provider/migrations/0001_initial.py index 1d1a38e0e..5663eb76d 100644 --- a/oauth2_provider/migrations/0001_initial.py +++ b/oauth2_provider/migrations/0001_initial.py @@ -36,6 +36,8 @@ class Migration(migrations.Migration): ('skip_authorization', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now_add=True)), ('updated', models.DateTimeField(auto_now=True)), + ('access_token_expire_seconds', models.IntegerField(default=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)), + ('refresh_token_expire_seconds', models.IntegerField(default=oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS)), ], options={ 'abstract': False, diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index a76db37c0..8efc696da 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -148,6 +148,12 @@ class AbstractApplication(models.Model): default="", ) + access_token_expire_seconds = models.IntegerField( + default=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS + ) + refresh_token_expire_seconds = models.IntegerField( + default=oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS + ) class Meta: abstract = True @@ -516,6 +522,29 @@ class AbstractRefreshToken(models.Model): updated = models.DateTimeField(auto_now=True) revoked = models.DateTimeField(null=True) + @property + def is_expired(self): + """Determine if RefreshToken is expired.""" + expire_seconds = self.application.refresh_token_expire_seconds + expires = self.created + timedelta(seconds=expire_seconds) + + now = timezone.now() + is_refresh_token_expired = now >= expires + + # Access token assumed to be expired, by default. + is_access_token_expired = True + + # RefreshToken should not outlive AccessToken. + # NOTE: Check AccessToken expiration for backwards compatibility with + # long-lived tokens. + if self.access_token: + access_token_expires = self.access_token.expires + is_access_token_expired = now >= access_token_expires + + # RefreshToken expired if and only if both refresh and access tokens + # are expired. + return is_refresh_token_expired and is_access_token_expired + def revoke(self): """ Mark this refresh token revoked and revoke related access token diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index db459a446..0343f677a 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -589,14 +589,22 @@ def _save_bearer_token(self, token, request, *args, **kwargs): if "scope" not in token: raise FatalClientError("Failed to renew access token: missing scope") + # "authenticate_client" sets the client (Application) on request. + app = request.client + + # Users on older app versions should get long-lived tokens for + # backwards compatibility. + TRUE_VALUES = [True, 'True', 'true'] + is_legacy_token = getattr(request, 'is_legacy_token', False) + + if is_legacy_token in TRUE_VALUES: + expire_seconds = oauth2_settings.LEGACY_ACCESS_TOKEN_EXPIRE_SECONDS + else: + expire_seconds = app.access_token_expire_seconds + # expires_in is passed to Server on initialization # custom server class can have logic to override this - expires = timezone.now() + timedelta( - seconds=token.get( - "expires_in", - oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS, - ) - ) + expires = timezone.now() + timedelta(seconds=expire_seconds) if request.grant_type == "client_credentials": request.user = None @@ -760,7 +768,10 @@ def validate_user(self, username, password, client, request, *args, **kwargs): getattr(http_request, request.http_method).update(dict(request.decoded_body)) http_request.META = request.headers u = authenticate(http_request, username=username, password=password) - if u is not None and u.is_active: + + # NOTE: [11/20/2020] Removed check for u.is_active because the check + # will be made *before* calling DOT (Django OAuth Toolkit) + if u is not None: request.user = u return True return False @@ -782,11 +793,25 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs Also attach User instance to the request object """ - rt = RefreshToken.objects.filter(token=refresh_token).select_related("access_token").first() + + rt = ( + RefreshToken.objects + .filter(token=refresh_token) + .select_related("user", "access_token", "application") + .first() + ) if not rt: return False + # Revoke token if expired. + if rt.is_expired: + try: + rt.revoke() + # Catch exception in case access or refresh token do not exist + except (AccessToken.DoesNotExist, RefreshToken.DoesNotExist): + pass + if rt.revoked is not None and rt.revoked <= timezone.now() - timedelta( seconds=oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS ): @@ -801,7 +826,13 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs # Temporary store RefreshToken instance to be reused by get_original_scopes and save_bearer_token. request.refresh_token_instance = rt - return rt.application == client + # Token is valid if it refers to the right client AND is not expired + is_valid = ( + rt.application == client and + not rt.is_expired + ) + + return is_valid def _save_id_token(self, jti, request, expires, *args, **kwargs): scopes = request.scope or " ".join(request.scopes) diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index 9771aa4e7..b1e1dbdf9 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -51,9 +51,13 @@ "READ_SCOPE": "read", "WRITE_SCOPE": "write", "AUTHORIZATION_CODE_EXPIRE_SECONDS": 60, - "ACCESS_TOKEN_EXPIRE_SECONDS": 36000, + "ACCESS_TOKEN_EXPIRE_SECONDS": 86400, # 24 hours in seconds. "ID_TOKEN_EXPIRE_SECONDS": 36000, - "REFRESH_TOKEN_EXPIRE_SECONDS": None, + "REFRESH_TOKEN_EXPIRE_SECONDS": 31556952, # 1 year in seconds. + + # Older app versions should get long-lived auth tokens. + "LEGACY_ACCESS_TOKEN_EXPIRE_SECONDS": 315569520, # 10 years + "REFRESH_TOKEN_GRACE_PERIOD_SECONDS": 0, "REFRESH_TOKEN_REUSE_PROTECTION": False, "ROTATE_REFRESH_TOKEN": True,