The “Phantom” 403: Solving Django CSRF Failures in Golang Clients

In the world of backend development, few things are as frustrating as an API that works the first time perfectly but fails mysteriously on the second attempt. This is a common phenomenon when a Golang client interacts with a Django backend using session-based authentication.

The Problem: The “Second-Login” Lockout

You’ve built a secure Django API. You have CsrfViewMiddleware enabled and CSRF_COOKIE_SECURE = True because you're running over HTTPS.

When you test your Golang client, the following happens:

  1. First Attempt: User A logs in. Success! The Go client receives a sessionid and a csrftoken.
  2. The Logout: User A logs out.
  3. Second Attempt: User B tries to log in using the same Go client instance. 403 Forbidden.

Why does it fail only the second time?

Django’s CSRF protection is “lazy” for anonymous requests without cookies. On the first login, the client has no cookies, so Django often lets the request slide through to the authentication logic.

However, once that first login is successful, Django sends a Set-Cookie header. Your Go client’s CookieJar dutifully saves it. On the next request, the client sends that cookie back. Django sees a csrftoken cookie present and thinks: "If you're sending a cookie, you must be a browser. If you're a browser, you must provide the X-CSRFToken header to prove this isn't a cross-site attack."

Because the Go client sends the cookie but forgets the header, the request is rejected.

The Solution: The “Exempt” vs. “Handshake” Patterns

Depending on your security requirements, there are two primary ways to fix this.

Solution 1: The csrf_exempt (The Quick Fix)

If your API is consumed exclusively by non-browser clients (like your Go binary), the risk of a CSRF attack is virtually zero. CSRF is a vulnerability that targets browser behavior.

You can tell Django to stop looking for tokens on the login endpoint:

python
from django.views.decorators.csrf
import csrf_exempt
from django.utils.decorators
import method_decorator@method_decorator(csrf_exempt, name='dispatch')
class LoginView(APIView):

def post(self, request):        # Authentication logic here        return Response({"status":
"Authenticated"})

Solution 2: The Handshake (The Secure Way)

If you want to keep your security tight, your Go client must act like a browser. This requires a two-step process:

  1. The Token Fetch: Perform a GET request to a simple "health check" endpoint to receive the initial cookies.
  2. The Header Injection: Manually extract the CSRF string from the cookie and inject it into the X-CSRFToken header for the POST request.

The Go Implementation:

go
// 1. Extract token from CookieJarvar token stringfor _, cookie := range jar.Cookies(req.URL) {
if cookie.Name == "csrftoken" {        token = cookie.Value    }}// 2. Set the header
manuallyreq.Header.Set("X-CSRFToken", token)

The Verdict

The “Second-Login” problem isn’t a bug; it’s Django’s security working exactly as intended. By default, Django assumes that if you have a cookie, you are at risk of a CSRF attack.

  • Use Solution 1 if your Go client is a standalone tool and you want a clean, simple API.
  • Use Solution 2 if you are building a high-security application where every layer of defense matters.