diff --git a/AUTHORS b/AUTHORS index 8fa585f0d..edaf09330 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ David Fischer Ash Christopher Rodney Richardson Hiroki Kiyohara +Bas van Oostveen \ No newline at end of file diff --git a/docs/settings.rst b/docs/settings.rst index 6fc46da46..92a1e47de 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -77,6 +77,19 @@ READ_SCOPE ~~~~~~~~~~ The name of the *read* scope. +REFRESH_TOKEN_EXPIRE_SECONDS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The number of seconds before a refresh token gets removed from the database by +the ``cleartokens``` management command. It's important that ``cleartokens`` +runs regularly (eg: via cron) in order for this setting to work. + +If ``cleartokens`` runs daily the maximum delay before a refresh token is +removed is ``REFRESH_TOKEN_EXPIRE_SECONDS`` + 1 day. This is normally not a +problem since refresh tokens are long lived. + +Note: Refresh tokens need to expire before AccessTokens can be removed from the +database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect. + WRITE_SCOPE ~~~~~~~~~~~ The name of the *write* scope. diff --git a/oauth2_provider/management/__init__.py b/oauth2_provider/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oauth2_provider/management/commands/__init__.py b/oauth2_provider/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oauth2_provider/management/commands/cleartokens.py b/oauth2_provider/management/commands/cleartokens.py new file mode 100644 index 000000000..5b56d2bc1 --- /dev/null +++ b/oauth2_provider/management/commands/cleartokens.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand, CommandError +from ...models import clear_expired + + +class Command(BaseCommand): + help = "Can be run as a cronjob or directly to clean out expired tokens" + + def handle(self, *args, **options): + clear_expired() diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index d3091fd2a..3488928a0 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +from datetime import timedelta + from django.core.urlresolvers import reverse -from django.db import models +from django.db import models, transaction from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -253,3 +255,24 @@ def get_application_model(): e = "APPLICATION_MODEL refers to model {0} that has not been installed" raise ImproperlyConfigured(e.format(oauth2_settings.APPLICATION_MODEL)) return app_model + + +def clear_expired(): + now = timezone.now() + refresh_expire_at = None + + REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS + if REFRESH_TOKEN_EXPIRE_SECONDS: + if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta): + try: + REFRESH_TOKEN_EXPIRE_SECONDS = timedelta(seconds=REFRESH_TOKEN_EXPIRE_SECONDS) + except TypeError: + e = "REFRESH_TOKEN_EXPIRE_SECONDS must be either a timedelta or seconds" + raise ImproperlyConfigured(e) + refresh_expire_at = now - REFRESH_TOKEN_EXPIRE_SECONDS + + with transaction.atomic(): + if refresh_expire_at: + RefreshToken.objects.filter(access_token__expires__lt=refresh_expire_at).delete() + AccessToken.objects.filter(refresh_token__isnull=True, expires__lt=now).delete() + Grant.objects.filter(expires__lt=now).delete() diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index db5768686..fabda4fc7 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -40,6 +40,7 @@ 'WRITE_SCOPE': 'write', 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60, 'ACCESS_TOKEN_EXPIRE_SECONDS': 36000, + 'REFRESH_TOKEN_EXPIRE_SECONDS': None, 'APPLICATION_MODEL': getattr(settings, 'OAUTH2_PROVIDER_APPLICATION_MODEL', 'oauth2_provider.Application'), 'REQUEST_APPROVAL_PROMPT': 'force', 'ALLOWED_REDIRECT_URI_SCHEMES': ['http', 'https'],