Skip to content

Commit

Permalink
Vendor django rest knox (#3706)
Browse files Browse the repository at this point in the history
This vendors Django Rest Knox from v4.2.0, and removes many settings and
features that we do not use. The implementation is now closer to Django
Rest Frameworks `TokenAuthentication`, but with multiple tokens per
user, the token digests being stored, and expiry dates added.

Closes
DIAGNijmegen/rse-grand-challenge-admin#387
  • Loading branch information
jmsmkn authored Nov 19, 2024
1 parent 0b8dbac commit 0c8f20b
Show file tree
Hide file tree
Showing 28 changed files with 626 additions and 80 deletions.
2 changes: 0 additions & 2 deletions app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -944,8 +944,6 @@ def sentry_before_send(event, hint):
"COMPONENT_SPLIT_REQUEST": True,
}

REST_KNOX = {"AUTH_HEADER_PREFIX": "Bearer"}

###############################################################################
#
# CORS
Expand Down
12 changes: 1 addition & 11 deletions app/grandchallenge/api/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from drf_spectacular.extensions import OpenApiAuthenticationExtension
from knox.settings import knox_settings


class KnoxTokenScheme(OpenApiAuthenticationExtension):
Expand All @@ -9,13 +8,4 @@ class KnoxTokenScheme(OpenApiAuthenticationExtension):
priority = -1

def get_security_definition(self, auto_schema):
prefix = knox_settings.AUTH_HEADER_PREFIX
if prefix == "Bearer":
return {"type": "http", "scheme": "bearer"}
else:
return {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": f"Token-based authentication with required prefix {prefix!r}",
}
return {"type": "http", "scheme": "bearer"}
21 changes: 21 additions & 0 deletions app/knox/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2015 James McMahon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
108 changes: 108 additions & 0 deletions app/knox/METADATA
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
Metadata-Version: 2.1
Name: django-rest-knox
Version: 4.2.0
Summary: Authentication for django rest framework
Home-page: https://github.com/James1345/django-rest-knox
Author: James McMahon
Author-email: james1345@googlemail.com
License: MIT
Keywords: django rest authentication login
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Requires-Python: >=3.6
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography
Requires-Dist: django (>=3.2)
Requires-Dist: djangorestframework
Provides-Extra: dev
Provides-Extra: test

django-rest-knox
================

[![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

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 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 *all* devices to be logged out if a server-side
logout is required (i.e. the token is deleted).

Knox provides one token per call to the login view - allowing each
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*
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
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.

- DRF tokens track their creation time, but have no inbuilt mechanism
for tokens expiring. Knox tokens can have an expiry configured in
the app settings (default is 10 hours.)

More information can be found in the
[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:

simply run the ``./docker-run-tests.sh`` script and it will run the test suite in every Python /
Django versions.

You could also simply run regular ``tox`` in the root folder as well, but that would make testing the matrix of
Python / Django versions a bit more tricky.

# Work on the documentation

Our documentation is generated by [Mkdocs](https://www.mkdocs.org).

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.

Running the script without any params triggers the `serve` command.
The server is exposed on localhost on port 8000.

To configure the port the `serve` command will be exposing the server to, you
can use the following env var:

```
MKDOCS_DEV_PORT="8080"
```

You can also pass any `mkdocs` command like this:

```
./mkdocs build
./mkdocs --help
```

Check the [Mkdocs documentation](https://www.mkdocs.org/) for more.
Empty file added app/knox/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions app/knox/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.contrib import admin
from knox import models


@admin.register(models.AuthToken)
class AuthTokenAdmin(admin.ModelAdmin):
list_display = (
"key",
"user",
"created",
"expiry",
)
fields = ()
raw_id_fields = ("user",)
28 changes: 28 additions & 0 deletions app/knox/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import binascii

from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from knox.crypto import hash_token
from knox.models import AuthToken
from rest_framework import exceptions
from rest_framework.authentication import (
TokenAuthentication as BaseTokenAuthentication,
)


class TokenAuthentication(BaseTokenAuthentication):
model = AuthToken
keyword = "Bearer"

def authenticate_credentials(self, key):
try:
digest = hash_token(key)
except (TypeError, binascii.Error):
raise exceptions.AuthenticationFailed(_("Invalid token."))

user, token = super().authenticate_credentials(key=digest)

if token.expiry is not None and token.expiry < timezone.now():
raise exceptions.AuthenticationFailed(_("Invalid token."))

return (user, token)
21 changes: 21 additions & 0 deletions app/knox/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import binascii
import hashlib
from os import urandom as generate_bytes


def create_token_string():
auth_token_character_length = 64
return binascii.hexlify(
generate_bytes(int(auth_token_character_length / 2))
).decode()


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 = hashlib.sha512()
digest.update(binascii.unhexlify(token))
return digest.hexdigest()
32 changes: 32 additions & 0 deletions app/knox/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="AuthToken",
fields=[
(
"key",
models.CharField(
max_length=64, serialize=False, primary_key=True
),
),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
to=settings.AUTH_USER_MODEL,
related_name="auth_token_set",
on_delete=models.CASCADE,
),
),
],
),
]
39 changes: 39 additions & 0 deletions app/knox/migrations/0002_auto_20150916_1425.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("knox", "0001_initial"),
]

operations = [
migrations.DeleteModel("AuthToken"),
migrations.CreateModel(
name="AuthToken",
fields=[
(
"digest",
models.CharField(
max_length=64, serialize=False, primary_key=True
),
),
(
"salt",
models.CharField(
max_length=16, serialize=False, unique=True
),
),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
to=settings.AUTH_USER_MODEL,
related_name="auth_token_set",
on_delete=models.CASCADE,
),
),
],
),
]
23 changes: 23 additions & 0 deletions app/knox/migrations/0003_auto_20150916_1526.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("knox", "0002_auto_20150916_1425"),
]

operations = [
migrations.AlterField(
model_name="authtoken",
name="digest",
field=models.CharField(
primary_key=True, serialize=False, max_length=128
),
),
migrations.AlterField(
model_name="authtoken",
name="salt",
field=models.CharField(unique=True, max_length=16),
),
]
16 changes: 16 additions & 0 deletions app/knox/migrations/0004_authtoken_expires.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("knox", "0003_auto_20150916_1526"),
]

operations = [
migrations.AddField(
model_name="authtoken",
name="expires",
field=models.DateTimeField(null=True, blank=True),
),
]
20 changes: 20 additions & 0 deletions app/knox/migrations/0005_authtoken_token_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 1.10 on 2016-08-18 09:23

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("knox", "0004_authtoken_expires"),
]

operations = [
migrations.AddField(
model_name="authtoken",
name="token_key",
field=models.CharField(
blank=True, db_index=True, max_length=8, null=True
),
),
]
25 changes: 25 additions & 0 deletions app/knox/migrations/0006_auto_20160818_0932.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 1.10 on 2016-08-18 09:32

from django.db import migrations, models


def cleanup_tokens(apps, schema_editor):
AuthToken = apps.get_model("knox", "AuthToken") # noqa: N806
AuthToken.objects.filter(token_key__isnull=True).delete()


class Migration(migrations.Migration):

dependencies = [
("knox", "0005_authtoken_token_key"),
]

operations = [
migrations.RunPython(cleanup_tokens),
migrations.AlterField(
model_name="authtoken",
name="token_key",
field=models.CharField(db_index=True, default="", max_length=8),
preserve_default=False,
),
]
Loading

0 comments on commit 0c8f20b

Please sign in to comment.