express + AngularJS でCSRFの設定

最近カップラーメンに納豆を入れるのが好きで、カップラーメンに納豆3パックとかドバっと入れて、そこに卵を入れて食べたりする。
見た目は悪いけど意外といけるので、興味ある方はお試しあれ。

ところで最近は Node.js とか express とか mongodb とか AngularJS を勉強中。
こちら のデモアプリをベースにいろいろといじって変更しながら、挙動を確認しつつ勉強してるけど、どれも勉強し始めでつまずくことが多い。
今回は、AngularJS でデータの更新をしようとしたときに、express 側で CSRF エラーが出たので、その内容と対処方法をまとめてみた。

CSRF エラー

AngularJS でデータ更新をしようとしたところ、express 側で下記のようなエラーが出た。

1
Error: Forbidden
    at Object.exports.error (/home/nodejs/em_test/node_modules/express/node_modules/connect/lib/utils.js:63:13)
    at createToken (/home/nodejs/em_test/node_modules/express/node_modules/connect/lib/middleware/csrf.js:82:55)
    at Object.handle (/home/nodejs/em_test/node_modules/express/node_modules/connect/lib/middleware/csrf.js:48:24)
    at next (/home/nodejs/em_test/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at Object.handle (/home/nodejs/em_test/node_modules/view-helpers/index.js:65:5)
    at next (/home/nodejs/em_test/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at Object.handle (/home/nodejs/em_test/node_modules/connect-flash/lib/flash.js:21:5)
    at next (/home/nodejs/em_test/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at SessionStrategy.strategy.pass (/home/nodejs/em_test/node_modules/passport/lib/middleware/authenticate.js:314:9)
    at /home/nodejs/em_test/node_modules/passport/lib/strategies/session.js:61:12
PUT /admin/expenses/537bfa12db0460ff19812b80 500 490ms

CSRF のエラー。
express では、確かに CSRF の設定して、フォームに下記の行を書いており、

1
input(type="hidden", name="_csrf", value="#{csrf_token}")

こんな感じで値が入っていたなーと。

1
<input type="hidden" name="_csrf" value="hSPcdgooM3t461mJRl98Zx6o9KeNLU18ncXEA=">

AngularJS ではそういう設定してないし、エラーが出るのも納得。
ということで、express + AngularJS での CSRF の設定方法を調べてやってみた。
参考URL:
Using CSRF protection with Express and AngularJS
ExpressJS, AngularJS and Cross-site request forgery (csrf) protection

対応方法

1. express アプリに CSRF middleware を設定

まず、CSRF middleware を追加。
セッションを扱えないとダメなので、session() の後に追加すること。
今回のデモアプリには既に設定済みだったけど。

1
app.use(express.csrf());

Cross Site Request Forgery (XSRF) Protection

Angular provides a mechanism to counter XSRF. 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).

AngularJS の $http サービスでは、cookie の XSRF-TOKEN を読んで、HTTP ヘッダーに X-XSRF-TOKEN としてセットするので、express 側で cookie の XSRF-TOKEN として値を入れてやる必要がある。

1
app.use(function(req, res, next){
  res.locals.csrf_token = req.csrfToken();
  res.cookie('XSRF-TOKEN', req.csrfToken()); // この行を追加
  next();
});
3. CSRF middleware の token 設定 *1

express 3.x docs - csrf()

The default value function checks req.body generated by the bodyParser() middleware, req.query generated by query(), and the “X-CSRF-Token” header field.

CSRF middleware はデフォルトでは、req.body や req.query やヘッダーの X-CSRF-Token を見に行く。
AngularJS は X-XSRF-TOKEN を使うので、express 側でヘッダーの X-XSRF-TOKEN を見に行くように設定してやる必要がある。
既存の処理に、Angular に必要な req.headers[‘x-xsrf-token’] を追加で見に行くファンクション作成。

1
var csrfValue = 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;
};
  1. で追加した、csrf() に value として設定する。
1
app.use(express.csrf({value: csrfValue}));

*1 express の 4.x では、デフォルトで x-xsrf-token も見るようになっていて、この設定が必要ないようなので注意。
csrf(options)

The default function checks four possible token locations:
_csrf parameter in req.body generated by the body-parser middleware.
_csrf parameter in req.query generated by query().
x-csrf-token and x-xsrf-token header fields.

と、ここまで書いて実際にデモアプリで使っている csrf.js を見てみたらちゃんと

1
|| (req.headers['x-xsrf-token']);

が入っていた!
使っている express のバージョンは、3.4.8なんだけど。
2013年の7月に対応されているっぽい…。
Update csrf.js · b236bc3 · senchalabs/connect

express 3.x docs - csrf() が最新の情報に更新されてないのかな…。
せっかくなので、古いバージョンの express をお使いの方に参考になれば、とここまで書いた内容は残しておこう…。

4. AngularJS でデータ更新

express 側で設定ができたら、後は AngularJS 側では通常通り、データ更新処理を行えばOK。
例えばこんな感じで、データ更新。

1
app.controller('EditCtrl', ['$scope', '$location', 'expense',
  function($scope, $location, expense) {
    $scope.expense = expense;

    $scope.save = function() {
      $scope.expense.$update(function(expense) {
        $location.path('/admin/#/expenses/' + expense.id);
      });
    };

  }
]);

まとめ

express + AngularJS の CSRF 設定は意外と簡単にできちゃうんだな、という印象。
それと、ブログにまとめるっていうのは自分の理解を深める上でいいことだな、と。
今回もこれを書きながら、実は不要な処理があったのに気づけたので…。

このエントリーをはてなブックマークに追加