Skip to content

Commit

Permalink
Merge c35ae2f into cb5b231
Browse files Browse the repository at this point in the history
  • Loading branch information
menecio committed Apr 20, 2017
2 parents cb5b231 + c35ae2f commit 553c693
Show file tree
Hide file tree
Showing 21 changed files with 365 additions and 61 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -9,6 +9,7 @@ Contributors

Alessandro De Angelis
Ash Christopher
Aristóbulo Meneses
Bart Merenda
Bas van Oostveen
David Fischer
Expand Down
21 changes: 20 additions & 1 deletion docs/settings.rst
Expand Up @@ -2,7 +2,8 @@ Settings
========

Our configurations are all namespaced under the `OAUTH2_PROVIDER` settings with the solely exception of
`OAUTH2_PROVIDER_APPLICATION_MODEL`: this is because of the way Django currently implements
`OAUTH2_PROVIDER_APPLICATION_MODEL, OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL, OAUTH2_PROVIDER_GRANT_MODEL,
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL`: this is because of the way Django currently implements
swappable models. See issue #90 (https://github.com/evonove/django-oauth-toolkit/issues/90) for details.

For example:
Expand Down Expand Up @@ -32,6 +33,12 @@ The number of seconds an access token remains valid. Requesting a protected
resource after this duration will fail. Keep this value high enough so clients
can cache the token for a reasonable amount of time.

ACCESS_TOKEN_MODEL
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your access tokens. Overwrite
this value if you wrote your own implementation (subclass of
``oauth2_provider.models.AccessToken``).

ALLOWED_REDIRECT_URI_SCHEMES
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -67,6 +74,12 @@ CLIENT_SECRET_GENERATOR_LENGTH
The length of the generated secrets, in characters. If this value is too low,
secrets may become subject to bruteforce guessing.

GRANT_MODEL
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your grants. Overwrite
this value if you wrote your own implementation (subclass of
``oauth2_provider.models.Grant``).

OAUTH2_SERVER_CLASS
~~~~~~~~~~~~~~~~~~~
The import string for the ``server_class`` (or ``oauthlib.oauth2.Server`` subclass)
Expand All @@ -87,6 +100,12 @@ REFRESH_TOKEN_EXPIRE_SECONDS
The number of seconds before a refresh token gets removed from the database by
the ``cleartokens`` management command. Check :ref:`cleartokens` management command for further info.

REFRESH_TOKEN_MODEL
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your refresh tokens. Overwrite
this value if you wrote your own implementation (subclass of
``oauth2_provider.models.RefreshToken``).

ROTATE_REFRESH_TOKEN
~~~~~~~~~~~~~~~~~~~~
When is set to `True` (default) a new refresh token is issued to the client when the client refreshes an access token.
Expand Down
10 changes: 9 additions & 1 deletion oauth2_provider/admin.py
@@ -1,6 +1,11 @@
from django.contrib import admin

from .models import AccessToken, get_application_model, Grant, RefreshToken
from .models import (
get_access_token_model,
get_application_model,
get_grant_model,
get_refresh_token_model,
)


class ApplicationAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -29,6 +34,9 @@ class RefreshTokenAdmin(admin.ModelAdmin):


Application = get_application_model()
Grant = get_grant_model()
AccessToken = get_access_token_model()
RefreshToken = get_refresh_token_model()

admin.site.register(Application, ApplicationAdmin)
admin.site.register(Grant, GrantAdmin)
Expand Down
17 changes: 16 additions & 1 deletion oauth2_provider/migrations/0001_initial.py
Expand Up @@ -13,6 +13,9 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
migrations.swappable_dependency(oauth2_settings.APPLICATION_MODEL),
migrations.swappable_dependency(oauth2_settings.ACCESS_TOKEN_MODEL),
migrations.swappable_dependency(oauth2_settings.REFRESH_TOKEN_MODEL),
migrations.swappable_dependency(oauth2_settings.GRANT_MODEL),
]

operations = [
Expand Down Expand Up @@ -43,6 +46,10 @@ class Migration(migrations.Migration):
('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
'abstract': False,
'swappable': 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL',
},
),
migrations.CreateModel(
name='Grant',
Expand All @@ -55,15 +62,23 @@ class Migration(migrations.Migration):
('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
'abstract': False,
'swappable': 'OAUTH2_PROVIDER_GRANT_MODEL',
},
),
migrations.CreateModel(
name='RefreshToken',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('token', models.CharField(max_length=255, db_index=True)),
('access_token', models.OneToOneField(related_name='refresh_token', to='oauth2_provider.AccessToken', on_delete=models.CASCADE)),
('access_token', models.OneToOneField(related_name='refresh_token', to=oauth2_settings.ACCESS_TOKEN_MODEL, on_delete=models.CASCADE)),
('application', models.ForeignKey(to=oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
'abstract': False,
'swappable': 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL',
},
),
]
85 changes: 61 additions & 24 deletions oauth2_provider/models.py
Expand Up @@ -59,7 +59,8 @@ class AbstractApplication(models.Model):

client_id = models.CharField(max_length=100, unique=True,
default=generate_client_id, db_index=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s",
user = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name="%(app_label)s_%(class)s",
null=True, blank=True, on_delete=models.CASCADE)

help_text = _("Allowed URIs list, space separated")
Expand Down Expand Up @@ -144,7 +145,7 @@ class Meta(AbstractApplication.Meta):


@python_2_unicode_compatible
class Grant(models.Model):
class AbstractGrant(models.Model):
"""
A Grant instance represents a token with a short lifetime that can
be swapped for an access token, as described in :rfc:`4.1.2`
Expand All @@ -159,7 +160,8 @@ class Grant(models.Model):
* :attr:`redirect_uri` Self explained
* :attr:`scope` Required scopes, optional
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s")
code = models.CharField(max_length=255, unique=True) # code comes from oauthlib
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL,
on_delete=models.CASCADE)
Expand All @@ -182,9 +184,17 @@ def redirect_uri_allowed(self, uri):
def __str__(self):
return self.code

class Meta:
abstract = True


class Grant(AbstractGrant):
class Meta(AbstractGrant.Meta):
swappable = 'OAUTH2_PROVIDER_GRANT_MODEL'


@python_2_unicode_compatible
class AccessToken(models.Model):
class AbstractAccessToken(models.Model):
"""
An AccessToken instance represents the actual access token to
access user's resources, as in :rfc:`5`.
Expand All @@ -198,8 +208,9 @@ class AccessToken(models.Model):
* :attr:`scope` Allowed scopes
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
on_delete=models.CASCADE)
token = models.CharField(max_length=255, unique=True)
on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s")
token = models.CharField(max_length=255, unique=True, )
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL,
on_delete=models.CASCADE)
expires = models.DateTimeField()
Expand Down Expand Up @@ -255,9 +266,17 @@ def scopes(self):
def __str__(self):
return self.token

class Meta:
abstract = True


class AccessToken(AbstractAccessToken):
class Meta(AbstractAccessToken.Meta):
swappable = 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL'


@python_2_unicode_compatible
class RefreshToken(models.Model):
class AbstractRefreshToken(models.Model):
"""
A RefreshToken instance represents a token that can be swapped for a new
access token when it expires.
Expand All @@ -270,43 +289,61 @@ class RefreshToken(models.Model):
* :attr:`access_token` AccessToken instance this refresh token is
bounded to
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE,
related_name="%(app_label)s_%(class)s")
token = models.CharField(max_length=255, unique=True)
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL,
on_delete=models.CASCADE)
access_token = models.OneToOneField(AccessToken,
access_token = models.OneToOneField(oauth2_settings.ACCESS_TOKEN_MODEL,
related_name='refresh_token',
on_delete=models.CASCADE)

def revoke(self):
"""
Delete this refresh token along with related access token
"""
AccessToken.objects.get(id=self.access_token.id).revoke()
access_token_model = get_access_token_model()
access_token_model.objects.get(id=self.access_token.id).revoke()
self.delete()

def __str__(self):
return self.token

class Meta:
abstract = True


class RefreshToken(AbstractRefreshToken):
class Meta(AbstractRefreshToken.Meta):
swappable = 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL'


def get_application_model():
""" Return the Application model that is active in this project. """
try:
app_label, model_name = oauth2_settings.APPLICATION_MODEL.split('.')
except ValueError:
e = "APPLICATION_MODEL must be of the form 'app_label.model_name'"
raise ImproperlyConfigured(e)
app_model = apps.get_model(app_label, model_name)
if app_model is None:
e = "APPLICATION_MODEL refers to model {0} that has not been installed"
raise ImproperlyConfigured(e.format(oauth2_settings.APPLICATION_MODEL))
return app_model
return apps.get_model(oauth2_settings.APPLICATION_MODEL)


def get_grant_model():
""" Return the Grant model that is active in this project. """
return apps.get_model(oauth2_settings.GRANT_MODEL)


def get_access_token_model():
""" Return the AccessToken model that is active in this project. """
return apps.get_model(oauth2_settings.ACCESS_TOKEN_MODEL)


def get_refresh_token_model():
""" Return the RefreshToken model that is active in this project. """
return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL)


def clear_expired():
now = timezone.now()
refresh_expire_at = None

access_token_model = get_access_token_model()
refresh_token_model = get_refresh_token_model()
grant_model = get_grant_model()
REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS
if REFRESH_TOKEN_EXPIRE_SECONDS:
if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta):
Expand All @@ -319,6 +356,6 @@ def clear_expired():

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()
refresh_token_model.objects.filter(access_token__expires__lt=refresh_expire_at).delete()
access_token_model.objects.filter(refresh_token__isnull=True, expires__lt=now).delete()
grant_model.objects.filter(expires__lt=now).delete()
19 changes: 15 additions & 4 deletions oauth2_provider/oauth2_validators.py
Expand Up @@ -14,7 +14,13 @@

from .compat import unquote_plus
from .exceptions import FatalClientError
from .models import AbstractApplication, AccessToken, get_application_model, Grant, RefreshToken
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

Expand All @@ -25,10 +31,16 @@
'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)
'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()


class OAuth2Validator(RequestValidator):
def _extract_basic_auth(self, request):
Expand Down Expand Up @@ -128,7 +140,6 @@ def _load_application(self, client_id, request):
# we want to be sure that request has the client attribute!
assert hasattr(request, "client"), "'request' instance has no 'client' attribute"

Application = get_application_model()
try:
request.client = request.client or Application.objects.get(client_id=client_id)
# Check that the application can be used (defaults to always True)
Expand Down
6 changes: 6 additions & 0 deletions oauth2_provider/settings.py
Expand Up @@ -26,6 +26,9 @@
USER_SETTINGS = getattr(settings, 'OAUTH2_PROVIDER', None)

APPLICATION_MODEL = getattr(settings, "OAUTH2_PROVIDER_APPLICATION_MODEL", "oauth2_provider.Application")
ACCESS_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL", "oauth2_provider.AccessToken")
GRANT_MODEL = getattr(settings, "OAUTH2_PROVIDER_GRANT_MODEL", "oauth2_provider.Grant")
REFRESH_TOKEN_MODEL = getattr(settings, "OAUTH2_PROVIDER_REFRESH_MODEL", "oauth2_provider.RefreshToken")

DEFAULTS = {
'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator',
Expand All @@ -44,6 +47,9 @@
'REFRESH_TOKEN_EXPIRE_SECONDS': None,
'ROTATE_REFRESH_TOKEN': True,
'APPLICATION_MODEL': APPLICATION_MODEL,
'ACCESS_TOKEN_MODEL': ACCESS_TOKEN_MODEL,
'GRANT_MODEL': GRANT_MODEL,
'REFRESH_TOKEN_MODEL': REFRESH_TOKEN_MODEL,
'REQUEST_APPROVAL_PROMPT': 'force',
'ALLOWED_REDIRECT_URI_SCHEMES': ['http', 'https'],

Expand Down
10 changes: 7 additions & 3 deletions oauth2_provider/views/base.py
Expand Up @@ -12,7 +12,7 @@
from ..exceptions import OAuthToolkitError
from ..forms import AllowForm
from ..http import HttpResponseUriRedirect
from ..models import get_application_model
from ..models import get_access_token_model, get_application_model
from ..scopes import get_scopes_backend
from ..settings import oauth2_settings

Expand Down Expand Up @@ -146,8 +146,12 @@ def get(self, request, *args, **kwargs):
return HttpResponseUriRedirect(uri)

elif require_approval == 'auto':
tokens = request.user.accesstoken_set.filter(application=kwargs['application'],
expires__gt=timezone.now()).all()
tokens = get_access_token_model().objects.filter(
user=request.user,
application=kwargs['application'],
expires__gt=timezone.now()
).all()

# check past authorizations regarded the same scopes as the current one
for token in tokens:
if token.allow_scopes(scopes):
Expand Down

0 comments on commit 553c693

Please sign in to comment.