Skip to content

Commit

Permalink
Merge pull request #16 from luizalabs/aj-oauth2
Browse files Browse the repository at this point in the history
Adding the oauth2 app
  • Loading branch information
jarussi-luizalabs committed Nov 14, 2016
2 parents dfaed2e + 24414bc commit 1051731
Show file tree
Hide file tree
Showing 13 changed files with 333 additions and 1 deletion.
1 change: 1 addition & 0 deletions django_toolkit/oauth2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'django_toolkit.oauth2.apps.OAuth2AppConfig'
11 changes: 11 additions & 0 deletions django_toolkit/oauth2/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
from django.apps import AppConfig


class OAuth2AppConfig(AppConfig):
name = 'django_toolkit.oauth2'
verbose_name = 'OAuth2'

def ready(self):
super(OAuth2AppConfig, self).ready()
from . import receivers # noqa
14 changes: 14 additions & 0 deletions django_toolkit/oauth2/receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from django.core.cache import caches
from django.db.models.signals import post_delete
from django.dispatch import receiver
from oauth2_provider.models import AccessToken

from django_toolkit import toolkit_settings

cache = caches[toolkit_settings.ACCESS_TOKEN_CACHE_BACKEND]


@receiver(post_delete, sender=AccessToken)
def invalidate_token_cache(sender, instance, **kwargs):
cache.delete(instance.token)
48 changes: 48 additions & 0 deletions django_toolkit/oauth2/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
from django.core.cache import caches
from django.utils import timezone
from oauth2_provider.models import AccessToken
from oauth2_provider.oauth2_validators import OAuth2Validator

from django_toolkit import toolkit_settings

cache = caches[toolkit_settings.ACCESS_TOKEN_CACHE_BACKEND]


class CachedOAuth2Validator(OAuth2Validator):

def validate_bearer_token(self, token, scopes, request):
if not token:
return False

try:
access_token = self._get_access_token(token)
if access_token.is_valid(scopes):
request.client = access_token.application
request.user = access_token.user
request.scopes = scopes

# this is needed by django rest framework
request.access_token = access_token
return True
return False
except AccessToken.DoesNotExist:
return False

def _get_access_token(self, token):
access_token = cache.get(token)

if access_token is None:
access_token = AccessToken.objects.select_related(
'application',
'user'
).get(
token=token
)
now = timezone.now()
if (access_token.expires > now):
timeout = (access_token.expires - now).seconds

cache.set(token, access_token, timeout)

return access_token
8 changes: 8 additions & 0 deletions django_toolkit/toolkit_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.conf import settings

_toolkit_settings = getattr(settings, 'TOOLKIT', {})

ACCESS_TOKEN_CACHE_BACKEND = _toolkit_settings.get(
'ACCESS_TOKEN_CACHE_BACKEND',
'access_token'
)
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ projects using the [Django web framework][django-website].
This package includes the utility modules:

* [concurrent](concurrent)
* [fallbacks](fallbacks)
* [logs](logs)
* [middlewares](middlewares)
* [oauth2](oauth2)
* [shortcuts](shortcuts)
* [fallbacks](fallbacks)

[django-website]: https://www.djangoproject.com/
38 changes: 38 additions & 0 deletions docs/oauth2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Oauth2
======

Oauth2 is a django app that will can be used to cache your `django-oauth-toolkit`
access token model.


Usage
-----
To start caching your api access tokens, add `django_toolkit.oauth2` to your
`INSTALLED_APPS` and then add the oauth2 validator class in the `OAUTH2_PROVIDER`
settings.


Example:
```python
OAUTH2_PROVIDER = {
'OAUTH2_VALIDATOR_CLASS': 'django_toolkit.oauth2.validators.CachedOAuth2Validator',
}
```

You can specify wich cache you want to use by setting the cache name
in the `TOOLKIT` settings variable. If no name is specified, `access_token` will be used.
Example:
```python
# toolkit settings
TOOLKIT = {
'ACCESS_TOKEN_CACHE_BACKEND': 'access_token'
}

# django cache settings
CACHES = {
'access_token': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'KEY_PREFIX': 'token',
}
}
```
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os

from setuptools import find_packages, setup


Expand All @@ -22,6 +23,9 @@ def read(fname):
install_requires=[
'Django>=1.8',
],
extra_requires={
'oauth2': ['django-oauth-toolkit'],
},
packages=find_packages(exclude=[
'tests*'
]),
Expand Down
Empty file added tests/oauth2/__init__.py
Empty file.
57 changes: 57 additions & 0 deletions tests/oauth2/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from datetime import timedelta

import pytest
from django.core.cache import caches
from django.utils import timezone
from oauth2_provider.compat import get_user_model
from oauth2_provider.models import AccessToken, get_application_model


@pytest.fixture(autouse=True)
def cache():
cache = caches['access_token']
yield cache
cache.clear()


@pytest.fixture
def scopes():
return ['permission:read', 'permission:write']


@pytest.fixture
def user():
UserModel = get_user_model()

return UserModel.objects.create_user(
'my-user', 'my@user.com', '123456'
)


@pytest.fixture
def application(user):
Application = get_application_model()
application = Application.objects.first()

if application:
return application

application = Application.objects.create(
user=user,
name='Test Application',
client_type=Application.CLIENT_CONFIDENTIAL,
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
)

return application


@pytest.fixture
def access_token(application, scopes):
return AccessToken.objects.create(
scope=' '.join(scopes),
expires=timezone.now() + timedelta(seconds=300),
token='secret-access-token-key',
application=application
)
16 changes: 16 additions & 0 deletions tests/oauth2/test_receivers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
import pytest


@pytest.mark.django_db
class TestDeleteAccessTokenCache(object):

def test_should_delete_token_cache(self, cache, access_token):
key = 'my-token'
access_token.token = key
access_token.save()
cache.set(key, 'a value')

access_token.delete()

assert cache.get(key) is None
129 changes: 129 additions & 0 deletions tests/oauth2/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
from datetime import timedelta

import pytest
from django.db import connection
from django.test.utils import CaptureQueriesContext
from django.utils import timezone

from django_toolkit.oauth2.validators import CachedOAuth2Validator


@pytest.mark.django_db
class TestCachedOAuth2Validator(object):

@pytest.fixture
def validator(self):
return CachedOAuth2Validator()

@pytest.fixture
def http_request(self, rf):
return rf.get('/foo')

def _warm_up_cache(self, validator, token, scopes, request):
return validator.validate_bearer_token(token, scopes, request)

def test_validate_bearer_token_should_not_reach_db_when_cached(
self,
access_token,
validator,
http_request,
scopes
):
db_result = self._warm_up_cache(
validator,
access_token.token,
scopes,
http_request
)

with CaptureQueriesContext(connection) as context:
cached_result = validator.validate_bearer_token(
access_token.token,
scopes,
http_request
)

assert len(context.captured_queries) == 0
assert db_result == cached_result

def test_validate_bearer_token_should_set_request_attributes(
self,
access_token,
validator,
scopes,
rf
):
self._warm_up_cache(
validator,
access_token.token,
scopes,
rf.get('/foo')
)

request = rf.get('/foo')
validator.validate_bearer_token(
access_token.token,
scopes,
request
)

assert request.client == access_token.application
assert request.user == access_token.user
assert request.scopes == scopes
assert request.access_token == access_token

def test_validate_bearer_token_should_get_cache_expiration_from_token(
self,
access_token,
validator,
scopes,
http_request
):
expires = timezone.now() - timedelta(seconds=5)
access_token.expires = expires
access_token.save()

self._warm_up_cache(
validator,
access_token.token,
scopes,
http_request
)

with CaptureQueriesContext(connection) as context:
validator.validate_bearer_token(
access_token.token,
scopes,
http_request
)

assert len(context.captured_queries) == 1

def test_validate_bearer_returns_false_when_no_token_is_provided(
self,
validator,
scopes,
http_request
):
token = None
is_valid = validator.validate_bearer_token(
token,
scopes,
http_request
)
assert not is_valid

def test_validate_bearer_returns_false_when_invalid_token_is_provided(
self,
validator,
scopes,
http_request
):
token = 'invalid-token'
is_valid = validator.validate_bearer_token(
token,
scopes,
http_request
)
assert not is_valid
5 changes: 5 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
'django.contrib.auth',
'django.contrib.contenttypes',
'oauth2_provider',

'django_toolkit.oauth2',
)

REST_FRAMEWORK = {
Expand All @@ -27,6 +29,9 @@
},
'locks': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'access_token': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}

Expand Down

0 comments on commit 1051731

Please sign in to comment.