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]
以上
