Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
49fde2a
Change Grant model and make it swappable
Mar 27, 2017
0dea399
Make AccessToken swappable
Mar 27, 2017
4c8f88c
Make RefreshToken swappable
Mar 27, 2017
9cd7771
Add get_grant_model method
Mar 27, 2017
2088882
Add get_access_token_model method
Mar 27, 2017
434e1f7
Add get_refresh_token_model method
Mar 27, 2017
27d97d4
Use swappable models on clear_expired method
Mar 27, 2017
c1cc8c4
Fix AbstractRefreshToken, make it use ACCESS_TOKEN_MODEL
Mar 27, 2017
0586f83
Use swappable models to register on admin.site
Mar 27, 2017
732a1b1
Use get_*_model methods to call swappable models
Mar 27, 2017
ed40c4d
Set default models
Mar 27, 2017
e293af1
Use get_access_token_model instead of AccessToken
Mar 27, 2017
bc6ed74
Use get_access_token_model method instead of directly AccessToken
Mar 27, 2017
2234e20
Use get_*_model methods to call swappable models instead of importing…
Mar 27, 2017
c3b4d9e
Use swappable methods to declare migrations
Mar 27, 2017
dbbe809
Fix typo declaring RefreshToken
Mar 27, 2017
82b48fc
Fix ImproperlyConfigured exceptions, refer to the correct setting.
Mar 28, 2017
22b51ff
Follow AbstractModel as guideline and set user related_name on the re…
Mar 28, 2017
b147b4d
Fix AuthorizationView.get method, as user.accesstoken is not accesibl…
Mar 28, 2017
7fd5171
Declare sample models for access_token, refresh_token and grant.
Mar 28, 2017
f7df5cc
Application model is swapped, use get_application_model instead.
Mar 28, 2017
a1bfbff
Test all custom models, as Application, Grant, AccessToken and Refres…
Mar 28, 2017
02d750c
Add new swappable models into settings
Mar 28, 2017
2005742
Get a little of credit
Mar 28, 2017
2e41c5c
Add test cases for improper swappable model names
Mar 28, 2017
b947e77
Django already handles these validations when model names are incorre…
Mar 28, 2017
c35ae2f
Add test cases to validate proper model names and that models are ins…
Mar 28, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading