3

I am using httpie to play with my api written in django 1.7 and django rest framework 2.4. Today I was trying to delete an object:

$ http DELETE :8000/api/items/8/ --verbose
DELETE /api/items/8/ HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 0
Host: 127.0.0.1:8000
User-Agent: HTTPie/0.8.0


HTTP/1.0 204 NO CONTENT
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Language: cs
Content-Length: 0
Date: Wed, 07 Jan 2015 21:47:06 GMT
Server: WSGIServer/0.1 Python/2.7.6
Vary: Accept, Accept-Language, Cookie

Which was successful even though it should require CSRF token. When I try to delete the object from Chrome with following code:

$.ajax({
    type: "DELETE",
    url: "http://127.0.0.1:8000/api/items/6/"
});

I get a following request:

DELETE /api/items/6/ HTTP/1.1
Host: 127.0.0.1:8000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: */*
Origin: http://127.0.0.1:8000
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
DNT: 1
Referer: http://127.0.0.1:8000/inventory
Accept-Encoding: gzip, deflate, sdch
Accept-Language: cs,en-US;q=0.8,en;q=0.6,es;q=0.4,pt;q=0.2,sk;q=0.2
Cookie: cc_csrf=bd9fbbc8f75cffa2e1e3d2c95c2185c5; _ga=GA1.1.2038400685.1386436341; __utma=96992031.2038400685.1386436341.1417173095.1417428975.79; __utmz=96992031.1409752584.3.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __zlcmid=MpdRtV3vZuf3D9; djdt=hide; sessionid=kiihjh6m77jm8v9ol7xrryip89sny55i; csrftoken=FtnnEWPLhMh0CAGMRMH77nB0AAno93uW

Response:

HTTP/1.0 403 FORBIDDEN
Date: Wed, 07 Jan 2015 21:57:40 GMT
Server: WSGIServer/0.1 Python/2.7.6
Vary: Accept, Accept-Language, Cookie
Content-Type: application/json
Content-Language: en
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS

{"detail": "CSRF Failed: CSRF token missing or incorrect."}

My settings:

REST_FRAMEWORK = {
    # Use hyperlinked styles by default.
    # Only used if the `serializer_class` attribute is not set on a view.
    'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.HyperlinkedModelSerializer',

    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],

    'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',),
    'DATETIME_FORMAT': "%B %d, %Y"
}
MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'debug_toolbar.middleware.DebugToolbarMiddleware',
)

So my question is: what is the difference between sending a DELETE request with JS ajax and with sending the request with http?

Visgean Skeloru
  • 2,237
  • 1
  • 24
  • 33
  • Is the httpie command in your question the exact one you are using? Or are you passing credentials along somewhere? That request should have failed if it was anonymous. – Kevin Brown-Silva Jan 07 '15 at 23:13
  • @VisgeanSkeloru, can you try to remove your Javascript's cookie or try using `private` mode to send the ajax again? – Anzel Jan 07 '15 at 23:18
  • 1
    @VisgeanSkeloru, I've updated my answer accordingly, apologise I was in the wrong direction before and after discussing with other members, and re-study your request/response bodies, I realise I have missed the main point, which isn't about your *missing* CSRF token, it's about *incorrect* token – Anzel Jan 08 '15 at 00:01

3 Answers3

5

It's because the CSRF check is only performed when authenticated using SessionAuthentication, (i.e. using the sessionid cookie set by django.contrib.auth):

If you're using SessionAuthentication you'll need to include valid CSRF tokens for any POST, PUT, PATCH or DELETE operations. (source)

I assume you use another auth method for the HTTPie-sent request, and therefore the CSRF check doesn't get applied there.

https://github.com/tomchristie/django-rest-framework/blob/master/tests/test_authentication.py

Jakub Roztocil
  • 15,930
  • 5
  • 50
  • 52
2

When the request is made through the browser, it is including the sessionid token in the Cookie header. This header is automatically set by the browser, and includes other cookies that have been set (like djdt=hide by the Django Debug Toolbar).

Cookie: ...; sessionid=kiihjh6m77jm8v9ol7xrryip89sny55i; ...

Because of this, Django is authenticating the request automatically (just like it normally would), which is triggering the SessionAuthentication provided by Django REST framework. SessionAuthentication requires that the CSRF token is being validated, which is included in the csrftoken cookie and X-CSRFToken header, to ensure that nothing suspicious is happening.

This means that you must set the X-CSRFToken header when making your request in the browser. Django includes some useful snippets of code for popular libraries in their documentation on CSRF.

Now, when making requests through HTTPie, you are typically using a different form of authentication such as basic authentication. By default, Django REST framework enables BasicAuthentication and SessionAuhentication, unless you override them, and most of the documentation expects that you are using basic authentication.

HTTPie supports basic authentication through the -a username:password parameter. This would explain why you are not getting any permission issues when making the DELETE request, as without authentication you should be getting a 403 error. The DjangoModelPermissionsOrAnonReadOnly should not allow you to make the request you have provided without being authenticated.

Community
  • 1
  • 1
Kevin Brown-Silva
  • 40,873
  • 40
  • 203
  • 237
1

EDITED & UPDATED

OK apart from the explanations others already mentioned, after all that, we can conclude the reason why Httpie allows your DELETE but not Javascript:

1) Since you have actually disabled your authentication, in theory all METHODS will be allowed from individual HTTP call, hence your Httpie works (just like when you use Curl), because Restframework doesn't require you to.

2) Ajax call from Javascript, however is slightly different because you're using your browser console to do the call, which is in fact within the browser session. Further to this, your cookie has stored your previous GET's CSRF token which afterward when you perform the Ajax call, the CSRF token has been extracted from Django/Restframework which doesn't MATCH (because CSRF token will be automatically re-generated on each request. So this is a matter of INCORRECT rather than *MISSING** token.

Hence like in my above comment, removing your browser's cookie / using a Private session has indeed resolved the issue, and successfully allowed you to perform Ajax style DELETE.

Hope this helps, and thanks for everyone's guidance and hints leading me to this conclusion.

Anzel
  • 19,825
  • 5
  • 51
  • 52
  • I removed the authentication for demonstration purposes... I am kind of confused: where does http get the csrf cookie in the first place? If it is a single request there is no prior communication between server and httpie. Also would not that show up in the http request? I specified the verbose parameter and there is no csrf mentioned. – Visgean Skeloru Jan 07 '15 at 22:32
  • @VisgenSkeloru, can you show the full request and response body? I need to check the doc, but I think httpie will simulate a get right before a post if there is no previous session found. – Anzel Jan 07 '15 at 22:36
  • The full requests and response are in my original question. There is no get request - I would see that in the server log. – Visgean Skeloru Jan 07 '15 at 22:47
  • @VisgeanSkeloru, have you set your httpie to be persistent, or have you done any previous GET/POST etc calls to the site? Session will be stored when you make the calls from the same host address. – Anzel Jan 07 '15 at 22:52
  • I did some get request but not with persistent cookies. I deleted .httpie/sessions but it still behaves the same. – Visgean Skeloru Jan 07 '15 at 23:01
  • [HTTPie doesn't remember cookies by default](https://github.com/jakubroztocil/httpie#sessions) – Jakub Roztocil Jan 07 '15 at 23:03
  • @JakubRoztočil, you're partially right. It doesn't by default but it will persist when custom headers are sent (which httpie does), please see my updated answer – Anzel Jan 07 '15 at 23:13
  • @Anzel you need to explicitelly enable the session feature (e.g. with `--session=name`) – Jakub Roztocil Jan 07 '15 at 23:14
  • @JakubRoztočil, you're correct, my apologies, I'll remove my answer if it doesn't explain the reason. We will need to investigate a little further. – Anzel Jan 07 '15 at 23:16
  • @JakubRoztočil, I've further investigated and provided a working solution for OP, and updated my explanation. I was in the wrong direction, and thanks for your guidance ;) – Anzel Jan 08 '15 at 00:07
  • @JakubRoztočil, OMG! I just checked and realise I had been debating with the author of **Httpie**? Please accept my apologies for being so arrogant before. And thanks for the awesome *Httpie* tool :) – Anzel Jan 08 '15 at 00:09