Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS + Django Rest Framework + CORS ( CSRF Cookie not showing up in client )

I am developing a 1-page application in AngularJS using and Django Rest Framework + Django CORS Headers.

My problem is that the "csrftoken" cookie never shows up in my browser when I have contacted the backend.

For example: I am doing a login using a post. I get the "sessionid" cookie properly but the "csrftoken" never shows up and therefor I cannot do proper posts from my client since I will get denied due the lack of the csrf token.

  • I have analyzed the response headers from the API and the csrftoken is not ther.
  • I have looked directly in the rest API browser and it shows up fine there.
  • Just to point out, I can do my first POST to login since Django Rest Framework only forces CSRF for authenticated users. If I try to relogin it will fail since the "sessionid"-cookie it present.
  • I am not interessted in bypassing the CSRF protection as some posts on stackoverflow suggests.

Some code snippets from front/backend. These are unfinnished snippets, so dont get hung up on poorly written code.

Backend API LoginView

class LoginView(APIView):

renderer_classes = (JSONPRenderer, JSONRenderer)

def post(self, request, format=None):
    serializer = LoginSerializer(data=request.DATA)

    if serializer.is_valid():
        userAuth = authenticate(username=serializer.data['username'], password=serializer.data['password'])

        if userAuth:

            if userAuth.is_active:
                login(request, userAuth)

                loggedInUser = AuthUserProfile.objects.get(pk=1)
                serializer = UserProfileSerializer(loggedInUser)

                user = [serializer.data, {'isLogged': True}]



        else:
            user = {'isLogged': False}

        return Response(user, status=status.HTTP_200_OK)

    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Client side AngularJS Login Controller

.controller('LoginCtrl', ['$scope', '$http', 'uService', '$rootScope', function(scope, $http, User, rootScope) {

scope.login = function() {

    var config = {
        method: 'POST',
        withCredentials: true,
        url: rootScope.apiURL+'/user/login/',
        data : scope.loginForm
    };

    $http(config)
    .success(function(data, status, headers, config) {

        if (status == 200) {
            console.log(data[0]); //Test code
            // succefull login
            User.isLogged = true;
            User.username = data.username;

        }
        else {
            console.log(data); //Test code
            User.isLogged = false;
            User.username = '';
        }

    })
    .error(function(data, status, headers, config) {
        console.log('Testing console error');
        User.isLogged = false;
        User.username = '';
    });
};

}]);

Anyone with any good tips/ideas/examples?

like image 886
JoakimB Avatar asked Jul 29 '13 18:07

JoakimB


3 Answers

AngularJS Single Page Web Application on Sub-domain A, talking to a Django JSON (REST) API on Sub-domain B using CORS and CSRF protection

Since I'm currently working on a similar setup and was battling to get CORS to work properly in combination with CSRF protection, I wanted to share my own learnings here.

Setup - The SPA and the API are both on different sub-domains of the same domain:

  • AngularJS (1.2.14) Single Page Web Application on sub-domain app.mydomain.com
  • Django App (1.6.2) implements a JSON REST API on sub-domain api.mydomain.com

The AngularJS app is served through a Django App in the same project as the Django API APP such that it sets a CSRF Cookie. See, for instance, also How to run multiple websites from one Django project

Django API App - In order to get CORS and CSRF protection working I needed to do the following at the API backend.

In settings.py for this app (an extension of the Django project settings.py):

  • Add the corsheaders app and middleware and the CSRF middleware:
INSTALLED_APPS = (
    ...
    'corsheaders',
    ...
)

MIDDLEWARE_CLASSES = (
    ...
    'django.middleware.csrf.CsrfViewMiddleware',
    ...
    'corsheaders.middleware.CorsMiddleware',
)

Also see Django CORS headers on GitHub

  • Add the domain for the SPA Webapp to the CORS_ORIGIN_WHITELIST
CORS_ORIGIN_WHITELIST = [
    ...
    'app.mydomain.com',
    ...
]
  • Set CORS_ALLOW_CREDENTIALS to True. This is important, if you don't do this, no CSRF cookie will be sent with the request

CORS_ALLOW_CREDENTIALS = True

Add the ensure_csrf_cookie decorator to your views handling the JSON API requests:

from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def myResource(request):
    ...

Django App for AngularJS - The AngularJS app is served through a Django App in the same project. This Django App is set-up to set a CSRF Cookie. The CSRF token from the cookie is then used for requests to the API (which thus runs as a part of the same Django project).

Note that almost all files related to the AngularJS application are just static files from the Django perspective. The Django App only needs to serve the index.html to set the cookie.

In settings.py for this app (again an extension of the Django project settings.py), set the CSRF_COOKIE_DOMAIN such that subdomains can also use them:

CSRF_COOKIE_DOMAIN = ".mydomain.com"

In views.py, I only need to render the AngularJS index.html file, again using the ensure_csrf_cookie decorator:

from django.shortcuts import render
from django.views.decorators.csrf import ensure_csrf_cookie

# Create your views here.
@ensure_csrf_cookie
def index(request):
    return render(request, 'index.html')

Sending requests to the API using AngularJS - In the AngularJS App config set the following $httpProvider defaults:

$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
$httpProvider.defaults.withCredentials = true;

Again, take note of the withCredentials, this ensures that the CSRF Cookie is used in the request.

Below I show how you can make requests to the api using the AngularJS $http service and JQuery:

$http.post("http://api.mydomain.com/myresource", {
    field1   : ...,
      ...
    fieldN   : ...
}, {
    headers : {
        "x-csrftoken" : $cookies.csrftoken
    }
});

Also see ngCookies module.

Using JQuery (1.11.0):

$.ajax("http://api.mydomain.com/myresource", {
    type: 'POST',
    dataType : 'json',
    beforeSend : function(jqXHR, settings) {
        jqXHR.setRequestHeader("x-csrftoken", get_the_csrf_token_from_cookie());
    },
    cache : false,
    contentType   : "application/json; charset=UTF-8",
    data : JSON.stringify({
        field1   : ...,
          ...
        fieldN   : ...
    }),
    xhrFields: {
        withCredentials: true
    }
});

I hope this helps!!

like image 168
Visionscaper Avatar answered Nov 09 '22 06:11

Visionscaper


Directly from the docs https://docs.djangoproject.com/en/1.9/ref/csrf/#ajax

If your view is not rendering a template containing the csrf_token template tag, Django might not set the CSRF token cookie. This is common in cases where forms are dynamically added to the page. To address this case, Django provides a view decorator which forces setting of the cookie: ensure_csrf_cookie().

Since your application is a single-page application, you can add ensure_csrf_cookie() to the view that is responsible for the initial page load.

like image 21
Derek Kwok Avatar answered Nov 09 '22 06:11

Derek Kwok


So I found my own solution to this, seems to work great.

This is the new snippets of my code:

Backend API LoginView ( added a decorator forcing the csrf token to be added to the body )

class LoginView(APIView):

renderer_classes = (JSONPRenderer, JSONRenderer)

@method_decorator(ensure_csrf_cookie)
def post(self, request, format=None):
    c = {}
    c.update(csrf(request))
    serializer = LoginSerializer(data=request.DATA)

    if serializer.is_valid():
        userAuth = authenticate(username=serializer.data['username'], password=serializer.data['password'])

        if userAuth:

            if userAuth.is_active:
                login(request, userAuth)

                loggedInUser = AuthUserProfile.objects.get(pk=1)
                serializer = UserProfileSerializer(loggedInUser)

                user = [serializer.data, {'isLogged': True}]



        else:
            user = {'isLogged': False}

        return Response(user, status=status.HTTP_200_OK)

    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

AngularJS Client side ( add token to the request header )

$http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;

Server side settings file ( Specificly for django-cors-headers )

First 5 are added by default, but you need to add "X-CSRFToken" to allow such a header from the client to the API using CORS, else the post will be denied.

CORS_ALLOW_HEADERS = (
'x-requested-with',
'content-type',
'accept',
'origin',
'authorization',
'X-CSRFToken'

)

Thats it!

like image 7
JoakimB Avatar answered Nov 09 '22 04:11

JoakimB