How can I send a POST request with a CSRF Token and a JSON body to a Django endpoint?


I'm developing an Android Studio app which talks to a server running Django. I'm in the process of implementing the login process on the phone, however the server always returns 403 Forbidden to my POST requests, even with the X-CSRFToken header set.

In the app, an initial screen makes a GET request to the /phone/login endpoint, in order to get the CSRF Token from the Headers (I use the @ensure_csrf_cookie decorator on the Django view to ensure it's sent). Based on the response, the app navigates to a login screen where the user can input their details. That screen then makes a POST request to the /phone/login endpoint, with the token set in the headers and the details as JSON in the body (converted by gson). The server then gets this and processes it (not implemented).

I can see in the logs that the POST request is sent with the header and correct csrf token (as in, the same as I received from the server). Despite this, the server always responds with 403 Forbidden.

To make sure it wasn't a problem with the header name, I explicitly set CSRF_HEADER_NAME to the default value in my Django settings file. I've also tried sending an empty String in the body, and getting a csrf token with get_token(request) which I include in the LoginRequestBody object, like would be included with a html form, to the same result. The browser login page works fine, it's just the requests from the app getting this. I could obviously use @crsf_exempt on the view, but as it's still accessible from a browser wouldn't this be a security concern? Any help appreciated.

ViewModel.kt:

fun getLogin() { viewModelScope.launch { try { val response = CheckInApi.retrofitService.getLoggedIn() val body = response.body() ?: LoginResponse() if (response.code() == 200 ) { if (body.result == "LOGGED IN") { _uiState.update { currentState -> currentState.copy(result = AuthenticationState.SUCCESS)} } else if (body.result == "NOT LOGGED IN") { val cookies = response.headers().get("Set-Cookie") val csrfCookieHeader = cookies?.substring(10,42) ?: "No cookie" _uiState.update { currentState -> currentState.copy(result = AuthenticationState.LOGIN_REQUIRED,csrfCookieHeader = csrfCookieHeader)} } } } catch (e: IOException) { Log.d(TAG, e.toString()) } } } fun postLogin() { viewModelScope.launch { val loginRequestBody = LoginRequestBody(_uiState.value.username, _uiState.value.password) try { val result = CheckInApi.retrofitService.requestLogin( uiState.value.csrfCookieHeader, loginRequestBody) } catch (e: IOException) { Log.d(TAG, e.toString()) } } }

CheckInApiService.kt:

private val retrofit = Retrofit.Builder() .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .baseUrl(BASE_URL) .build() interface CheckInApiService { @POST("phone/login/") suspend fun requestLogin(@Header("X-CSRFToken") token : String, @Body loginBody : LoginRequestBody): Response<String> @GET("phone/login/") suspend fun getLoggedIn(): Response<LoginResponse> } object CheckInApi { val retrofitService : CheckInApiService by lazy { retrofit.create(CheckInApiService::class.java) } }

views.py:

@ensure_csrf_cookie def login(request): if request.method not in ["GET", "POST"]: return HttpResponseNotAllowed(["GET", "POST"]) if request.user.is_authenticated: return HttpResponse("LOGGED IN") if request.method == "GET": response = { "result" : "NOT LOGGED IN" } return JsonResponse(response) else: # Log user in pass
0
Mar 20 at 1:52 PM
User Avataruser32522523
#android#json#django#retrofit2#django-csrf

Accepted Answer

You’re getting a 403 because Django CSRF needs two things, not just the header:

  1. The X-CSRFToken header

  2. The csrftoken cookie

Right now you’re only sending the header. Django checks that the token in the header matches the one in the cookie. If the cookie isn’t there → 403.


Why it works in browser but not Android

Browsers automatically:

  • store cookies from the GET response

  • send them back on POST

Your Android app doesn’t do that unless you explicitly handle cookies.


What’s wrong in your code

This part:

val csrfCookieHeader = cookies?.substring(10,42)

You’re not actually storing/sending the cookie properly. You’re just slicing a string.


What you need to do

  1. Extract the CSRF token properly from Set-Cookie

  2. Send:

    • header: X-CSRFToken

    • header: Cookie: csrftoken=...


Fix

Update your API:

@POST("phone/login/")
suspend fun requestLogin(
    @Header("X-CSRFToken") token: String,
    @Header("Cookie") cookie: String,
    @Body loginBody: LoginRequestBody
): Response<String>

Then send it like:

val csrfToken = ... // extract properly
val cookie = "csrftoken=$csrfToken"

CheckInApi.retrofitService.requestLogin(
    csrfToken,
    cookie,
    loginRequestBody
)

Important

  • Cookie name must be exactly csrftoken

  • Header must be X-CSRFToken

  • Both values must match


About csrf_exempt

Don’t use it here. For login endpoints, that’s a bad idea.

If this is a mobile-only API, better long-term solution is:

  • use JWT or token auth

  • skip CSRF entirely

User AvatarTravis Fernandes
Mar 20 at 6:34 PM
0