Angular + HttpClientXsrfModule + Flask で CSRF
Angular と Flaskを使って、CSRFを実現しようとしたのだが、若干手数がかかったのでメモ。
1.Angular + HttpClientXsrfModule
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]
以上