Angular + HttpClientXsrfModule + Flask で CSRF

Angular と Flaskを使って、CSRFを実現しようとしたのだが、若干手数がかかったのでメモ。

1.Angular + HttpClientXsrfModule

Angularの全体像から、コンセプト、豊富な実例と必携の良書

Angular で、CSRFを実装しようとしてググってみたりしたのだが、サンプルとして言及されている、XSRFStrategy は、Deprecated で、@angular/common/http を代わりに使用するようにとのこと。

https://angular.io/api/http/XSRFStrategy

そもそも @angular/http が、Deprecated なんですね。こちらも、common/httpを使うように書かれていました。

https://angular.io/api/http/HttpModule  こちらではなく、

1.1 モジュール宣言

https://angular.io/guide/http#security-xsrf-protection に沿って実装する。

まず、以下の2つのモジュールを app.module に import。

https://angular.io/api/common/http/HttpClientModule

https://angular.io/api/common/http/HttpClientXsrfModule

サーバー側でCookie にセットした、Cookie名「XSFR-TOKEN」のCSRFトークンを、リクエストヘッダーにキー名「X-XSRF-TOKEN」として返すことを、HttpClientModule、HttpClientXsrfModule の宣言をするだけで自動でやってくれる。

上記のキー名を変更したい場合、imports セクションで、withOption に別名を指定する。下のコードではコメントアウトしている。

app.module

import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
@NgModule({
  declarations: [
     : 略
  ],
  imports: [
     : 略
    HttpClientModule,
    HttpClientXsrfModule, /*.withOptions({ cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN'})*/
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

1.2 サービスの実装

HttpClient を importし、リクエストを送信。とりあえず、今回は、CSRFを適用するリクエストは POSTのみ対応とするため、POSTで送信。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'

@Injectable()
export class HogeService {

  configureRequest(req: Request): void {
    
  }

  constructor(private http: HttpClient) { }

  hello(name: string){
    this.http.post('/api/hello',
      JSON.stringify({ params: { name: name } })
    ).subscribe(
      response => {
        console.log(response);
      },
      error => {
        console.log(error);
      }
    );
  }
}

Angular側は以上。

2.Flask

Flaskのコードスニペットを参考にCSRFを実装する。

http://flask.pocoo.org/snippets/3/

この例では、HTMLの hidden にキーを設定するのだが、Angular で生成したHTMLファイルを、Flaskのテンプレートとして、Flaskの変数を埋め込むのは難儀、かつ、上記Angularの戦略として、CookieにCSRFトークンを埋め込んで渡す。Cookieの作成のスニペットは以下。

http://flask.pocoo.org/snippets/30/

2.1 注意点など

app.secret_key = 'hogehoge'

session を使用するときに、secret_key に任意の値を設定しないとエラーとなる。

sessionには、最初に画面(index.html) を返す時に生成したCSRFトークンを格納する

redirect_ui_index_with_crsftoken()

Angularが生成した、index.html にリダイレクトし、Http Responseに、”XSRF-TOKEN"を設定して返す

name = request.json['params']['name']

application/json として送信されたデータは、Flask で、request.json として解析済み結果を取得できる。

def server_error(e)

共通のエラーハンドラー。@app.errorhandlerで対応するエラーコードを指定。

csrf_protect()

@app.before_request デコレーターで、リクエストの前処理。POSTの場合、sessionから、"XSRF-TOKEN"を取得。リクエストヘッダーとして送信された、”X-XSRF-TOKEN”の値と比較し、一致すればリクエストを受け付ける。

不一致の場合、不正アクセスとして403エラーとする。

generate_csrf_token()

sessionにCSRF_TOKENが含まれていない場合、生成(UUID)し格納。

import logging
import uuid

from flask import Flask, render_template, request, redirect, session, abort, jsonify

CSRF_TOKEN = '_csrf_token'

app = Flask(__name__, static_folder='app')
app.secret_key = 'hogehoge'

@app.route('/', methods=['GET'])
def redirect_ui_index_with_crsftoken():
    response = app.make_response(redirect("/hoge/index.html"))
    response.set_cookie('XSRF-TOKEN', value=generate_csrf_token()) 
    return response

@app.route('/api/hello', methods=['POST'])
def api_hello():
    name = request.json['params']['name']
    return jsonify({"result":{"name":name}})

@app.errorhandler(403)
@app.errorhandler(500)
def server_error(e):
    logging.exception(e)
    return 'an error occurred.', e.code

@app.before_request
def csrf_protect():
    if request.method == "POST":
        token = session[CSRF_TOKEN]
        if not token or token != request.headers.get('X-XSRF-TOKEN'):
            abort(403)

def generate_csrf_token():
    if CSRF_TOKEN not in session:
        session[CSRF_TOKEN] = str(uuid.uuid4())
    return session[CSRF_TOKEN]

以上