SessionAuthentication enforcing CSRF on @action decorator when permission_classes is overridden despite JWTAuthentication set on viewset

Body:

Environment:

  • Django 5.x
  • Django REST Framework
  • djangorestframework-simplejwt
  • React (Vite) frontend using JWT Bearer tokens

Problem:

I have a ModelViewSet with authentication_classes = [JWTAuthentication] set at the class level. When I add a custom @action decorator and override permission_classes, the endpoint starts returning 403 Forbidden (CSRF cookie not set) on unsafe methods (PATCH, POST).

Expected behavior: The action should inherit authentication_classes from the parent viewset.

Actual behavior: Overriding permission_classes on the action causes authentication_classes to stop inheriting from the viewset and fall back to DRF’s global defaults, which include SessionAuthentication. That class enforces CSRF on unsafe methods, causing the 403 even though the request has a valid Bearer token.

Workaround: Explicitly setting authentication_classes=[JWTAuthentication] on every @action that overrides permission_classes fixes it — but this feels like a bug or at least a surprising gotcha worth documenting.

Question: Is this intended DRF behavior? If so, where is it documented?

code:

from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticate

from applications.models import JobApplication
from applications.serializers.applications import JobApplicationSerializer

from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.decorators import  action
from rest_framework.response import Response

class JobApplicationViewSet(ModelViewSet):
    serializer_class = JobApplicationSerializer
    permission_classes = [IsAuthenticated]
    authentication_classes = [JWTAuthentication]

    def get_queryset(self):
        return JobApplication.objects.filter(
            employee=self.request.user.employee
        )

    def perform_create(self, serializer):
        serializer.save(
            employee=self.request.user.employee
        )

    @action(
        detail=True,
        methods=["patch"],
        permission_classes=[IsAuthenticated],
        authentication_classes=[JWTAuthentication],
    )

    def add_stage(self, request, pk=None):
        try:
            application = JobApplication.objects.get(pk=pk)
        except JobApplication.DoesNotExist:
            return Response({"error": "Not found"}, status=404)

        new_stage = request.data.get("stage")
        if not new_stage:
            return Response({"error": "Stage is required"}, status=400)

        history = application.status or []
        history.append(new_stage)
        application.status = history
        application.save()
        return Response({"status": application.status})

browser client:

    const res = await fetch(
      `http://localhost:8000/api/job-applications/${candidateId}/add-stage/`,
      {
        method: "PATCH",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({
          stage,
        }),
      }

Welcome @Nasirsuave !

Side Note: When posting code here, enclose the code between lines of three
backtick - ` characters. This means you’ll have a line of ```, then your code,
then another line of ```. This forces the forum software to keep your code
properly formatted. (I have taken the liberty of correcting your original post.
Please remember to do this in the future.)
Also, I would suggest that you not post your own original material as a “quote” by prefixing each line with >