├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── rest_framework_expiring_authtoken ├── __init__.py ├── authentication.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── settings.py └── views.py ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── settings.py ├── test_authentication.py ├── test_models.py ├── test_views.py └── urls.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = rest_framework_expiring_authtoken 3 | omit = rest_framework_expiring_authtoken/migrations/* 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | env: 9 | - DJANGO_VERSION=1.8.14 10 | - DJANGO_VERSION=1.9.9 11 | - DJANGO_VERSION=1.10.1 12 | matrix: 13 | exclude: 14 | - python: "3.2" 15 | env: DJANGO_VERSION=1.9.9 16 | - python: "3.2" 17 | env: DJANGO_VERSION=1.10.1 18 | - python: "3.3" 19 | env: DJANGO_VERSION=1.9.9 20 | - python: "3.3" 21 | env: DJANGO_VERSION=1.10.1 22 | fast_finish: true 23 | 24 | install: 25 | - pip install Django==$DJANGO_VERSION djangorestframework==3.4.6 26 | - pip install coverage==3.7.1 27 | - pip install coveralls 28 | script: 29 | coverage run runtests.py 30 | after_success: 31 | coveralls 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, James Ritchie 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expiring Tokens for Django Rest Framework 2 | 3 | [![Build Status](https://travis-ci.org/JamesRitchie/django-rest-framework-expiring-tokens.svg?branch=master)](https://travis-ci.org/JamesRitchie/django-rest-framework-expiring-tokens) 4 | [![Coverage Status](https://coveralls.io/repos/JamesRitchie/django-rest-framework-expiring-tokens/badge.svg)](https://coveralls.io/r/JamesRitchie/django-rest-framework-expiring-tokens) 5 | [![Code Health](https://landscape.io/github/JamesRitchie/django-rest-framework-expiring-tokens/master/landscape.svg?style=flat)](https://landscape.io/github/JamesRitchie/django-rest-framework-expiring-tokens/master) 6 | [![PyPI version](https://badge.fury.io/py/djangorestframework-expiring-authtoken.svg)](http://badge.fury.io/py/djangorestframework-expiring-authtoken) 7 | [![Requirements Status](https://requires.io/github/JamesRitchie/django-rest-framework-expiring-tokens/requirements.svg?branch=master)](https://requires.io/github/JamesRitchie/django-rest-framework-expiring-tokens/requirements/?branch=master) 8 | 9 | This package provides a lightweight extension to the included token 10 | authentication in 11 | [Django Rest Framework](http://www.django-rest-framework.org/), causing tokens 12 | to expire after a specified duration. 13 | 14 | This behaviour is good practice when using token authentication for production 15 | APIs. 16 | If you require more complex token functionality, you're probably better off 17 | looking at one of the OAuth2 implementations available for Django Rest 18 | Framework. 19 | 20 | This package was inspired by this 21 | [Stack Overflow answer](http://stackoverflow.com/a/15380732). 22 | 23 | ## Installation 24 | 25 | Expiring Tokens is tested against the latest versions of Django 1.6, 1.7 and 26 | the 1.8 preview release, and Django Rest Framework 3.1.1. 27 | It should in theory support Django 1.4. 28 | 29 | Grab the package from PyPI. 30 | 31 | ```zsh 32 | pip install djangorestframework-expiring-authtoken 33 | ``` 34 | 35 | As this package uses a proxy model on the original Token model, the first step 36 | is to setup the default 37 | [TokenAuthentication](http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication) 38 | scheme, and check that it works. 39 | 40 | Then, add the package to `INSTALLED_APPS` along with `rest_framework.authtoken` in `settings.py`. 41 | 42 | ```python 43 | INSTALLED_APPS = [ 44 | ... 45 | 'rest_framework', 46 | 'rest_framework.authtoken', 47 | 'rest_framework_expiring_authtoken', 48 | ... 49 | ] 50 | ``` 51 | 52 | Specify the desired lifespan of a token with `EXPIRING_TOKEN_LIFESPAN` in 53 | `settings.py` using a 54 | [timedelta object](https://docs.python.org/2/library/datetime.html#timedelta-objects). 55 | If not set, the default is 30 days. 56 | 57 | ```python 58 | import datetime 59 | EXPIRING_TOKEN_LIFESPAN = datetime.timedelta(days=25) 60 | ``` 61 | 62 | [Set the authentication scheme](http://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme) to `rest_framework_expiring_authtoken.authentication.ExpiringTokenAuthentication` 63 | on a default or per-view basis. 64 | 65 | If you used the `obtain_auth_token` view, you'll need to replace it with the `obtain_expiring_auth_token` view in your URLconf. 66 | 67 | ```python 68 | from rest_framework_expiring_authtoken import views 69 | urlpatterns += [ 70 | url(r'^api-token-auth/', views.obtain_expiring_auth_token) 71 | ] 72 | ``` 73 | 74 | If using Django 1.7 or later, you'll need to run `migrate`, even though nothing 75 | is changed, as Django requires proxy models that inherit from models in an 76 | app with migrations to also have migrations. 77 | 78 | ```zsh 79 | python manage.py migrate 80 | ``` 81 | 82 | ## Usage 83 | 84 | Expiring Tokens works exactly the same as the default TokenAuth, except that using an expired token will return a response with an HTTP 400 status and a `Token has expired` error message. 85 | 86 | The `obtain_expiring_auth_token` view works exactly the same as the `obtain_auth_token` view, except it will replace existing tokens that have expired with a new token. 87 | 88 | ## Improvements 89 | 90 | * Variable token lifespans. 91 | * Possibly change `obtain_expiring_auth_token` to always replace an existing token. (Configurable?) 92 | * South Migrations 93 | 94 | ## Contributors 95 | 96 | * [James Ritchie](https://github.com/JamesRitchie) 97 | * [fcasas](https://github.com/fcasas) 98 | 99 | ## Changelog 100 | 101 | * 0.1.4 102 | * Fixed a typo causing an incorrect 500 error response with an invalid token. 103 | * Support Django 1.10 and Django Rest Framework 3.4 104 | * 0.1.3 105 | * Set a default token lifespan of 30 days. 106 | * 0.1.2 107 | * Changed from deprecated `request.DATA` to `request.data` 108 | * 0.1.1 109 | * Initial release 110 | -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/__init__.py: -------------------------------------------------------------------------------- 1 | """Package adding time expiration to Django REST Framework's auth tokens.""" 2 | 3 | __all__ = [ 4 | 'authentication', 5 | 'models', 6 | 'views' 7 | ] 8 | 9 | __version__ = '0.1.4' 10 | -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/authentication.py: -------------------------------------------------------------------------------- 1 | """Authentication classes for Django Rest Framework. 2 | 3 | Classes: 4 | ExpiringTokenAuthentication: Authentication using extended authtoken model. 5 | """ 6 | 7 | from rest_framework import exceptions 8 | from rest_framework.authentication import TokenAuthentication 9 | 10 | from rest_framework_expiring_authtoken.models import ExpiringToken 11 | 12 | 13 | class ExpiringTokenAuthentication(TokenAuthentication): 14 | 15 | """ 16 | Extends default token auth to have time-based expiration. 17 | 18 | Based on http://stackoverflow.com/questions/14567586/ 19 | """ 20 | 21 | model = ExpiringToken 22 | 23 | def authenticate_credentials(self, key): 24 | """Attempt token authentication using the provided key.""" 25 | try: 26 | token = self.model.objects.get(key=key) 27 | except self.model.DoesNotExist: 28 | raise exceptions.AuthenticationFailed('Invalid token') 29 | 30 | if not token.user.is_active: 31 | raise exceptions.AuthenticationFailed('User inactive or deleted') 32 | 33 | if token.expired(): 34 | raise exceptions.AuthenticationFailed('Token has expired') 35 | 36 | return (token.user, token) 37 | -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('authtoken', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ExpiringToken', 16 | fields=[ 17 | ], 18 | options={ 19 | 'proxy': True, 20 | }, 21 | bases=('authtoken.token',), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesRitchie/django-rest-framework-expiring-tokens/e62f1f92a621575174172e970da624d367ac0cf6/rest_framework_expiring_authtoken/migrations/__init__.py -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/models.py: -------------------------------------------------------------------------------- 1 | """Expiring Token models. 2 | 3 | Classes: 4 | ExpiringToken 5 | """ 6 | 7 | from django.utils import timezone 8 | 9 | from rest_framework.authtoken.models import Token 10 | 11 | from rest_framework_expiring_authtoken.settings import token_settings 12 | 13 | 14 | class ExpiringToken(Token): 15 | 16 | """Extend Token to add an expired method.""" 17 | 18 | class Meta(object): 19 | proxy = True 20 | 21 | def expired(self): 22 | """Return boolean indicating token expiration.""" 23 | now = timezone.now() 24 | if self.created < now - token_settings.EXPIRING_TOKEN_LIFESPAN: 25 | return True 26 | return False 27 | -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides access to settings. 3 | 4 | Returns defaults if not set. 5 | """ 6 | from datetime import timedelta 7 | 8 | from django.conf import settings 9 | 10 | 11 | class TokenSettings(object): 12 | 13 | """Provides settings as defaults for working with tokens.""" 14 | 15 | @property 16 | def EXPIRING_TOKEN_LIFESPAN(self): 17 | """ 18 | Return the allowed lifespan of a token as a TimeDelta object. 19 | 20 | Defaults to 30 days. 21 | """ 22 | try: 23 | val = settings.EXPIRING_TOKEN_LIFESPAN 24 | except AttributeError: 25 | val = timedelta(days=30) 26 | 27 | return val 28 | 29 | token_settings = TokenSettings() 30 | -------------------------------------------------------------------------------- /rest_framework_expiring_authtoken/views.py: -------------------------------------------------------------------------------- 1 | """Utility views for Expiring Tokens. 2 | 3 | Classes: 4 | ObtainExpiringAuthToken: View to provide tokens to clients. 5 | """ 6 | from rest_framework.authtoken.serializers import AuthTokenSerializer 7 | from rest_framework.authtoken.views import ObtainAuthToken 8 | from rest_framework.response import Response 9 | from rest_framework.status import HTTP_400_BAD_REQUEST 10 | 11 | from rest_framework_expiring_authtoken.models import ExpiringToken 12 | 13 | 14 | class ObtainExpiringAuthToken(ObtainAuthToken): 15 | 16 | """View enabling username/password exchange for expiring token.""" 17 | 18 | model = ExpiringToken 19 | 20 | def post(self, request): 21 | """Respond to POSTed username/password with token.""" 22 | serializer = AuthTokenSerializer(data=request.data) 23 | 24 | if serializer.is_valid(): 25 | token, _ = ExpiringToken.objects.get_or_create( 26 | user=serializer.validated_data['user'] 27 | ) 28 | 29 | if token.expired(): 30 | # If the token is expired, generate a new one. 31 | token.delete() 32 | token = ExpiringToken.objects.create( 33 | user=serializer.validated_data['user'] 34 | ) 35 | 36 | data = {'token': token.key} 37 | return Response(data) 38 | 39 | return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) 40 | 41 | obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view() 42 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | def run(): 7 | """Run tests with Django setup.""" 8 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' 9 | from django.conf import settings 10 | 11 | # Will only work in 1.7 12 | try: 13 | from django import setup 14 | except ImportError: 15 | pass 16 | else: 17 | setup() 18 | 19 | from django.test.utils import get_runner 20 | 21 | TestRunner = get_runner(settings) 22 | test_runner = TestRunner() 23 | failures = test_runner.run_tests(["tests"]) 24 | sys.exit(bool(failures)) 25 | 26 | if __name__ == "__main__": 27 | run() 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup file for rest_framework_sav.""" 2 | import os 3 | import sys 4 | 5 | from setuptools import setup, find_packages 6 | 7 | import rest_framework_expiring_authtoken 8 | 9 | 10 | version = rest_framework_expiring_authtoken.__version__ 11 | 12 | if sys.argv[-1] == 'publish': 13 | if os.system("pip list | grep wheel"): 14 | print("wheel not installed.\nUse `pip install wheel`.\nExiting.") 15 | sys.exit() 16 | if os.system("pip freeze | grep twine"): 17 | print("twine not installed.\nUse `pip install twine`.\nExiting.") 18 | sys.exit() 19 | os.system("python setup.py sdist bdist_wheel") 20 | os.system("twine upload dist/*") 21 | print("You probably want to also tag the version now:") 22 | print(" git tag -a %s -m 'version %s'" % (version, version)) 23 | print(" git push --tags") 24 | sys.exit() 25 | 26 | setup( 27 | name='djangorestframework-expiring-authtoken', 28 | version=version, 29 | description='Expiring Authentication Tokens for Django REST Framework', 30 | url=( 31 | 'https://github.com/JamesRitchie/django-rest-framework-expiring-tokens' 32 | ), 33 | author='James Ritchie', 34 | author_email='james.a.ritchie@gmail.com', 35 | license='BSD', 36 | packages=find_packages(exclude=['tests*']), 37 | install_requires=[ 38 | 'djangorestframework>=3.2.3,<=3.4.6' 39 | ], 40 | test_suite='runtests.run', 41 | tests_require=[ 42 | 'Django>=1.8.14,<=1.10.1' 43 | ], 44 | zip_safe=False, 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Environment :: Web Environment', 48 | 'Framework :: Django', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: BSD License', 51 | 'Operating System :: OS Independent', 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 2.6', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3', 56 | 'Programming Language :: Python :: 3.2', 57 | 'Programming Language :: Python :: 3.3', 58 | 'Programming Language :: Python :: 3.4', 59 | 'Topic :: Internet :: WWW/HTTP', 60 | ] 61 | ) 62 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamesRitchie/django-rest-framework-expiring-tokens/e62f1f92a621575174172e970da624d367ac0cf6/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | SECRET_KEY = 'fake-key' 4 | 5 | MIDDLEWARE_CLASSES = [ 6 | 'django.middleware.common.CommonMiddleware', 7 | 'django.contrib.sessions.middleware.SessionMiddleware', 8 | 'django.middleware.csrf.CsrfViewMiddleware', 9 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 10 | ] 11 | 12 | INSTALLED_APPS = [ 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.sites', 17 | 'django.contrib.messages', 18 | 'django.contrib.staticfiles', 19 | 20 | 'rest_framework', 21 | 'rest_framework.authtoken', 22 | 'rest_framework_expiring_authtoken', 23 | "tests", 24 | ] 25 | 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'django.db.backends.sqlite3', 29 | 'NAME': ':memory:' 30 | } 31 | } 32 | 33 | ROOT_URLCONF = 'tests.urls' 34 | -------------------------------------------------------------------------------- /tests/test_authentication.py: -------------------------------------------------------------------------------- 1 | """Tests for Expiring Tokens authentication class.""" 2 | from datetime import timedelta 3 | from time import sleep 4 | 5 | from django.contrib.auth.models import User 6 | from django.test import TestCase 7 | 8 | from rest_framework_expiring_authtoken.authentication import ( 9 | ExpiringTokenAuthentication 10 | ) 11 | from rest_framework.exceptions import AuthenticationFailed 12 | from rest_framework_expiring_authtoken.models import ExpiringToken 13 | 14 | 15 | class ExpiringTokenAuthenticationTestCase(TestCase): 16 | 17 | """Test the authentication class directly.""" 18 | 19 | def setUp(self): 20 | """Create a user and associated token.""" 21 | self.username = 'test' 22 | self.email = 'test@test.com' 23 | self.password = 'test' 24 | self.user = User.objects.create_user( 25 | username=self.username, 26 | email=self.email, 27 | password=self.password 28 | ) 29 | 30 | self.key = 'abc123' 31 | self.token = ExpiringToken.objects.create( 32 | user=self.user, 33 | key=self.key 34 | ) 35 | 36 | self.test_instance = ExpiringTokenAuthentication() 37 | 38 | def test_valid_token(self): 39 | """Check that a valid token authenticates correctly.""" 40 | result = self.test_instance.authenticate_credentials(self.key) 41 | 42 | self.assertEqual(result[0], self.user) 43 | self.assertEqual(result[1], self.token) 44 | 45 | def test_invalid_token(self): 46 | """Check that an invalid token does not authenticated.""" 47 | try: 48 | self.test_instance.authenticate_credentials('xyz789') 49 | except AuthenticationFailed as e: 50 | self.assertEqual(e.__str__(), 'Invalid token') 51 | else: 52 | self.fail("AuthenticationFailed not raised.") 53 | 54 | def test_inactive_user(self): 55 | """Check that a token for an inactive user cannot authenticate.""" 56 | # Make the user inactive 57 | self.user.is_active = False 58 | self.user.save() 59 | 60 | try: 61 | self.test_instance.authenticate_credentials(self.key) 62 | except AuthenticationFailed as e: 63 | self.assertEqual(e.__str__(), 'User inactive or deleted') 64 | else: 65 | self.fail("AuthenticationFailed not raised.") 66 | 67 | def test_expired_token(self): 68 | """Check that an expired token cannot authenticate.""" 69 | # Crude, but necessary as auto_now_add field can't be changed. 70 | with self.settings(EXPIRING_TOKEN_LIFESPAN=timedelta(milliseconds=1)): 71 | sleep(0.001) 72 | 73 | try: 74 | self.test_instance.authenticate_credentials(self.key) 75 | except AuthenticationFailed as e: 76 | self.assertEqual(e.__str__(), 'Token has expired') 77 | else: 78 | self.fail("AuthenticationFailed not raised.") 79 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | """Tests for Expiring Token models. 2 | 3 | Classes: 4 | ExpiringTokenTestCase: Tests ExpiringToken. 5 | """ 6 | from datetime import timedelta 7 | from time import sleep 8 | 9 | from django.contrib.auth.models import User 10 | from django.test import TestCase 11 | 12 | from rest_framework_expiring_authtoken.models import ExpiringToken 13 | 14 | 15 | class ExpiringTokenTestCase(TestCase): 16 | 17 | """Test case for Expiring Token model.""" 18 | 19 | def setUp(self): 20 | """Create a user and associated token.""" 21 | self.username = 'test' 22 | self.email = 'test@test.com' 23 | self.password = 'test' 24 | self.user = User.objects.create_user( 25 | username=self.username, 26 | email=self.email, 27 | password=self.password 28 | ) 29 | 30 | self.key = 'abc123' 31 | self.token = ExpiringToken.objects.create( 32 | user=self.user, 33 | key=self.key 34 | ) 35 | 36 | def test_expired_indated(self): 37 | """Check the expired method returns false for an indated token.""" 38 | self.assertFalse(self.token.expired()) 39 | 40 | def test_expired_outdated(self): 41 | """Check the expired method return true for an outdated token.""" 42 | # Crude, but necessary as auto_now_add field can't be changed. 43 | with self.settings(EXPIRING_TOKEN_LIFESPAN=timedelta(milliseconds=1)): 44 | sleep(0.001) 45 | self.assertTrue(self.token.expired()) 46 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | """Tests for Django Rest Framework Session Authentication package.""" 2 | from datetime import timedelta 3 | from time import sleep 4 | 5 | from django.contrib.auth.models import User 6 | 7 | from rest_framework import status 8 | from rest_framework.test import APITestCase 9 | 10 | from rest_framework_expiring_authtoken.models import ExpiringToken 11 | 12 | 13 | class ObtainExpiringTokenViewTestCase(APITestCase): 14 | 15 | """Tests for the Obtain Expiring Token View.""" 16 | 17 | def setUp(self): 18 | """Create a user.""" 19 | self.username = 'test' 20 | self.email = 'test@test.com' 21 | self.password = 'test' 22 | self.user = User.objects.create_user( 23 | username=self.username, 24 | email=self.email, 25 | password=self.password 26 | ) 27 | 28 | def test_post(self): 29 | """Check token can be obtained by posting credentials.""" 30 | token = ExpiringToken.objects.create(user=self.user) 31 | 32 | response = self.client.post( 33 | '/obtain-token/', 34 | { 35 | 'username': self.username, 36 | 'password': self.password 37 | } 38 | ) 39 | 40 | self.assertEqual(response.status_code, status.HTTP_200_OK) 41 | 42 | # Check the response contains the token key. 43 | self.assertEqual(token.key, response.data['token']) 44 | 45 | def test_post_create_token(self): 46 | """Check token is created if none exists.""" 47 | response = self.client.post( 48 | '/obtain-token/', 49 | { 50 | 'username': self.username, 51 | 'password': self.password 52 | } 53 | ) 54 | 55 | self.assertEqual(response.status_code, status.HTTP_200_OK) 56 | 57 | # Check token was created and the response contains the token key. 58 | token = ExpiringToken.objects.first() 59 | self.assertEqual(token.user, self.user) 60 | self.assertEqual(response.data['token'], token.key) 61 | 62 | def test_post_no_credentials(self): 63 | """Check POST request with no credentials fails.""" 64 | response = self.client.post('/obtain-token/') 65 | 66 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 67 | self.assertEqual(response.data, 68 | { 69 | 'username': ['This field is required.'], 70 | 'password': ['This field is required.'] 71 | } 72 | ) 73 | 74 | def test_post_wrong_credentials(self): 75 | """Check POST request with wrong credentials fails.""" 76 | response = self.client.post( 77 | '/obtain-token/', 78 | { 79 | 'username': self.username, 80 | 'password': 'wrong' 81 | } 82 | ) 83 | 84 | self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) 85 | self.assertEqual(response.data, 86 | { 87 | 'non_field_errors': [ 88 | 'Unable to log in with provided credentials.' 89 | ] 90 | } 91 | ) 92 | 93 | def test_post_expired_token(self): 94 | """Check that expired tokens are replaced.""" 95 | token = ExpiringToken.objects.create(user=self.user) 96 | key_1 = token.key 97 | 98 | # Make the first token expire. 99 | with self.settings(EXPIRING_TOKEN_LIFESPAN=timedelta(milliseconds=1)): 100 | sleep(0.001) 101 | response = self.client.post( 102 | '/obtain-token/', 103 | { 104 | 'username': self.username, 105 | 'password': self.password 106 | } 107 | ) 108 | 109 | self.assertEqual(response.status_code, status.HTTP_200_OK) 110 | 111 | # Check token was renewed and the response contains the token key. 112 | token = ExpiringToken.objects.first() 113 | key_2 = token.key 114 | self.assertEqual(token.user, self.user) 115 | self.assertEqual(response.data['token'], token.key) 116 | self.assertTrue(key_1 != key_2) 117 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | """URL conf for testing Expiring Tokens.""" 2 | from django.conf.urls import url 3 | 4 | from rest_framework_expiring_authtoken.views import obtain_expiring_auth_token 5 | 6 | urlpatterns = [ 7 | url(r'^obtain-token/$', obtain_expiring_auth_token), 8 | ] 9 | --------------------------------------------------------------------------------