From b75c2bd61f4a7c37412fe831598fd7d1c924fb31 Mon Sep 17 00:00:00 2001
From: James Meakin <12661555+jmsmkn@users.noreply.github.com>
Date: Fri, 2 Aug 2024 10:27:45 +0200
Subject: [PATCH] Ensure old tokens continue to work

---
 .gitignore                                    |  1 +
 knox/auth.py                                  | 17 +++++++++---
 .../0010_alter_authtoken_token_key.py         | 18 +++++++++++++
 knox/models.py                                |  5 +---
 tests/tests.py                                | 27 +++++++++++++++++++
 tox.ini                                       |  2 +-
 6 files changed, 61 insertions(+), 9 deletions(-)
 create mode 100644 knox/migrations/0010_alter_authtoken_token_key.py

diff --git a/.gitignore b/.gitignore
index 7791a82c..eeecd427 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ __pycache__/
 # Distribution / packaging
 .Python
 env/
+.venv/
 build/
 develop-eggs/
 dist/
diff --git a/knox/auth.py b/knox/auth.py
index 858ee504..39815f6d 100644
--- a/knox/auth.py
+++ b/knox/auth.py
@@ -56,15 +56,24 @@ def authenticate_credentials(self, token):
         '''
         msg = _('Invalid token.')
         token = token.decode("utf-8")
+
+        try:
+            digest = hash_token(token)
+        except (TypeError, binascii.Error):
+            raise exceptions.AuthenticationFailed(msg)
+
+        for auth_token in get_token_model().objects.filter(token_key=token[:8]):
+            # Migrate tokens that were created prior to 3a1bc58
+            # TODO: This will have terrible performance if TOKEN_PREFIX is used
+            if compare_digest(digest, auth_token.digest):
+                auth_token.token_key = token[:CONSTANTS.TOKEN_KEY_LENGTH]
+                auth_token.save()
+
         for auth_token in get_token_model().objects.filter(
                 token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH]):
             if self._cleanup_token(auth_token):
                 continue
 
-            try:
-                digest = hash_token(token)
-            except (TypeError, binascii.Error):
-                raise exceptions.AuthenticationFailed(msg)
             if compare_digest(digest, auth_token.digest):
                 if knox_settings.AUTO_REFRESH and auth_token.expiry:
                     self.renew_token(auth_token)
diff --git a/knox/migrations/0010_alter_authtoken_token_key.py b/knox/migrations/0010_alter_authtoken_token_key.py
new file mode 100644
index 00000000..c07b5ad8
--- /dev/null
+++ b/knox/migrations/0010_alter_authtoken_token_key.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.7 on 2024-08-02 08:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('knox', '0009_extend_authtoken_field'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='authtoken',
+            name='token_key',
+            field=models.CharField(db_index=True, max_length=15),
+        ),
+    ]
diff --git a/knox/models.py b/knox/models.py
index 8b0a179d..a9b24eae 100644
--- a/knox/models.py
+++ b/knox/models.py
@@ -7,8 +7,6 @@
 from knox import crypto
 from knox.settings import CONSTANTS, knox_settings
 
-sha = knox_settings.SECURE_HASH_ALGORITHM
-
 User = settings.AUTH_USER_MODEL
 
 
@@ -37,8 +35,7 @@ class AbstractAuthToken(models.Model):
     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,
+        max_length=CONSTANTS.TOKEN_KEY_LENGTH,
         db_index=True
     )
     user = models.ForeignKey(User, null=False, blank=False,
diff --git a/tests/tests.py b/tests/tests.py
index 9494db0b..ab744595 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -496,3 +496,30 @@ def test_tokens_created_before_prefix_still_work(self):
             response = self.client.get(root_url, {}, format='json')
             self.assertEqual(response.status_code, 200)
         reload(views)
+
+    def test_old_tokens_still_work(self):
+        self.assertEqual(AuthToken.objects.count(), 0)
+
+        old_token = "02d233c901e7bd38df1dbc486b7e22c5c81b089c40cbb31d35d7b032615f5778"
+        # Hash generated using crypto.hash_token on 4.2.0 with
+        # SECURE_HASH_ALGORITHM = 'cryptography.hazmat.primitives.hashes.SHA512'
+        old_hash = (
+            "c7f9f2904decf77e0fa0341bc3eb96daa1437649825f4bfdd38cdad64d69c4be55938d71f17"
+            "34131c656f9bbbfc5d991bef295accd268921b23d9cdd0d9d60d0"
+        )
+
+        AuthToken(
+            token_key=old_token[: 8],  # 8 was the key length prior to 3a1bc58
+            digest=old_hash,
+            user=self.user,
+        ).save()
+
+        rf = APIRequestFactory()
+        request = rf.get('/')
+        request.META = {'HTTP_AUTHORIZATION': f'Token {old_token}'}
+        user, auth_token = TokenAuthentication().authenticate(request)
+        self.assertEqual(self.user, user)
+        self.assertEqual(
+            old_token[:CONSTANTS.TOKEN_KEY_LENGTH],
+            auth_token.token_key,
+        )
diff --git a/tox.ini b/tox.ini
index 5b17cb80..c49f2e81 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,7 +6,7 @@ envlist =
 [testenv]
 commands =
     python manage.py migrate
-    coverage run manage.py test
+    coverage run manage.py test {posargs}
     coverage report
 setenv =
     DJANGO_SETTINGS_MODULE = knox_project.settings