1
votes

I have a simple Django (Rest Framework) application. I've enabled properly the CSRF, CORS, Session middlewares. I try to debug the front-end UI written with Backbone and the sessionid and csrftoken aren't in the persistent storage of the browser. To confuse me more, when I logout, I receive the sessionid of the Anonymous user (without the pair csrftoken) and that cookie gets persisted.

I use Google Chrome. Symptoms:

  • When i perform the login, I receive in the response the Set-Cookie headers for both tokens
  • The tokens have different expiration dates
  • The tokens appears in the chrome's response cookies tab, but not in the cookies storage
  • If i logout, I receive the sessionid of the anonymous user, without csrftoken and this is persisted as a cookie.

I'm only trying to debug on 127.0.0.1:63342 with the help of Pycharm and Chrome.

Valid settings snippet:

Application definition

INSTALLED_APPS = (
    # 'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.contenttypes',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders',
    'south',

    'tenant',
    'agriculture',
)

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
        # 'rest_framework.authentication.BasicAuthentication',
    ),
    'PAGINATE_BY': 10,
    'PAGINATE_BY_PARAM': 'page_size',
}

SESSION_COOKIE_DOMAIN = CSRF_COOKIE_DOMAIN = None
SESSION_COOKIE_AGE = 60 * 60 * 24 * 30

# CORS headers settings
CORS_ORIGIN_WHITELIST = (
    'localhost:63342',          # List here all the white-listed access points for the API
    '127.0.0.1:63342',
    ...,
)
CORS_ALLOW_CREDENTIALS = True

Relevant views:

class LoginView(APIView):
    """
    The view will respond to the login request by using the underlying Django session authentication. In addition to the
    default behavior will return rich information about the current user being logged in.
    """
    serializer_class = serializers.UserSerializer

    def post(self, request, *args, **kwargs):
        # Get the parameters from the request
        username = request.DATA['username']
        password = request.DATA['password']
        remember = request.DATA.get('remember', False)
        logger.debug('Attempt authentication with %s : "%s"' % (username, password,))
        # Attempt authentication
        user = authenticate(username=username, password=password)
        if user is not None:
            if user.is_active:
                # Care for the session
                login(request, user)
                # se the expiration to 0 if remember wasn't requested
                if not remember:
                    request.session.set_expiry(0)
                # Return successful response
                logger.debug('Login successfully')
                return Response(self.serializer_class(user).data)
            else:
                logger.warn('User %s is de-activated' % username)
                return Response(status=status.HTTP_403_FORBIDDEN)
        else:
            logger.debug('Unauthorized access with %s : "%s"' % (username, password,))
            return Response(status=status.HTTP_401_UNAUTHORIZED)


class AuthenticateView(APIView):
    """
    Based on the received session token, we will check if the session is still valid, meaning that we will check if the
    user is authenticated. If the request gets to be processed, means that the session token is still valid, otherwise
    we will issue an 401 status. If the session is valid, then return the user data.
    """
    permission_classes = (IsAuthenticated,)
    serializer_class = serializers.UserSerializer

    def get(self, request, *args, **kwargs):
        return Response(self.serializer_class(request.user).data)


class LogoutView(APIView):
    """
    Will simply care to logout the user which was logged in. Will use the default behavior form Django, which doesn't
    require that the uses is logged in.
    """
    def post(self, request, *args, **kwargs):
        logout(request)
        return Response(status=status.HTTP_200_OK)
1
The reason they aren't set is because you are sending Response, when you should be sending an instance of RequestContext, but more importantly - if external clients will be using POST, then you should not expose your CSRF token (your endpoints should be exempt). So you really should be disabled CSRF for your API. - Burhan Khalid
I beg to differ. The framework enforces CSRF exactly for methods which alter the state of the server, namely for POST, PUT, PATCH and DESTROY. Also, being in the context of Django Rest Framework, I need to send a response from the view (rest_framework.response.Response), not a RequestContext. - Roba

1 Answers

3
votes

OK, I've identify the issue.

It has to do with how are the Ajax calls configured on the server. I was attempting to set them up only after a successful call to the authentication service, when this should have been done with the very first occasion when the application initializes.

So, the code on the back-end is correct and functions as expected. The corrected behaviour from the front end application was implemented using a service component, responsible with the Ajax initialization; I'm running this with Backbone and RequireJS.

The ajaxSetup.js service:

define([
    'jquery',
    'const'
], function ($, Const) {
    "use strict";

    // these methods don't need csrf header
    var isCsrfSafeMethod = /^(GET|HEAD|OPTIONS|TRACE)$/;

    var setupAjax = function () {
        // Setup the AJAX calls
        $.ajaxSetup({
            // enable authentication
            xhrFields: { withCredentials: true },

            // setup csrf handling
            beforeSend: function (xhr, settings) {
                if (!isCsrfSafeMethod.test(settings.type) &&
                    $.cookie(Const.CSRF_COOKIE_NAME)) {

                    xhr.setRequestHeader("X-CSRFToken", $.cookie(Const.CSRF_COOKIE_NAME));

                }
            },

            // This will setup an handler for the errors 401, redirecting us to an internal login route.
            statusCode: {
                401: function () {
                    console.log("Unauthorized access, trying to re-direct to login.");
                    // Redirect the to the login page.
                    window.location.replace("#/login");
                }
            }
        });
    };

    return { setupAjax: setupAjax };
});

Obviously enough, I have a module Const which exposes the CSRF_COOKIE_NAME constant (among others). When the one-page-app bootstraps, it will make something like this:

The desktopInit.js module:

define([
    'jquery',
    'backbone',
    'underscore',
    'services/ajaxSetup',
    'services/authentication',
    'text!templates/desktop/body.html',
    'views/desktop/navbar',
    'bootstrap'     // load dependency to be used by views
], function ($, Backbone, _, AjaxSetup, Authentication, BodyTemplate, NavbarView) {

    console.log("Desktop initialization ...");

    // Setup the AJAX calls
    AjaxSetup.setupAjax();

    // Try to authenticate, if there is a logged in user.
    Authentication.authenticate();

    // Fill in the body element with the bare bone layout
    $("body").html(_.template(BodyTemplate, {}));

    // we instantiate here the view because we need to control the creation moment of this view, rather then to have it
    // as a singleton served by require.js. At this stage, the bare bone layout for the <body> element exist and the
    // view can find it's anchor and it can render itself properly.
    (new NavbarView()).render();

    // Start the backbone history
    Backbone.history.start();

});

This was fun ...

--Roba


Latter edit for the ones using Angular: there is a special place to place your CSRF handshake code: in one of my projects, in the main config call I'm doing something like this for the $http service:

(function () {

    angular.module('MyApp').config(['$httpProvider', function () {
        // set up CRSF handshake with Django Rest Framework
        $httpProvider.defaults.xsrfCookieName = CONST.CSRF_COOKIE_NAME;
        $httpProvider.defaults.xsrfHeaderName = CONST.CSRF_HEADER_NAME;
        // we will use credentials
        $httpProvider.defaults.withCredentials = true;
    }]);

})();