From a4208c46f591e9602bfe8c52a1684cef545d1684 Mon Sep 17 00:00:00 2001 From: Jonathan Liuti Date: Thu, 2 May 2024 10:29:45 +0200 Subject: [PATCH] Revert "Merge branch 'develop' into master" This reverts commit 44a0296c492fc279f514202df3ba89da26cba4a4, reversing changes made to 238b1ea061de4dcb86e78e2b5ca6124f2befbeba. --- .codecov.yml | 11 -- .coveragerc | 5 - .github/workflows/test.yml | 18 +-- .gitignore | 3 - .pre-commit-config.yaml | 9 -- CHANGELOG.md | 4 +- CONTRIBUTING.md | 3 - README.md | 25 ++-- docs/auth.md | 18 +-- docs/index.md | 4 +- docs/installation.md | 25 +++- docs/settings.md | 33 +---- docs/views.md | 19 +-- knox/auth.py | 18 ++- knox/crypto.py | 33 +++-- knox/migrations/0001_initial.py | 1 - knox/migrations/0006_auto_20160818_0932.py | 4 +- .../migrations/0009_extend_authtoken_field.py | 18 --- knox/models.py | 52 ++------ knox/settings.py | 11 +- knox/views.py | 31 ++--- knox_project/settings.py | 4 +- mkdocs.yml | 2 +- setup.py | 5 +- tests/tests.py | 119 ++++-------------- tox.ini | 17 +-- 26 files changed, 141 insertions(+), 351 deletions(-) delete mode 100644 .codecov.yml delete mode 100644 .coveragerc delete mode 100644 .pre-commit-config.yaml delete mode 100644 CONTRIBUTING.md delete mode 100644 knox/migrations/0009_extend_authtoken_field.py diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index cfce0f82..00000000 --- a/.codecov.yml +++ /dev/null @@ -1,11 +0,0 @@ -coverage: - status: - project: - default: false - tests: - paths: tests - informational: true - knox: - paths: knox - informational: true - patch: off \ No newline at end of file diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 9d0f838e..00000000 --- a/.coveragerc +++ /dev/null @@ -1,5 +0,0 @@ -[run] -branch = True -source = knox -omit = - */migrations/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index de70e1e1..379d39cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,23 +9,23 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Get pip cache dir id: pip-cache run: | - echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT + echo "::set-output name=dir::$(pip cache dir)" - name: Cache - uses: actions/cache@v3 + uses: actions/cache@v2 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -36,14 +36,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install --upgrade tox tox-gh-actions coverage + python -m pip install --upgrade tox tox-gh-actions - name: Tox tests run: | tox -v - - - name: Generate coverage XML report - run: coverage xml - - - name: Codecov - uses: codecov/codecov-action@v3 diff --git a/.gitignore b/.gitignore index 7791a82c..6e5bf840 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,3 @@ docs/_build/ target/ db.sqlite3 site/ - -# PyCharm Project -.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index eac9b90d..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -repos: - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 - hooks: - - id: flake8 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8280b3ac..96a31be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 4.2.0 -- compatibility with Python up to 3.10 and Django up to 4.0 +- compatibility with Python up to 3.10 and Django up to 3.2 - integration with github CI instead of travis -- Migration: "salt" field of model "AuthToken" is removed, WARNING: invalidates old tokens! +- Migration: "salt" field of model "AuthToken" is removed ## 4.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 10d79191..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) - -This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). \ No newline at end of file diff --git a/README.md b/README.md index 001fccc2..203db1f1 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ django-rest-knox ================ -[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) -[![image](https://github.com/jazzband/django-rest-knox/workflows/Test/badge.svg?branch=develop)](https://github.com/jazzband/django-rest-knox/actions) +[![image](https://github.com/James1345/django-rest-knox/workflows/Test/badge.svg?branch=develop)](https://github.com/James1345/django-rest-knox/actions) -Authentication module for Django rest auth. +Authentication Module for django rest auth -Knox provides easy-to-use authentication for [Django REST +Knox provides easy to use authentication for [Django REST Framework](https://www.django-rest-framework.org/) The aim is to allow -for common patterns in applications that are REST-based, with little +for common patterns in applications that are REST based, with little extra effort; and to ensure that connections remain secure. -Knox authentication is token-based, similar to the `TokenAuthentication` -built into DRF. However, it overcomes some problems present in the +Knox authentication is token based, similar to the `TokenAuthentication` +built in to DRF. However, it overcomes some problems present in the default implementation: - DRF tokens are limited to one per user. This does not facilitate @@ -24,14 +23,13 @@ default implementation: client to have its own token which is deleted on the server side when the client logs out. - Knox also provides an option for a logged-in client to remove *all* + Knox also provides an option for a logged in client to remove *all* tokens that the server has - forcing all clients to re-authenticate. - DRF tokens are stored unencrypted in the database. This would allow - an attacker unrestricted access to an account with a token if the + an attacker unrestricted access to an account with a token if the database were compromised. - Knox tokens are only stored in a secure hash form (like a password). Even if the database were somehow stolen, an attacker would not be able to log in with the stolen credentials. @@ -41,11 +39,12 @@ default implementation: the app settings (default is 10 hours.) More information can be found in the -[Documentation](https://jazzband.github.io/django-rest-knox/) +[Documentation](https://james1345.github.io/django-rest-knox/) # Run the tests locally -If you need to debug a test locally and if you have [docker](https://www.docker.com/) installed, +If you need to debug a test locally and if you have [docker](https://www.docker.com/) installed: + simply run the ``./docker-run-tests.sh`` script and it will run the test suite in every Python / Django versions. @@ -56,7 +55,7 @@ Python / Django versions a bit more tricky. Our documentation is generated by [Mkdocs](https://www.mkdocs.org). -You can refer to their [documentation](https://www.mkdocs.org/user-guide/installation/) on how to install it locally. +You can refer to their documentation on how to install it locally. Another option is to use `mkdocs.sh` in this repository. It will run mkdocs in a [docker](https://www.docker.com/) container. diff --git a/docs/auth.md b/docs/auth.md index 8e81c01f..63828ed9 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -49,17 +49,18 @@ If it is your only default authentication class, remember to overwrite knox's Lo For instance, you can authenticate users using Basic Authentication by simply overwriting knox's LoginView and setting BasicAuthentication as one of the acceptable authentication classes, as follows: -**views.py:** ```python + +views.py: + from knox.views import LoginView as KnoxLoginView from rest_framework.authentication import BasicAuthentication class LoginView(KnoxLoginView): authentication_classes = [BasicAuthentication] -``` -**urls.py:** -```python +urls.py: + from knox import views as knox_views from yourapp.api.views import LoginView @@ -74,8 +75,10 @@ You can use any number of authentication classes if you want to be able to authe If you decide to use Token Authentication as your only authentication class, you can overwrite knox's login view as such: -**views.py:** ```python + +views.py: + from django.contrib.auth import login from rest_framework import permissions @@ -91,10 +94,9 @@ class LoginView(KnoxLoginView): user = serializer.validated_data['user'] login(request, user) return super(LoginView, self).post(request, format=None) -``` -**urls.py:** -```python +urls.py: + from knox import views as knox_views from yourapp.api.views import LoginView diff --git a/docs/index.md b/docs/index.md index 4a674fed..1619797b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,10 @@ # Django-Rest-Knox -Knox provides easy-to-use authentication for [Django REST Framework](https://www.django-rest-framework.org/) +Knox provides easy to use authentication for [Django REST Framework](https://www.django-rest-framework.org/) The aim is to allow for common patterns in applications that are REST based, with little extra effort; and to ensure that connections remain secure. Knox authentication is token based, similar to the `TokenAuthentication` built -into DRF. However, it overcomes some problems present in the default implementation: +in to DRF. However, it overcomes some problems present in the default implementation: - DRF tokens are limited to one per user. This does not facilitate securely signing in from multiple devices, as the token is shared. It also requires diff --git a/docs/installation.md b/docs/installation.md index afaeb559..e9a9d83a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,8 +2,27 @@ ## Requirements -Knox depends on pythons internal library `hashlib` to provide bindings to `OpenSSL` or uses -an internal implementation of hashing algorithms for token generation. +Knox depends on `cryptography` to provide bindings to `OpenSSL` for token generation. +This requires the OpenSSL build libraries to be available. + +### Windows +Cryptography is a statically linked build, no extra steps are needed. + +### Linux +`cryptography` should build very easily on Linux provided you have a C compiler, +headers for Python (if you’re not using `pypy`), and headers for the OpenSSL and +`libffi` libraries available on your system. + +Debian and Ubuntu: +```bash +sudo apt-get install build-essential libssl-dev libffi-dev python3-dev python-dev +``` + +Fedora and RHEL-derivatives: +```bash +sudo yum install gcc libffi-devel python-devel openssl-devel +``` +For other systems or problems, see the [cryptography installation docs](https://cryptography.io/en/latest/installation/) ## Installing Knox Knox should be installed with pip @@ -40,7 +59,7 @@ REST_FRAMEWORK = { - If you set TokenAuthentication as the only default authentication class on the second step, [override knox's LoginView](auth.md#global-usage-on-all-views) to accept another authentication method and use it instead of knox's default login view. -- Apply the migrations for the models. +- Apply the migrations for the models ```bash python manage.py migrate diff --git a/docs/settings.md b/docs/settings.md index 0ba2317d..e44e322b 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -10,27 +10,18 @@ Example `settings.py` # These are the default values if none are set from datetime import timedelta from rest_framework.settings import api_settings - -KNOX_TOKEN_MODEL = 'knox.AuthToken' - REST_KNOX = { - 'SECURE_HASH_ALGORITHM': 'hashlib.sha512', + 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', 'AUTH_TOKEN_CHARACTER_LENGTH': 64, 'TOKEN_TTL': timedelta(hours=10), 'USER_SERIALIZER': 'knox.serializers.UserSerializer', 'TOKEN_LIMIT_PER_USER': None, 'AUTO_REFRESH': False, - 'MIN_REFRESH_INTERVAL': 60, - 'AUTH_HEADER_PREFIX': 'Token', 'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT, - 'TOKEN_MODEL': 'knox.AuthToken', } #...snip... ``` -## KNOX_TOKEN_MODEL -This is the variable used in the swappable dependency of the `AuthToken` model - ## SECURE_HASH_ALGORITHM This is a reference to the class used to provide the hashing algorithm for token storage. @@ -39,13 +30,14 @@ token storage. By default, Knox uses SHA-512 to hash tokens in the database. -`hashlib.sha3_512` is an acceptable alternative setting for production use. +`cryptography.hazmat.primitives.hashes.Whirlpool` is an acceptable alternative setting +for production use. ### Tests -SHA-512 and SHA3-512 are secure, however, they are slow. This should not be a +SHA-512 and Whirlpool are secure, however, they are slow. This should not be a problem for your users, but when testing it may be noticeable (as test cases tend to use many more requests much more quickly than real users). In testing scenarios -it is acceptable to use `MD5` hashing (`hashlib.md5`). +it is acceptable to use `MD5` hashing.(`cryptography.hazmat.primitives.hashes.MD5`) MD5 is **not secure** and must *never* be used in production sites. @@ -66,8 +58,7 @@ Warning: setting a 0 or negative timedelta will create tokens that instantly exp the system will not prevent you setting this. ## TOKEN_LIMIT_PER_USER -This allows you to control how many valid tokens can be issued per user. -If the limit for valid tokens is reached, an error is returned at login. +This allows you to control how many tokens can be issued per user. By default this option is disabled and set to `None` -- thus no limit. ## USER_SERIALIZER @@ -90,18 +81,9 @@ This is the expiry datetime format returned in the login view. The default is th [DATETIME_FORMAT][DATETIME_FORMAT] of Django REST framework. May be any of `None`, `iso-8601` or a Python [strftime format][strftime format] string. -## TOKEN_MODEL -This is the reference to the model used as `AuthToken`. We can define a custom `AuthToken` -model in our project that extends `knox.AbstractAuthToken` and add our business logic to it. -The default is `knox.AuthToken` - [DATETIME_FORMAT]: https://www.django-rest-framework.org/api-guide/settings/#date-and-time-formatting [strftime format]: https://docs.python.org/3/library/time.html#time.strftime -## TOKEN_PREFIX -This is the prefix for the generated token that is used in the Authorization header. The default is just an empty string. -It can be up to `CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH` long. - # Constants `knox.settings` Knox also provides some constants for information. These must not be changed in external code; they are used in the model definitions in knox and an error will @@ -115,6 +97,3 @@ print(CONSTANTS.DIGEST_LENGTH) #=> 128 ## DIGEST_LENGTH This is the length of the digest that will be stored in the database for each token. - -## MAXIMUM_TOKEN_PREFIX_LENGTH -This is the maximum length of the token prefix. diff --git a/docs/views.md b/docs/views.md index 285222da..c8ed762b 100644 --- a/docs/views.md +++ b/docs/views.md @@ -21,7 +21,6 @@ helper methods: - `get_user_serializer_class(self)`, to change the class used for serializing the user - `get_expiry_datetime_format(self)`, to change the datetime format used for expiry - `format_expiry_datetime(self, expiry)`, to format the expiry `datetime` object at your convenience -- `create_token(self)`, to create the `AuthToken` instance at your convenience Finally, if none of these helper methods are sufficient, you can also override `get_post_response_data` to return a fully customized payload. @@ -67,24 +66,12 @@ It responds to Knox Token Authentication. On a successful request, the token used to authenticate is deleted from the system and can no longer be used to authenticate. -By default, this endpoint returns a HTTP 204 response on a successful request. To -customize this behavior, you can override the `get_post_response` method, for example -to include a body in the logout response and/or to modify the status code: - -```python -...snip... - def get_post_response(self, request): - return Response({"bye-bye": request.user.username}, status=200) -...snip... -``` - ## LogoutAllView This view accepts only a post request with an empty body. It responds to Knox Token Authentication. -On a successful request, a HTTP 204 is returned and the token used to authenticate, -and *all other tokens* registered to the same `User` account, are deleted from the -system and can no longer be used to authenticate. The success response can be modified -like the `LogoutView` by overriding the `get_post_response` method. +On a successful request, the token used to authenticate, and *all other tokens* +registered to the same `User` account, are deleted from the +system and can no longer be used to authenticate. **Note** It is not recommended to alter the Logout views. They are designed specifically for token management, and to respond to Knox authentication. diff --git a/knox/auth.py b/knox/auth.py index 858ee504..c013c4dd 100644 --- a/knox/auth.py +++ b/knox/auth.py @@ -1,5 +1,10 @@ +try: + from hmac import compare_digest +except ImportError: + def compare_digest(a, b): + return a == b + import binascii -from hmac import compare_digest from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -9,7 +14,7 @@ ) from knox.crypto import hash_token -from knox.models import get_token_model +from knox.models import AuthToken from knox.settings import CONSTANTS, knox_settings from knox.signals import token_expired @@ -26,10 +31,11 @@ class TokenAuthentication(BaseAuthentication): - `request.user` will be a django `User` instance - `request.auth` will be an `AuthToken` instance ''' + model = AuthToken def authenticate(self, request): auth = get_authorization_header(request).split() - prefix = self.authenticate_header(request).encode() + prefix = knox_settings.AUTH_HEADER_PREFIX.encode() if not auth: return None @@ -56,7 +62,7 @@ def authenticate_credentials(self, token): ''' msg = _('Invalid token.') token = token.decode("utf-8") - for auth_token in get_token_model().objects.filter( + for auth_token in AuthToken.objects.filter( token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH]): if self._cleanup_token(auth_token): continue @@ -71,7 +77,7 @@ def authenticate_credentials(self, token): return self.validate_user(auth_token) raise exceptions.AuthenticationFailed(msg) - def renew_token(self, auth_token) -> None: + def renew_token(self, auth_token): current_expiry = auth_token.expiry new_expiry = timezone.now() + knox_settings.TOKEN_TTL auth_token.expiry = new_expiry @@ -89,7 +95,7 @@ def validate_user(self, auth_token): def authenticate_header(self, request): return knox_settings.AUTH_HEADER_PREFIX - def _cleanup_token(self, auth_token) -> bool: + def _cleanup_token(self, auth_token): for other_token in auth_token.user.auth_token_set.all(): if other_token.digest != auth_token.digest and other_token.expiry: if other_token.expiry < timezone.now(): diff --git a/knox/crypto.py b/knox/crypto.py index 02e70ffe..dba4f754 100644 --- a/knox/crypto.py +++ b/knox/crypto.py @@ -1,31 +1,28 @@ import binascii from os import urandom as generate_bytes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes + from knox.settings import knox_settings -hash_func = knox_settings.SECURE_HASH_ALGORITHM +sha = knox_settings.SECURE_HASH_ALGORITHM -def create_token_string() -> str: +def create_token_string(): return binascii.hexlify( generate_bytes(int(knox_settings.AUTH_TOKEN_CHARACTER_LENGTH / 2)) ).decode() -def make_hex_compatible(token: str) -> bytes: - """ - We need to make sure that the token, that is send is hex-compatible. - When a token prefix is used, we cannot guarantee that. - """ - return binascii.unhexlify(binascii.hexlify(bytes(token, 'utf-8'))) - - -def hash_token(token: str) -> str: - """ +def hash_token(token): + ''' Calculates the hash of a token. - Token must contain an even number of hex digits or - a binascii.Error exception will be raised. - """ - digest = hash_func() - digest.update(make_hex_compatible(token)) - return digest.hexdigest() + input is unhexlified + + token must contain an even number of hex digits or a binascii.Error + exception will be raised + ''' + digest = hashes.Hash(sha(), backend=default_backend()) + digest.update(binascii.unhexlify(token)) + return binascii.hexlify(digest.finalize()).decode() diff --git a/knox/migrations/0001_initial.py b/knox/migrations/0001_initial.py index 822176ea..e2b157ec 100644 --- a/knox/migrations/0001_initial.py +++ b/knox/migrations/0001_initial.py @@ -9,7 +9,6 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - migrations.swappable_dependency(settings.KNOX_TOKEN_MODEL), ] operations = [ diff --git a/knox/migrations/0006_auto_20160818_0932.py b/knox/migrations/0006_auto_20160818_0932.py index ea3e5303..b8540905 100644 --- a/knox/migrations/0006_auto_20160818_0932.py +++ b/knox/migrations/0006_auto_20160818_0932.py @@ -7,7 +7,7 @@ def cleanup_tokens(apps, schema_editor): AuthToken = apps.get_model('knox', 'AuthToken') - AuthToken.objects.using(schema_editor.connection.alias).filter(token_key__isnull=True).delete() + AuthToken.objects.filter(token_key__isnull=True).delete() class Migration(migrations.Migration): @@ -17,7 +17,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(cleanup_tokens, reverse_code=migrations.RunPython.noop), + migrations.RunPython(cleanup_tokens), migrations.AlterField( model_name='authtoken', name='token_key', diff --git a/knox/migrations/0009_extend_authtoken_field.py b/knox/migrations/0009_extend_authtoken_field.py deleted file mode 100644 index 18a33836..00000000 --- a/knox/migrations/0009_extend_authtoken_field.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.1rc1 on 2022-07-20 17:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("knox", "0008_remove_authtoken_salt"), - ] - - operations = [ - migrations.AlterField( - model_name="authtoken", - name="token_key", - field=models.CharField(db_index=True, max_length=25), - ), - ] diff --git a/knox/models.py b/knox/models.py index 32f95ea4..4dbee1ae 100644 --- a/knox/models.py +++ b/knox/models.py @@ -1,75 +1,39 @@ -from django.apps import apps from django.conf import settings -from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils import timezone from knox import crypto from knox.settings import CONSTANTS, knox_settings -sha = knox_settings.SECURE_HASH_ALGORITHM - User = settings.AUTH_USER_MODEL class AuthTokenManager(models.Manager): - def create( - self, - user, - expiry=knox_settings.TOKEN_TTL, - prefix=knox_settings.TOKEN_PREFIX - ): - token = prefix + crypto.create_token_string() + def create(self, user, expiry=knox_settings.TOKEN_TTL): + token = crypto.create_token_string() digest = crypto.hash_token(token) + if expiry is not None: expiry = timezone.now() + expiry + instance = super(AuthTokenManager, self).create( token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH], digest=digest, user=user, expiry=expiry) return instance, token -class AbstractAuthToken(models.Model): +class AuthToken(models.Model): objects = AuthTokenManager() digest = models.CharField( max_length=CONSTANTS.DIGEST_LENGTH, primary_key=True) token_key = models.CharField( - max_length=CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH + - CONSTANTS.TOKEN_KEY_LENGTH, - db_index=True - ) + max_length=CONSTANTS.TOKEN_KEY_LENGTH, db_index=True) user = models.ForeignKey(User, null=False, blank=False, related_name='auth_token_set', on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) expiry = models.DateTimeField(null=True, blank=True) - class Meta: - abstract = True - - def __str__(self) -> str: - return f'{self.digest} : {self.user}' - - -class AuthToken(AbstractAuthToken): - class Meta: - swappable = 'KNOX_TOKEN_MODEL' - - -def get_token_model(): - """ - Return the AuthToken model that is active in this project. - """ - - try: - return apps.get_model(knox_settings.TOKEN_MODEL, require_ready=False) - except ValueError: - raise ImproperlyConfigured( - "TOKEN_MODEL must be of the form 'app_label.model_name'" - ) - except LookupError: - raise ImproperlyConfigured( - "TOKEN_MODEL refers to model '%s' that has not been installed" - % knox_settings.TOKEN_MODEL - ) + def __str__(self): + return '%s : %s' % (self.digest, self.user) diff --git a/knox/settings.py b/knox/settings.py index d1b09347..5bb14483 100644 --- a/knox/settings.py +++ b/knox/settings.py @@ -7,7 +7,7 @@ USER_SETTINGS = getattr(settings, 'REST_KNOX', None) DEFAULTS = { - 'SECURE_HASH_ALGORITHM': 'hashlib.sha512', + 'SECURE_HASH_ALGORITHM': 'cryptography.hazmat.primitives.hashes.SHA512', 'AUTH_TOKEN_CHARACTER_LENGTH': 64, 'TOKEN_TTL': timedelta(hours=10), 'USER_SERIALIZER': None, @@ -16,8 +16,6 @@ 'MIN_REFRESH_INTERVAL': 60, 'AUTH_HEADER_PREFIX': 'Token', 'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT, - 'TOKEN_MODEL': getattr(settings, 'KNOX_TOKEN_MODEL', 'knox.AuthToken'), - 'TOKEN_PREFIX': '', } IMPORT_STRINGS = { @@ -33,8 +31,6 @@ def reload_api_settings(*args, **kwargs): setting, value = kwargs['setting'], kwargs['value'] if setting == 'REST_KNOX': knox_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS) - if len(knox_settings.TOKEN_PREFIX) > CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH: - raise ValueError("Illegal TOKEN_PREFIX length") setting_changed.connect(reload_api_settings) @@ -44,9 +40,8 @@ class CONSTANTS: ''' Constants cannot be changed at runtime ''' - TOKEN_KEY_LENGTH = 15 + TOKEN_KEY_LENGTH = 8 DIGEST_LENGTH = 128 - MAXIMUM_TOKEN_PREFIX_LENGTH = 10 def __setattr__(self, *args, **kwargs): raise Exception(''' @@ -55,4 +50,4 @@ def __setattr__(self, *args, **kwargs): ''') -CONSTANTS = CONSTANTS() # type: ignore +CONSTANTS = CONSTANTS() diff --git a/knox/views.py b/knox/views.py index 97cbe42c..7975bbeb 100644 --- a/knox/views.py +++ b/knox/views.py @@ -8,7 +8,7 @@ from rest_framework.views import APIView from knox.auth import TokenAuthentication -from knox.models import get_token_model +from knox.models import AuthToken from knox.settings import knox_settings @@ -22,9 +22,6 @@ def get_context(self): def get_token_ttl(self): return knox_settings.TOKEN_TTL - def get_token_prefix(self): - return knox_settings.TOKEN_PREFIX - def get_token_limit_per_user(self): return knox_settings.TOKEN_LIMIT_PER_USER @@ -38,12 +35,6 @@ def format_expiry_datetime(self, expiry): datetime_format = self.get_expiry_datetime_format() return DateTimeField(format=datetime_format).to_representation(expiry) - def create_token(self): - token_prefix = self.get_token_prefix() - return get_token_model().objects.create( - user=self.request.user, expiry=self.get_token_ttl(), prefix=token_prefix - ) - def get_post_response_data(self, request, token, instance): UserSerializer = self.get_user_serializer_class() @@ -58,10 +49,6 @@ def get_post_response_data(self, request, token, instance): ).data return data - def get_post_response(self, request, token, instance): - data = self.get_post_response_data(request, token, instance) - return Response(data) - def post(self, request, format=None): token_limit_per_user = self.get_token_limit_per_user() if token_limit_per_user is not None: @@ -72,24 +59,23 @@ def post(self, request, format=None): {"error": "Maximum amount of tokens allowed per user exceeded."}, status=status.HTTP_403_FORBIDDEN ) - instance, token = self.create_token() + token_ttl = self.get_token_ttl() + instance, token = AuthToken.objects.create(request.user, token_ttl) user_logged_in.send(sender=request.user.__class__, request=request, user=request.user) - return self.get_post_response(request, token, instance) + data = self.get_post_response_data(request, token, instance) + return Response(data) class LogoutView(APIView): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) - def get_post_response(self, request): - return Response(None, status=status.HTTP_204_NO_CONTENT) - def post(self, request, format=None): request._auth.delete() user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) - return self.get_post_response(request) + return Response(None, status=status.HTTP_204_NO_CONTENT) class LogoutAllView(APIView): @@ -100,11 +86,8 @@ class LogoutAllView(APIView): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) - def get_post_response(self, request): - return Response(None, status=status.HTTP_204_NO_CONTENT) - def post(self, request, format=None): request.user.auth_token_set.all().delete() user_logged_out.send(sender=request.user.__class__, request=request, user=request.user) - return self.get_post_response(request) + return Response(None, status=status.HTTP_204_NO_CONTENT) diff --git a/knox_project/settings.py b/knox_project/settings.py index d5b6c40c..85dcc48d 100644 --- a/knox_project/settings.py +++ b/knox_project/settings.py @@ -49,9 +49,7 @@ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True -USE_L10N = True # Deprecated since django 4.0. +USE_L10N = True USE_TZ = True STATIC_URL = '/static/' - -KNOX_TOKEN_MODEL = 'knox.AuthToken' diff --git a/mkdocs.yml b/mkdocs.yml index 2ecec2a3..571ed993 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Django-Rest-Knox -repo_url: https://github.com/jazzband/django-rest-knox +repo_url: https://github.com/James1345/django-rest-knox theme: readthedocs nav: - Home: 'index.md' diff --git a/setup.py b/setup.py index 6ff24321..30d63bdb 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ long_description_content_type='text/markdown', # The project's main homepage. - url='https://github.com/jazzband/django-rest-knox', + url='https://github.com/James1345/django-rest-knox', # Author details author='James McMahon', @@ -51,8 +51,6 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', ], # What does your project relate to? @@ -71,6 +69,7 @@ install_requires=[ 'django>=3.2', 'djangorestframework', + 'cryptography', ], # List additional groups of dependencies here (e.g. development diff --git a/tests/tests.py b/tests/tests.py index 9494db0b..914ce204 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,6 +1,5 @@ import base64 from datetime import datetime, timedelta -from importlib import reload from django.contrib.auth import get_user_model from django.test import override_settings @@ -9,8 +8,9 @@ from rest_framework.exceptions import AuthenticationFailed from rest_framework.serializers import DateTimeField from rest_framework.test import APIRequestFactory, APITestCase as TestCase +from six.moves import reload_module -from knox import auth, crypto, views +from knox import auth, views from knox.auth import TokenAuthentication from knox.models import AuthToken from knox.serializers import UserSerializer @@ -23,7 +23,7 @@ def get_basic_auth_header(username, password): return 'Basic %s' % base64.b64encode( - (f'{username}:{password}').encode('ascii')).decode() + ('%s:%s' % (username, password)).encode('ascii')).decode() auto_refresh_knox = knox_settings.defaults.copy() @@ -45,14 +45,6 @@ def get_basic_auth_header(username, password): expiry_datetime_format_knox = knox_settings.defaults.copy() expiry_datetime_format_knox["EXPIRY_DATETIME_FORMAT"] = EXPIRY_DATETIME_FORMAT -token_prefix = "TEST_" -token_prefix_knox = knox_settings.defaults.copy() -token_prefix_knox["TOKEN_PREFIX"] = token_prefix - -token_prefix_too_long = "a" * CONSTANTS.MAXIMUM_TOKEN_PREFIX_LENGTH + "a" -token_prefix_too_long_knox = knox_settings.defaults.copy() -token_prefix_too_long_knox["TOKEN_PREFIX"] = token_prefix_too_long - class AuthTestCase(TestCase): @@ -94,7 +86,7 @@ def test_login_returns_serialized_token(self): def test_login_returns_serialized_token_and_username_field(self): with override_settings(REST_KNOX=user_serializer_knox): - reload(views) + reload_module(views) self.assertEqual(AuthToken.objects.count(), 0) url = reverse('knox_login') self.client.credentials( @@ -102,7 +94,7 @@ def test_login_returns_serialized_token_and_username_field(self): ) response = self.client.post(url, {}, format='json') self.assertEqual(user_serializer_knox["USER_SERIALIZER"], UserSerializer) - (views) + reload_module(views) self.assertEqual(response.status_code, 200) self.assertIn('token', response.data) username_field = self.user.USERNAME_FIELD @@ -112,7 +104,7 @@ def test_login_returns_serialized_token_and_username_field(self): def test_login_returns_configured_expiry_datetime_format(self): with override_settings(REST_KNOX=expiry_datetime_format_knox): - reload(views) + reload_module(views) self.assertEqual(AuthToken.objects.count(), 0) url = reverse('knox_login') self.client.credentials( @@ -123,7 +115,7 @@ def test_login_returns_configured_expiry_datetime_format(self): expiry_datetime_format_knox["EXPIRY_DATETIME_FORMAT"], EXPIRY_DATETIME_FORMAT ) - reload(views) + reload_module(views) self.assertEqual(response.status_code, 200) self.assertIn('token', response.data) self.assertNotIn('user', response.data) @@ -198,7 +190,7 @@ def test_update_token_key(self): instance, token = AuthToken.objects.create(self.user) rf = APIRequestFactory() request = rf.get('/') - request.META = {'HTTP_AUTHORIZATION': f'Token {token}'} + request.META = {'HTTP_AUTHORIZATION': 'Token {}'.format(token)} (self.user, auth_token) = TokenAuthentication().authenticate(request) self.assertEqual( token[:CONSTANTS.TOKEN_KEY_LENGTH], @@ -258,10 +250,10 @@ def test_token_expiry_is_extended_with_auto_refresh_activated(self): self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) five_hours_later = original_time + timedelta(hours=5) with override_settings(REST_KNOX=auto_refresh_knox): - reload(auth) # necessary to reload settings in core code + reload_module(auth) # necessary to reload settings in core code with freeze_time(five_hours_later): response = self.client.get(root_url, {}, format='json') - reload(auth) + reload_module(auth) self.assertEqual(response.status_code, 200) # original expiry date was extended: @@ -310,10 +302,10 @@ def test_token_expiry_is_not_extended_within_MIN_REFRESH_INTERVAL(self): self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) in_min_interval = now + timedelta(seconds=knox_settings.MIN_REFRESH_INTERVAL - 10) with override_settings(REST_KNOX=auto_refresh_knox): - reload(auth) # necessary to reload settings in core code + reload_module(auth) # necessary to reload settings in core code with freeze_time(in_min_interval): response = self.client.get(root_url, {}, format='json') - reload(auth) # necessary to reload settings in core code + reload_module(auth) # necessary to reload settings in core code self.assertEqual(response.status_code, 200) self.assertEqual(original_expiry, AuthToken.objects.get().expiry) @@ -326,10 +318,7 @@ def handler(sender, username, **kwargs): token_expired.connect(handler) - instance, token = AuthToken.objects.create( - user=self.user, - expiry=timedelta(seconds=-1), - ) + instance, token = AuthToken.objects.create(user=self.user, expiry=timedelta(seconds=-1)) self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) self.client.post(root_url, {}, format='json') @@ -338,7 +327,7 @@ def handler(sender, username, **kwargs): def test_exceed_token_amount_per_user(self): with override_settings(REST_KNOX=token_user_limit_knox): - reload(views) + reload_module(views) for _ in range(10): AuthToken.objects.create(user=self.user) url = reverse('knox_login') @@ -346,7 +335,7 @@ def test_exceed_token_amount_per_user(self): HTTP_AUTHORIZATION=get_basic_auth_header(self.username, self.password) ) response = self.client.post(url, {}, format='json') - reload(views) + reload_module(views) self.assertEqual(response.status_code, 403) self.assertEqual(response.data, {"error": "Maximum amount of tokens allowed per user exceeded."}) @@ -354,7 +343,7 @@ def test_exceed_token_amount_per_user(self): def test_does_not_exceed_on_expired_keys(self): with override_settings(REST_KNOX=token_user_limit_knox): - reload(views) + reload_module(views) for _ in range(9): AuthToken.objects.create(user=self.user) AuthToken.objects.create(user=self.user, expiry=timedelta(seconds=-1)) @@ -365,7 +354,7 @@ def test_does_not_exceed_on_expired_keys(self): ) response = self.client.post(url, {}, format='json') failed_response = self.client.post(url, {}, format='json') - reload(views) + reload_module(views) self.assertEqual(response.status_code, 200) self.assertIn('token', response.data) self.assertEqual(failed_response.status_code, 403) @@ -375,7 +364,7 @@ def test_does_not_exceed_on_expired_keys(self): def test_invalid_prefix_return_401(self): with override_settings(REST_KNOX=auth_header_prefix_knox): - reload(auth) + reload_module(auth) instance, token = AuthToken.objects.create(user=self.user) self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) failed_response = self.client.get(root_url) @@ -385,13 +374,13 @@ def test_invalid_prefix_return_401(self): ) ) response = self.client.get(root_url) - reload(auth) + reload_module(auth) self.assertEqual(failed_response.status_code, 401) self.assertEqual(response.status_code, 200) def test_expiry_present_also_when_none(self): with override_settings(REST_KNOX=token_no_expiration_knox): - reload(views) + reload_module(views) self.assertEqual(AuthToken.objects.count(), 0) url = reverse('knox_login') self.client.credentials( @@ -410,7 +399,7 @@ def test_expiry_present_also_when_none(self): response.data['expiry'], None ) - reload(views) + reload_module(views) def test_expiry_is_present(self): self.assertEqual(AuthToken.objects.count(), 0) @@ -430,69 +419,3 @@ def test_expiry_is_present(self): response.data['expiry'], DateTimeField().to_representation(AuthToken.objects.first().expiry) ) - - def test_login_returns_serialized_token_with_prefix_when_prefix_set(self): - with override_settings(REST_KNOX=token_prefix_knox): - reload(views) - reload(crypto) - self.assertEqual(AuthToken.objects.count(), 0) - url = reverse('knox_login') - self.client.credentials( - HTTP_AUTHORIZATION=get_basic_auth_header(self.username, self.password) - ) - response = self.client.post( - url, - {}, - format='json' - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(response.data['token'].startswith(token_prefix)) - reload(views) - reload(crypto) - - def test_token_with_prefix_returns_200(self): - with override_settings(REST_KNOX=token_prefix_knox): - reload(views) - self.assertEqual(AuthToken.objects.count(), 0) - url = reverse('knox_login') - self.client.credentials( - HTTP_AUTHORIZATION=get_basic_auth_header(self.username, self.password) - ) - response = self.client.post( - url, - {}, - format='json' - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(response.data['token'].startswith(token_prefix)) - self.client.credentials( - HTTP_AUTHORIZATION=('Token %s' % response.data['token']) - ) - response = self.client.get(root_url, {}, format='json') - self.assertEqual(response.status_code, 200) - reload(views) - - def test_prefix_set_longer_than_max_length_raises_valueerror(self): - with self.assertRaises(ValueError): - with override_settings(REST_KNOX=token_prefix_too_long_knox): - pass - - def test_tokens_created_before_prefix_still_work(self): - self.client.credentials( - HTTP_AUTHORIZATION=get_basic_auth_header(self.username, self.password) - ) - url = reverse('knox_login') - response = self.client.post( - url, - {}, - format='json' - ) - self.assertFalse(response.data['token'].startswith(token_prefix)) - with override_settings(REST_KNOX=token_prefix_knox): - reload(views) - self.client.credentials( - HTTP_AUTHORIZATION=('Token %s' % response.data['token']) - ) - response = self.client.get(root_url, {}, format='json') - self.assertEqual(response.status_code, 200) - reload(views) diff --git a/tox.ini b/tox.ini index 3a159a54..c6610d86 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,7 @@ envlist = isort, flake8, py{36,37,38,39,310}-django32, - py{38,39,310,311,312}-django42, - py{310,311,312}-django50, + py{38,39,310}-django40, [testenv:flake8] deps = flake8 @@ -23,32 +22,28 @@ commands = isort --check-only --diff \ [testenv] commands = python manage.py migrate - coverage run manage.py test - coverage report + python manage.py test setenv = DJANGO_SETTINGS_MODULE = knox_project.settings PIP_INDEX_URL = https://pypi.python.org/simple/ deps = django32: Django>=3.2,<3.3 - django42: Django>=4.2,<4.3 - django50: Django>=5.0,<5.1 - markdown>=3.0 + django40: Django>=4.0,<4.1 + markdown<3.0 isort>=5.0 djangorestframework freezegun mkdocs + cryptography pytest-django setuptools twine wheel - coverage [gh-actions] python = 3.6: py36 3.7: py37 3.8: py38 - 3.9: py39 + 3.9: py39, isort, flake8 3.10: py310 - 3.11: py311, isort, flake8 - 3.12: py312