-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from luizalabs/aj-oauth2
Adding the oauth2 app
- Loading branch information
Showing
13 changed files
with
333 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
default_app_config = 'django_toolkit.oauth2.apps.OAuth2AppConfig' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters