Skip to content

Commit

Permalink
Merge branch 'develop' into test_old_tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
jmsmkn authored Oct 28, 2024
2 parents 7034480 + 42ea374 commit 2fb0868
Show file tree
Hide file tree
Showing 15 changed files with 175 additions and 52 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.20.0
rev: 1.22.1
hooks:
- id: django-upgrade
args: [--target-version, "4.2"]
Expand All @@ -10,6 +10,6 @@ repos:
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 7.1.0
rev: 7.1.1
hooks:
- id: flake8
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
django-rest-knox
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## 5.0.2
## 5.1.0
- Removed `token_key` which was no longer necessary after removal of salt in 4.2.0
- Fix old tokens not being accepted in 5.0.0 and 5.0.1
- Fix old tokens not being accepted in 5.0.0, 5.0.1 and 5.0.2

## 5.0.2
- Implement AUTO_REFRESH_MAX_TTL to limit total token lifetime when AUTO_REFRESH = True

## 5.0.1
- Fix migration: retrieve `TOKEN_MODEL` from `knox_settings` instead of Django settings.
Expand Down
6 changes: 6 additions & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ REST_KNOX = {
'USER_SERIALIZER': 'knox.serializers.UserSerializer',
'TOKEN_LIMIT_PER_USER': None,
'AUTO_REFRESH': False,
'AUTO_REFRESH_MAX_TTL': None,
'MIN_REFRESH_INTERVAL': 60,
'AUTH_HEADER_PREFIX': 'Token',
'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT,
Expand Down Expand Up @@ -78,6 +79,11 @@ successfully returning from `LoginView`. The default is `knox.serializers.UserSe
This defines if the token expiry time is extended by TOKEN_TTL each time the token
is used.

## AUTO_REFRESH_MAX_TTL
When automatically extending token expiry time, limit the total token lifetime. If
AUTO_REFRESH_MAX_TTL is set, then the token lifetime since the original creation date cannot
exceed AUTO_REFRESH_MAX_TTL.

## MIN_REFRESH_INTERVAL
This is the minimum time in seconds that needs to pass for the token expiry to be updated
in the database.
Expand Down
12 changes: 12 additions & 0 deletions knox/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import binascii
import logging

from django.utils import timezone
from django.utils.translation import gettext_lazy as _
Expand All @@ -12,6 +13,8 @@
from knox.settings import knox_settings
from knox.signals import token_expired

logger = logging.getLogger(__name__)


class TokenAuthentication(BaseAuthentication):
'''
Expand Down Expand Up @@ -74,7 +77,16 @@ def authenticate_credentials(self, token):
def renew_token(self, auth_token) -> None:
current_expiry = auth_token.expiry
new_expiry = timezone.now() + knox_settings.TOKEN_TTL

# Do not auto-renew tokens past AUTO_REFRESH_MAX_TTL.
if knox_settings.AUTO_REFRESH_MAX_TTL is not None:
max_expiry = auth_token.created + knox_settings.AUTO_REFRESH_MAX_TTL
if new_expiry > max_expiry:
new_expiry = max_expiry
logger.info('Token renewal truncated due to AUTO_REFRESH_MAX_TTL.')

auth_token.expiry = new_expiry

# Throttle refreshing of token to avoid db writes
delta = (new_expiry - current_expiry).total_seconds()
if delta > knox_settings.MIN_REFRESH_INTERVAL:
Expand Down
1 change: 1 addition & 0 deletions knox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'USER_SERIALIZER': None,
'TOKEN_LIMIT_PER_USER': None,
'AUTO_REFRESH': False,
'AUTO_REFRESH_MAX_TTL': None,
'MIN_REFRESH_INTERVAL': 60,
'AUTH_HEADER_PREFIX': 'Token',
'EXPIRY_DATETIME_FORMAT': api_settings.DATETIME_FORMAT,
Expand Down
5 changes: 4 additions & 1 deletion knox/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.db.models import Q
from django.utils import timezone
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
Expand Down Expand Up @@ -66,7 +67,9 @@ def post(self, request, format=None):
token_limit_per_user = self.get_token_limit_per_user()
if token_limit_per_user is not None:
now = timezone.now()
token = request.user.auth_token_set.filter(expiry__gt=now)
token = request.user.auth_token_set.filter(
Q(expiry__gt=now) | Q(expiry__isnull=True)
)
if token.count() >= token_limit_per_user:
return Response(
{"error": "Maximum amount of tokens allowed per user exceeded."},
Expand Down
16 changes: 16 additions & 0 deletions knox_project/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for project project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "knox_project.settings")

application = get_asgi_application()
103 changes: 65 additions & 38 deletions knox_project/settings.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,83 @@
import os
"""
Test project for Django REST Knox
"""

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY = 'gcr$j^h2@d@sd(f#-ihtv6*hg7qno$otw62^*rzcf0tk2wz&sb'
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "i-am-a-super-secret-key"
DEBUG = True
ALLOWED_HOSTS = []
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'rest_framework',
'knox',
)

MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.security.SecurityMiddleware',
)

ROOT_URLCONF = 'knox_project.urls'
ALLOWED_HOSTS = ["*"]

# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"knox",
]

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "knox_project.urls"

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]

WSGI_APPLICATION = 'knox_project.wsgi.application'

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "db.sqlite3",
}
}

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
WSGI_APPLICATION = "knox_project.wsgi.application"

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True

STATIC_URL = '/static/'
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATIC_URL = "static/"

KNOX_TOKEN_MODEL = 'knox.AuthToken'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# Django REST Knox settings
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
"knox.auth.TokenAuthentication",
]
}
6 changes: 5 additions & 1 deletion knox_project/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

from .views import RootView

urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('knox.urls')),
path('api/', RootView.as_view(), name="api-root"),
]
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
5 changes: 4 additions & 1 deletion knox_project/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@


class RootView(APIView):
"""
API Root View to test authentication.
"""
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)

def get(self, request):
return Response("api root")
return Response("User is authenticated.")
4 changes: 2 additions & 2 deletions knox_project/wsgi.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
WSGI config for knox_project project.
WSGI config for project project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""

import os
Expand Down
18 changes: 15 additions & 3 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

if __name__ == "__main__":

def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "knox_project.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)

from django.core.management import execute_from_command_line

execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# Versions should comply with PEP440. For a discussion on single-sourcing
# the version across setup.py and the project code, see
# https://packaging.python.org/en/latest/single_source_version.html
version='5.0.1',
version='5.0.2',
description='Authentication for django rest framework',
long_description=long_description,
long_description_content_type='text/markdown',
Expand Down
37 changes: 36 additions & 1 deletion tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def get_basic_auth_header(username, password):
auto_refresh_knox = knox_settings.defaults.copy()
auto_refresh_knox["AUTO_REFRESH"] = True

auto_refresh_max_ttl_knox = auto_refresh_knox.copy()
auto_refresh_max_ttl_knox["AUTO_REFRESH_MAX_TTL"] = timedelta(hours=12)

token_user_limit_knox = knox_settings.defaults.copy()
token_user_limit_knox["TOKEN_LIMIT_PER_USER"] = 10

Expand Down Expand Up @@ -318,6 +321,36 @@ def test_token_expiry_is_not_extended_within_MIN_REFRESH_INTERVAL(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(original_expiry, AuthToken.objects.get().expiry)

def test_token_expiry_is_not_extended_past_max_ttl(self):
ttl = knox_settings.TOKEN_TTL
self.assertEqual(ttl, timedelta(hours=10))
original_time = datetime(2018, 7, 25, 0, 0, 0, 0)

with freeze_time(original_time):
instance, token = AuthToken.objects.create(user=self.user)

self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token))
five_hours_later = original_time + timedelta(hours=5)
with override_settings(REST_KNOX=auto_refresh_max_ttl_knox):
reload(auth) # necessary to reload settings in core code
self.assertEqual(auth.knox_settings.AUTO_REFRESH, True)
self.assertEqual(auth.knox_settings.AUTO_REFRESH_MAX_TTL, timedelta(hours=12))
with freeze_time(five_hours_later):
response = self.client.get(root_url, {}, format='json')
reload(auth) # necessary to reload settings in core code
self.assertEqual(response.status_code, 200)

# original expiry date was extended, but not past max_ttl:
new_expiry = AuthToken.objects.get().expiry
expected_expiry = original_time + timedelta(hours=12)
self.assertEqual(new_expiry.replace(tzinfo=None), expected_expiry,
"Expiry time should have been extended to {} but is {}."
.format(expected_expiry, new_expiry))

with freeze_time(expected_expiry + timedelta(seconds=1)):
response = self.client.get(root_url, {}, format='json')
self.assertEqual(response.status_code, 401)

def test_expiry_signals(self):
self.signal_was_called = False

Expand All @@ -339,8 +372,10 @@ def test_exceed_token_amount_per_user(self):

with override_settings(REST_KNOX=token_user_limit_knox):
reload(views)
for _ in range(10):
for _ in range(5):
AuthToken.objects.create(user=self.user)
for _ in range(5):
AuthToken.objects.create(user=self.user, expiry=None)
url = reverse('knox_login')
self.client.credentials(
HTTP_AUTHORIZATION=get_basic_auth_header(self.username, self.password)
Expand Down

0 comments on commit 2fb0868

Please sign in to comment.