API-Token: Anfragen mit Bearer-Token brauchen kein CSRF-Token
All checks were successful
Build and push Docker image / build (push) Successful in 18s
Test / test (push) Successful in 1m36s

Native Clients (diora desktop) haben kein CSRF-Cookie; ein Browser hängt den
Authorization-Header nie von selbst an, daher ist das sicher. Nötig z. B. für
den M3U-Import, der nicht csrf_exempt ist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-09-29 19:33:18 +02:00
parent 086ce29020
commit 77d662a9cb
2 changed files with 19 additions and 0 deletions

View file

@ -22,4 +22,9 @@ class ApiTokenAuthMiddleware:
api_token = ApiToken.objects.select_related('user').filter(token=token).first()
if api_token is not None:
request.user = api_token.user
# Browsers never attach this header on their own, so a
# token request can't be a cross-site forgery; native
# clients have no CSRF cookie to send. (CsrfViewMiddleware
# checks this flag in process_view, which runs after us.)
request._dont_enforce_csrf_checks = True
return self.get_response(request)

View file

@ -42,6 +42,20 @@ class ApiTokenAuthMiddlewareTests(TestCase):
resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}')
self.assertEqual(resp.status_code, 200)
def test_bearer_token_needs_no_csrf_token(self):
# import_m3u is not csrf_exempt: without a token it must still be protected.
from django.test import Client
client = Client(enforce_csrf_checks=True)
m3u = '#EXTM3U\n#EXTINF:-1,Test FM\nhttps://example.com/stream\n'
resp = client.post('/radio/import/', data=m3u, content_type='text/plain')
self.assertIn(resp.status_code, (302, 401, 403))
resp = client.post('/radio/import/', data=m3u, content_type='text/plain',
HTTP_AUTHORIZATION=f'Bearer {self.token.token}')
self.assertNotEqual(resp.status_code, 403)
resp = client.post('/radio/import/', data=m3u, content_type='text/plain',
HTTP_AUTHORIZATION='Bearer not-a-real-token')
self.assertIn(resp.status_code, (302, 401, 403))
def test_regenerate_view_requires_login_and_rotates_token(self):
resp = self.client.post('/accounts/api-token/regenerate/')
self.assertEqual(resp.status_code, 302)