Files
WYGIWYH/app/apps/api/tests/test_authentication.py
T
obervinov 4273c541c5 Add API tokens and OAuth2 client support for external integrations
- Personal API tokens (model, user-settings UI, admin, management command,
  DRF auth class) for non-interactive API access from automations like n8n.
  Raw token shown once; only a SHA-256 hash is stored; last_used_at writes
  are throttled.
- OAuth2 authorization server via django-oauth-toolkit with authorization
  server metadata and optional, off-by-default Dynamic Client Registration
  (RFC 7591), so remote OAuth/MCP clients can authenticate and self-register.
- Tests for token auth, DCR gating and the management commands, plus
  .env.example and README documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:15:31 +04:00

110 lines
4.2 KiB
Python

from datetime import timedelta
from django.contrib.auth import get_user_model
from django.test import RequestFactory, TestCase, override_settings
from django.utils import timezone
from rest_framework.exceptions import AuthenticationFailed
from apps.api.authentication import APITokenAuthentication
from apps.users.models import APIToken
class APITokenAuthenticationTests(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.authentication = APITokenAuthentication()
self.user = get_user_model().objects.create_user(
email="automation@example.com",
password="test-password",
)
def test_returns_none_without_token_header(self):
request = self.factory.get("/api/accounts/")
self.assertIsNone(self.authentication.authenticate(request))
def test_authenticates_valid_api_token(self):
token, raw_token = APIToken.objects.create_token(user=self.user, name="n8n")
request = self.factory.get(
"/api/accounts/",
HTTP_AUTHORIZATION=f"Token {raw_token}",
)
authenticated_user, authenticated_token = self.authentication.authenticate(request)
self.assertEqual(authenticated_user, self.user)
self.assertEqual(authenticated_token.pk, token.pk)
token.refresh_from_db()
self.assertIsNotNone(token.last_used_at)
def test_rejects_expired_api_token(self):
token, raw_token = APIToken.objects.create_token(user=self.user, name="n8n")
token.expires_at = timezone.now() - timedelta(minutes=1)
token.save(update_fields=["expires_at"])
request = self.factory.get(
"/api/accounts/",
HTTP_AUTHORIZATION=f"Token {raw_token}",
)
with self.assertRaisesRegex(AuthenticationFailed, "expired"):
self.authentication.authenticate(request)
def test_rejects_revoked_api_token(self):
token, raw_token = APIToken.objects.create_token(user=self.user, name="n8n")
token.revoked_at = timezone.now()
token.save(update_fields=["revoked_at"])
request = self.factory.get(
"/api/accounts/",
HTTP_AUTHORIZATION=f"Token {raw_token}",
)
with self.assertRaisesRegex(AuthenticationFailed, "revoked"):
self.authentication.authenticate(request)
def test_stores_secret_as_sha256_not_raw(self):
token, raw_token = APIToken.objects.create_token(user=self.user, name="n8n")
_key, secret = APIToken.parse_raw_token(raw_token)
self.assertNotIn(secret, token.token_hash)
self.assertEqual(len(token.token_hash), 64)
self.assertTrue(token.check_secret(secret))
def test_falls_through_for_non_prefixed_token(self):
request = self.factory.get(
"/api/accounts/",
HTTP_AUTHORIZATION="Token deadbeefdeadbeefdeadbeef",
)
# Not our prefix: return None so another authenticator can handle it.
self.assertIsNone(self.authentication.authenticate(request))
@override_settings(API_TOKEN_LAST_USED_UPDATE_INTERVAL=600)
def test_last_used_at_is_throttled_within_interval(self):
token, raw_token = APIToken.objects.create_token(user=self.user, name="n8n")
request = self.factory.get(
"/api/accounts/",
HTTP_AUTHORIZATION=f"Token {raw_token}",
)
self.authentication.authenticate(request)
token.refresh_from_db()
first_used = token.last_used_at
self.assertIsNotNone(first_used)
self.authentication.authenticate(request)
token.refresh_from_db()
self.assertEqual(token.last_used_at, first_used)
@override_settings(API_TOKEN_LAST_USED_UPDATE_INTERVAL=0)
def test_last_used_at_updates_after_interval(self):
token, raw_token = APIToken.objects.create_token(user=self.user, name="n8n")
token.last_used_at = timezone.now() - timedelta(minutes=5)
token.save(update_fields=["last_used_at"])
stale = token.last_used_at
request = self.factory.get(
"/api/accounts/",
HTTP_AUTHORIZATION=f"Token {raw_token}",
)
self.authentication.authenticate(request)
token.refresh_from_db()
self.assertGreater(token.last_used_at, stale)