Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions dojo/api_v3/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <key>` scheme; anything else is left for session auth to try.
Expand All @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions unittests/api_v3/test_apiv3_auth.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
Loading