From fca7793359112a35593a75b847e92ab760c55247 Mon Sep 17 00:00:00 2001 From: kunalsz Date: Mon, 10 Mar 2025 05:26:13 -0400 Subject: [PATCH 1/2] Added Expiry for API Tokens Signed-off-by: kunalsz --- .../migrations/0089_expiringtoken.py | 33 +++++++ vulnerabilities/models.py | 24 ++++- .../tests/test_token_authentication.py | 96 +++++++++++++++++++ vulnerabilities/token_authentication.py | 21 ++++ vulnerablecode/settings.py | 7 +- 5 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 vulnerabilities/migrations/0089_expiringtoken.py create mode 100644 vulnerabilities/tests/test_token_authentication.py create mode 100644 vulnerabilities/token_authentication.py diff --git a/vulnerabilities/migrations/0089_expiringtoken.py b/vulnerabilities/migrations/0089_expiringtoken.py new file mode 100644 index 000000000..c5bc5a10e --- /dev/null +++ b/vulnerabilities/migrations/0089_expiringtoken.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.17 on 2025-03-09 19:00 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("authtoken", "0004_alter_tokenproxy_options"), + ("vulnerabilities", "0088_fix_alpine_purl_type"), + ] + + operations = [ + migrations.CreateModel( + name="ExpiringToken", + fields=[ + ( + "token_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="authtoken.token", + ), + ), + ("expires", models.DateTimeField()), + ], + bases=("authtoken.token",), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 9b6df7c13..ed1242161 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -13,6 +13,7 @@ import logging import xml.etree.ElementTree as ET from contextlib import suppress +from datetime import timedelta from functools import cached_property from itertools import groupby from operator import attrgetter @@ -56,6 +57,7 @@ from vulnerabilities.utils import normalize_purl from vulnerabilities.utils import purl_to_dict from vulnerablecode import __version__ as VULNERABLECODE_VERSION +from vulnerablecode import settings logger = logging.getLogger(__name__) @@ -1390,6 +1392,26 @@ def to_advisory_data(self) -> "AdvisoryData": UserModel = get_user_model() +class ExpiringToken(Token): + """ + Extends Django Rest Framework's Token model to include an expiration date. + """ + + expires = models.DateTimeField(null=False) + + def is_expired(self): + return timezone.now() > self.expires + + @classmethod + def get_or_create(cls, user): + token, created = Token._default_manager.get_or_create(user=user) + + if created: + token.expires = timezone.now() + timedelta(days=settings.TOKEN_EXPIRY_DAYS) # 30days + token.save() + return token, created + + class ApiUserManager(UserManager): def create_api_user(self, username, first_name="", last_name="", **extra_fields): """ @@ -1415,7 +1437,7 @@ def create_api_user(self, username, first_name="", last_name="", **extra_fields) user.set_unusable_password() user.save() - Token._default_manager.get_or_create(user=user) + ExpiringToken.get_or_create(user=user) return user diff --git a/vulnerabilities/tests/test_token_authentication.py b/vulnerabilities/tests/test_token_authentication.py new file mode 100644 index 000000000..2201eb32b --- /dev/null +++ b/vulnerabilities/tests/test_token_authentication.py @@ -0,0 +1,96 @@ +# tests/test_models.py +from datetime import timedelta + +import pytest +from django.contrib.auth import get_user_model +from django.utils import timezone +from rest_framework import exceptions + +from vulnerabilities.models import ExpiringToken +from vulnerabilities.token_authentication import ExpiringTokenAuthentication + +User = get_user_model() + + +@pytest.mark.django_db +def test_expiring_token_creation(): + """ + Test that an ExpiringToken is created with an expiration date. + """ + user = User.objects.create_user(username="testuser", email="test@example.com") + token, created = ExpiringToken.get_or_create(user=user) + + # Debugging: Print the token and its expiration date 30days from today + print(f"Token: {token.key}, Expires: {token.expires}") + + assert created is True + assert token.expires > timezone.now() + + +@pytest.mark.django_db +def test_expiring_token_is_expired(): + """ + Test that the is_expired method works correctly. + """ + user = User.objects.create_user(username="testuser", email="test@example.com") + token = ExpiringToken.objects.create(user=user, expires=timezone.now() - timedelta(days=1)) + # Debugging: Print the token and its expiration date,yesterday + # print(f"Token: {token.key}, Expires: {token.expires}") + + assert token.is_expired() is True + + +@pytest.mark.django_db +def test_expiring_token_is_not_expired(): + """ + Test that the is_expired method returns False for a non-expired token. + """ + user = User.objects.create_user(username="testuser", email="test@example.com") + token = ExpiringToken.objects.create(user=user, expires=timezone.now() + timedelta(days=1)) + + # Debugging: Print the token and its expiration date,tomorrow + # print(f"Token: {token.key}, Expires: {token.expires}") + + assert token.is_expired() is False + + +@pytest.mark.django_db +def test_expiring_token_authentication_valid(): + """ + Test that a valid token authenticates the user. + """ + user = User.objects.create_user(username="testuser", email="test@example.com") + token = ExpiringToken.objects.create(user=user, expires=timezone.now() + timedelta(days=1)) + + auth = ExpiringTokenAuthentication() + authenticated_user, authenticated_token = auth.authenticate_credentials(token.key) + # Debugging + # print(f'Authenticated User:{authenticated_user} and Authenticated Token:{authenticated_token}') + + assert authenticated_user == user + assert authenticated_token == token + + +@pytest.mark.django_db +def test_expiring_token_authentication_expired(): + """ + Test that an expired token raises an AuthenticationFailed error. + """ + user = User.objects.create_user(username="testuser", email="test@example.com") + token = ExpiringToken.objects.create(user=user, expires=timezone.now() - timedelta(days=1)) + + auth = ExpiringTokenAuthentication() + + with pytest.raises(exceptions.AuthenticationFailed): + auth.authenticate_credentials(token.key) + + +@pytest.mark.django_db +def test_expiring_token_authentication_invalid(): + """ + Test that an invalid token raises an AuthenticationFailed error. + """ + auth = ExpiringTokenAuthentication() + + with pytest.raises(exceptions.AuthenticationFailed): + auth.authenticate_credentials("invalid-token") diff --git a/vulnerabilities/token_authentication.py b/vulnerabilities/token_authentication.py new file mode 100644 index 000000000..52a1b7aad --- /dev/null +++ b/vulnerabilities/token_authentication.py @@ -0,0 +1,21 @@ +from rest_framework import exceptions +from rest_framework.authentication import TokenAuthentication + +from .models import ExpiringToken + + +class ExpiringTokenAuthentication(TokenAuthentication): + model = ExpiringToken + + def authenticate_credentials(self, key): + try: + # Try to fetch the token from the database + user, token = super().authenticate_credentials(key) + except self.model.DoesNotExist: + # If the token does not exist, raise an AuthenticationFailed exception + raise exceptions.AuthenticationFailed("Invalid token") + + if token.is_expired(): + # If the token has expired raise exception + raise exceptions.AuthenticationFailed("Token has expired") + return user, token diff --git a/vulnerablecode/settings.py b/vulnerablecode/settings.py index 0e545e0f2..72262de59 100644 --- a/vulnerablecode/settings.py +++ b/vulnerablecode/settings.py @@ -206,7 +206,9 @@ # Django restframework REST_FRAMEWORK = { - "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.TokenAuthentication",), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "vulnerabilities.token_authentication.ExpiringTokenAuthentication", + ), "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_RENDERER_CLASSES": ( "rest_framework.renderers.JSONRenderer", @@ -355,3 +357,6 @@ "handlers": ["console"], "level": "ERROR", } + +# Set the number of days until the API token expires +TOKEN_EXPIRY_DAYS = 30 From d20322a5bd78f53deb4c358558a7272f00fdd954 Mon Sep 17 00:00:00 2001 From: kunalsz Date: Thu, 20 Mar 2025 15:24:42 -0400 Subject: [PATCH 2/2] update Signed-off-by: kunalsz --- .../tests/test_token_authentication.py | 27 +++++++++---------- vulnerabilities/token_authentication.py | 6 ++--- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/vulnerabilities/tests/test_token_authentication.py b/vulnerabilities/tests/test_token_authentication.py index 2201eb32b..d7190e3c0 100644 --- a/vulnerabilities/tests/test_token_authentication.py +++ b/vulnerabilities/tests/test_token_authentication.py @@ -1,4 +1,3 @@ -# tests/test_models.py from datetime import timedelta import pytest @@ -15,13 +14,13 @@ @pytest.mark.django_db def test_expiring_token_creation(): """ - Test that an ExpiringToken is created with an expiration date. + Test tha ExpiringToken is created with an expiration date """ user = User.objects.create_user(username="testuser", email="test@example.com") token, created = ExpiringToken.get_or_create(user=user) - # Debugging: Print the token and its expiration date 30days from today - print(f"Token: {token.key}, Expires: {token.expires}") + #token and its expiration date 30days from today + #print(f"Token: {token.key}, Expires: {token.expires}") assert created is True assert token.expires > timezone.now() @@ -30,41 +29,41 @@ def test_expiring_token_creation(): @pytest.mark.django_db def test_expiring_token_is_expired(): """ - Test that the is_expired method works correctly. + Test that is_expired method """ user = User.objects.create_user(username="testuser", email="test@example.com") token = ExpiringToken.objects.create(user=user, expires=timezone.now() - timedelta(days=1)) - # Debugging: Print the token and its expiration date,yesterday + # token and its expiration date,yesterday # print(f"Token: {token.key}, Expires: {token.expires}") - assert token.is_expired() is True + assert token.is_expired() is True #expired @pytest.mark.django_db def test_expiring_token_is_not_expired(): """ - Test that the is_expired method returns False for a non-expired token. + Test the is_expired method for a non-expired token """ user = User.objects.create_user(username="testuser", email="test@example.com") token = ExpiringToken.objects.create(user=user, expires=timezone.now() + timedelta(days=1)) - # Debugging: Print the token and its expiration date,tomorrow + #token and its expiration date,tomorrow # print(f"Token: {token.key}, Expires: {token.expires}") - assert token.is_expired() is False + assert token.is_expired() is False #not expired @pytest.mark.django_db def test_expiring_token_authentication_valid(): """ - Test that a valid token authenticates the user. + Test that a valid token authenticates the user """ user = User.objects.create_user(username="testuser", email="test@example.com") token = ExpiringToken.objects.create(user=user, expires=timezone.now() + timedelta(days=1)) auth = ExpiringTokenAuthentication() authenticated_user, authenticated_token = auth.authenticate_credentials(token.key) - # Debugging + # print(f'Authenticated User:{authenticated_user} and Authenticated Token:{authenticated_token}') assert authenticated_user == user @@ -74,7 +73,7 @@ def test_expiring_token_authentication_valid(): @pytest.mark.django_db def test_expiring_token_authentication_expired(): """ - Test that an expired token raises an AuthenticationFailed error. + Test that an expired token raises an AuthenticationFailed error """ user = User.objects.create_user(username="testuser", email="test@example.com") token = ExpiringToken.objects.create(user=user, expires=timezone.now() - timedelta(days=1)) @@ -88,7 +87,7 @@ def test_expiring_token_authentication_expired(): @pytest.mark.django_db def test_expiring_token_authentication_invalid(): """ - Test that an invalid token raises an AuthenticationFailed error. + Test that an invalid/non-existin token raises an AuthenticationFailed error """ auth = ExpiringTokenAuthentication() diff --git a/vulnerabilities/token_authentication.py b/vulnerabilities/token_authentication.py index 52a1b7aad..789f64e7b 100644 --- a/vulnerabilities/token_authentication.py +++ b/vulnerabilities/token_authentication.py @@ -9,13 +9,13 @@ class ExpiringTokenAuthentication(TokenAuthentication): def authenticate_credentials(self, key): try: - # Try to fetch the token from the database + #try to fetch the token user, token = super().authenticate_credentials(key) except self.model.DoesNotExist: - # If the token does not exist, raise an AuthenticationFailed exception + #if the token does not exist/invalid raise exceptions.AuthenticationFailed("Invalid token") if token.is_expired(): - # If the token has expired raise exception + #if the token has expired raise exceptions.AuthenticationFailed("Token has expired") return user, token