diff --git a/oauth2_provider/admin.py b/oauth2_provider/admin.py index 8b963d981..1eef601c5 100644 --- a/oauth2_provider/admin.py +++ b/oauth2_provider/admin.py @@ -7,7 +7,15 @@ class ApplicationAdmin(admin.ModelAdmin): - list_display = ("id", "name", "user", "client_type", "authorization_grant_type") + list_display = ( + "id", + "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 5676bc0c5..d997b7099 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -83,6 +83,12 @@ class AbstractApplication(models.Model): 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 + ) class Meta: abstract = True @@ -381,6 +387,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 515353d6f..8bb73a0e6 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -475,11 +475,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 @@ -622,7 +633,10 @@ 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: + + # 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 @@ -647,15 +661,33 @@ def validate_refresh_token(self, refresh_token, client, request, *args, **kwargs seconds=oauth2_settings.REFRESH_TOKEN_GRACE_PERIOD_SECONDS ) ) - rt = RefreshToken.objects.filter(null_or_recent, token=refresh_token).select_related( - "access_token" - ).first() + rt = ( + RefreshToken.objects + .filter(null_or_recent, 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 + 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 + + # 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 diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index 0135da8b7..f38df3a86 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -44,8 +44,12 @@ "READ_SCOPE": "read", "WRITE_SCOPE": "write", "AUTHORIZATION_CODE_EXPIRE_SECONDS": 60, - "ACCESS_TOKEN_EXPIRE_SECONDS": 36000, - "REFRESH_TOKEN_EXPIRE_SECONDS": None, + "ACCESS_TOKEN_EXPIRE_SECONDS": 86400, # 24 hours in seconds. + "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, "ROTATE_REFRESH_TOKEN": True, "ERROR_RESPONSE_WITH_SCOPES": False,