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
Вернуться на верх