Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bad CSRF token with AngularJS and Express

I would like to add a CSRF protection in my app which uses a MEAN stack.

I tried the answer already given by someone : CSRF Protection in ExpressJS

But it was for the older express version, so I made some changes :

app.use(cookieParser(config.cookieSecret, { httpOnly: true }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({ secret: config.sessionSecret, resave: false, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());
app.use(csrf({value: function(req) {
        var token = (req.body && req.body._csrf)
            || (req.query && req.query._csrf)
            || (req.headers['x-csrf-token'])
            || (req.headers['x-xsrf-token']);
        return token;
    }
}));
app.use(function(req, res, next) {
  res.cookie('XSRF-TOKEN', req.csrfToken());
  next();
});

I can see the token named XSRF-TOKEN is well generated on my app (using Chrome inspection).

But when I post a form (Angular frontend), I have an error about the token :

{"message":"invalid csrf token","error":{"expose":true,"code":"EBADCSRFTOKEN","statusCode":403,"status":403}}

Did I miss something ? I'm wondering if req.csrfToken() generates the good token given by Angular...

EDIT :

I just see the XSRF-TOKEN is used by AngularJS in $http requests only. So I think I have to add a hidden input in my form to post with csrf value, to be checked by Express, but how ?

like image 911
jbltx Avatar asked Dec 11 '22 02:12

jbltx


2 Answers

Finally I was able to send the good token value. Here's the complete answer.

AngularJS uses CSRF protection (called XSRF by Angular) during requests made with $http service.

When performing XHR requests, the $http service reads a token from a cookie (by default, XSRF-TOKEN) and sets it as an HTTP header (X-XSRF-TOKEN). Since only JavaScript that runs on your domain could read the cookie, your server can be assured that the XHR came from JavaScript running on your domain. The header will not be set for cross-domain requests.

There are many posts which explained how to send XSRF-TOKEN with ExpressJS 3.xx, but some things change with 4.xx version. Connect middlewares are not included anymore. Express uses its own middelware : cookie-parser, body-parser, express-session, and csurf.

1 - Send XSRF-TOKEN cookie

The first step is to send the cookie, from the backend to the frontend (Express to Angular) :

var express         = require('express');
var app             = express();
var cookieParser    = require('cookie-parser');
var bodyParser      = require('body-parser');
var session         = require('express-session');
var config          = require('./lib/config'); //config.js file with hard-coded options.
var csrf            = require('csurf');

app.use(cookieParser(config.cookieSecret, { httpOnly: true }));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({ 
    name: 'sessionID', 
    secret: config.sessionSecret, 
    cookie: {
        path: '/',
        httpOnly: true,
        secure: false,
        maxAge: 3600000
    },
    rolling: true, 
    resave: false, 
    saveUninitialized: true 
}));
app.use(csrf());
app.use(function(req, res, next) {
    res.cookie('XSRF-TOKEN', req.csrfToken());
    next();
});

Now Angular is able to set its HTTP header (X-XSRF-TOKEN) during $http requests. Example :

<body ng-app="testApp">
    <div ng-controller="LoginCtrl">
        <form role="form" ng-submit="login()" >
           <input type="email" ng-model="user.email" />
           <input type="password" ng-model="user.password" />
           <input type="submit" value="Log In" />
        </form>
        <p style="color:red">{{loginMsg}}</p>
    </div>
</body>

<script>
    var app = angular.module('testApp', ['ngResource']);
    app.controller('LoginCtrl', function ($scope, $http) {
        $scope.user = {};
        $scope.loginMsg = '';
        $scope.login = function () {
             $http.post('/login', $scope.user).success(function () {
                  $window.location.href='/profile';
             }).error(function () {
                  $scope.loginMsg = 'Wrong credentials, try again.';
             });
        };
    });
</script>

2 - Add a _csrf input in non-Angular forms

That was my issue, I didn't know how to add this input. Finally I created a new Angular service named $csrf :

    app.factory('$csrf', function () {
        var cookies = document.cookie.split('; ');
        for (var i=0; i<cookies.length; i++) {
          var cookie = cookies[i].split('=');
          if(cookie[0].indexOf('XSRF-TOKEN') > -1) {
            return cookie[1];
          }
        }
        return 'none';
    });

I made an example on Plunker : http://plnkr.co/edit/G8oD0dDJQmjWaa6D0x3u

Hope my answer will help.

like image 200
jbltx Avatar answered Jan 03 '23 20:01

jbltx


if anyone gets "csurf configuration error", try this:

app.use( csrf( { cookie: true } ));
like image 32
Spock Avatar answered Jan 03 '23 21:01

Spock