From 729982d9671d73357ab7820f032b34f047093087 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Wed, 23 Sep 2026 14:23:32 -0500 Subject: [PATCH] Apply the API token settings to API v3 token auth API v3 token authentication now follows the same token settings as API v2: the instance-wide API token switch and the effective token expiry. Installs that keep both at their defaults see no change. Adds regression tests. --- dojo/api_v3/auth.py | 7 +++++-- unittests/api_v3/test_apiv3_auth.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/dojo/api_v3/auth.py b/dojo/api_v3/auth.py index 00d6425667b..869ee480d53 100644 --- a/dojo/api_v3/auth.py +++ b/dojo/api_v3/auth.py @@ -17,9 +17,12 @@ from typing import TYPE_CHECKING +from django.conf import settings from ninja.security import APIKeyHeader from rest_framework.authtoken.models import Token +from dojo.user.authentication import token_is_expired + if TYPE_CHECKING: from django.contrib.auth.models import AbstractBaseUser from django.http import HttpRequest @@ -35,7 +38,7 @@ class TokenAuth(APIKeyHeader): param_name = "Authorization" def authenticate(self, request: HttpRequest, key: str | None) -> AbstractBaseUser | None: - if not key: + if not key or not settings.API_TOKENS_ENABLED: return None parts = key.split() # Only handle the `Token ` scheme; anything else is left for session auth to try. @@ -46,7 +49,7 @@ def authenticate(self, request: HttpRequest, key: str | None) -> AbstractBaseUse except Token.DoesNotExist: return None user = token.user - if not user.is_active: + if not user.is_active or token_is_expired(token): return None # Set request.user so downstream authorized-queryset helpers (I8) resolve the right user # exactly as they do for session auth. diff --git a/unittests/api_v3/test_apiv3_auth.py b/unittests/api_v3/test_apiv3_auth.py index 7420c7dd354..5b0990f338f 100644 --- a/unittests/api_v3/test_apiv3_auth.py +++ b/unittests/api_v3/test_apiv3_auth.py @@ -1,6 +1,14 @@ """Auth contract tests for API v3 (D8 / ยง4.2): token AND session+CSRF, both on the same endpoint.""" from __future__ import annotations +from datetime import timedelta + +from django.test import override_settings +from django.utils import timezone +from rest_framework.authtoken.models import Token + +from dojo.models import UserContactInfo + from .base import ApiV3TestCase @@ -47,3 +55,24 @@ def test_session_csrf_enforced_on_unsafe_method(self): self.v3_url("import"), {"scan_type": "ZAP Scan", "mode": "import"}, format="multipart", ) self.assertEqual(403, response.status_code, response.content[:500]) + + def test_expired_token_is_401(self): + """A token past its per-user expiry is refused, as on v2.""" + uci, _ = UserContactInfo.objects.get_or_create(user=self.admin) + uci.token_expiry = timezone.now() - timedelta(days=1) + uci.save(update_fields=["token_expiry"]) + response = self.token_client().get(self.v3_url("findings")) + self.assertEqual(401, response.status_code, response.content[:500]) + + @override_settings(API_TOKEN_DEFAULT_EXPIRY_DAYS=1) + def test_token_older_than_default_expiry_is_401(self): + """A token older than the instance-wide lifetime is refused, as on v2.""" + Token.objects.filter(pk=self.token.pk).update(created=timezone.now() - timedelta(days=2)) + response = self.token_client().get(self.v3_url("findings")) + self.assertEqual(401, response.status_code, response.content[:500]) + + @override_settings(API_TOKENS_ENABLED=False) + def test_token_is_401_when_api_tokens_disabled(self): + """With API tokens disabled, v3 refuses tokens as v2 does; session auth still works.""" + self.assertEqual(401, self.token_client().get(self.v3_url("findings")).status_code) + self.assertEqual(200, self.session_client().get(self.v3_url("findings")).status_code)