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