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