ちょっとでもセキュリティに自信がないなら、 Firebase Authentication を検討しよう
(※ こちらの参照記事の内容自体に不備があるとか甘いとか指摘するものではないんですが、勝手に枕として使わせてもらいます)
上記記事は、Firebase Authenticationが提供するJavaScript APIを使ってJWTのトークンを取得し、自前のサーバにHTTPのヘッダで送りつけて検証をさせることで、認証の仕組みをセキュアかつかんたんに実現しよう、という内容です。
このようにJavaScriptのAPIでトークンを発行して自前バックエンドのAPI認証につかう方法はAuth0のSDKなどでも行われていますので、IDaaSをつかってSPAを開発する場合には一般的なのかもしれません。
話は変わりますが、SPAの開発に携わっている方は「localStorageにはセッション用のトークンは保存するな」という主張を聞いた方も多いかと思います。これは、もしXSSがあったときに容易にそのトークンを盗むことができる、という理由によるものです。
ここで主張したいのは
「JavaScriptのメモリ内でトークンを取り扱うことは、localStorageにトークンを保管することと、XSS時のリスクという意味ではあまり変わらない」
ということです。
JavaScriptからHTTPでAPIをコールする際には、ブラウザ環境下ではXMLHttpRequest
あるいはfetch
などのビルトインのオブジェクト/関数が最終的に使われますが、XSSがあることを想定した環境ではこれらの上書きは簡単にできます。
var _XMLHttpRequest_setRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
window.XMLHttpRequest.prototype.setRequestHeader = function() {
// ...
// argumentsに`Authorization: Bearer ...`が渡ってきたら値をログする処理を記述
// ...
return _XMLHttpRequest_setRequestHeader.apply(this, arguments);
};
つまり、JavaScriptでAPIのセッション用のトークンを取り扱うということは、XSSがあることを想定した環境下ではトークンの盗窃に無防備です。
もしあなたが「localStorageにはセッション用のトークンは保存するな」という主張を正しいとしたときには、同じ理由でFirebase AuthのようなJavaScriptでトークン取得してサーバに送りつけるやり方は推奨されない、ということになります。逆に、もしあなたが上記参照記事の内容を使えると判断したのであれば、localStorageでのトークン保管は許容されるべきと判断したのと同等だということは認識しておくべきかと思います。
以下は蛇足です
個人的には、XSSがあることを想定した対策として、APIアクセスのためのトークンをlocalStorageに入れない、という判断については過剰ではないか、と考えています。特にオンライン攻撃(ユーザがブラウザを開いている間の攻撃)に対してはHttpOnlyのCookieも実質意味がないことはわかっています。
なので焦点はオフライン攻撃をオンラインと比べて脅威ととるのかあるいは逆にそうでないのかに関しての議論になりますが、これはそのアプリケーションの特性を無視して常に断定できるものではないのでは、と見ています。
蛇足その2
緩和策として、トークン取得からHTTPでのAPIコールに関わる処理をWorkerに分離する、などのアイディアもあります。複雑度は上がりますが、非現実的かと言われるとそうではないでしょう。そのWorkerではXSS(ライブラリ汚染のようなものも含めます)がないことを想定してるので前提からしてちゃぶ台返しですが、最小限にしてアプリ全体からは分離してあることで対策を集中できるのがポイントともいえます。
蛇足その3
そもそもFirebase Authの場合は、トークンを発行する元になる認証情報はデフォルトでindexedDBに入れている、という話もあります。indexedDBはlocalStorageと同様ですので、XSS環境下では同じように読み取り可能です。
Auth0 SPA SDKも、以前は別ドメインのiframeで認証情報を保持して、期間の短いトークンをSDKのAPIから都度払い出す形で頑張っていたように記憶していますが、ITP2の影響なのか、しぶしぶlocalStorageで保持するオプションも設けているようです。
https://auth0.com/docs/libraries/auth0-single-page-app-sdk#change-storage-options
https://auth0.com/docs/tokens/token-storage#browser-local-storage-scenarios
追記(2020/8/19)
@tkudos からタレコミあった件
.@stomita 先生にはついでに https://t.co/FSuVTQ0Opr の解説おねがいしたいです
— Tatsuo Kudo (@tkudos) August 19, 2020
oauth-worker は、OAuth2.0 のアクセストークン取得/リフレッシュトークンによる更新を、SPAにおいても3rd party JSやXSSによる脅威を回避してセキュアにやるためのもの、というざっくりとした理解です。考え方としては蛇足その2で指摘したアイディアの実装と考えてもいいです。トークンの取得や利用はWorker内に閉じさせることで、Workerのバウンダリを経由しないとトークン付きのAPI利用はできないようにしています。
残念な点としては(localStorageなどに依存せず)Web Workerのみでやってしまっているので、タブを閉じるとrefresh tokenが失われる(再びAuthorizationのフローを行う必要がある)ということかなと。READMEではこれで特に問題ないといっていますが、実際に使い物になるのかどうかは少し疑問です。同じくREADME内にService Workerで同様のことをやる試みおよび実装があるという記述がありますが、逆にService Workerであればrefresh tokenは永続利用できるとあり、それであればそっちのアプローチのほうが見込みがあったりはしないでしょうか(詳しく見ていない)
あと、APIの呼び出しはfetch API互換のインターフェースにはなっていますが、必ずすべてのAPIアクセスがこのfacadeオブジェクトを介するというのはアプリケーション開発者にとってはまあまあの規約です。グローバルのfetchを置き換えするやり方が乱暴ながら手っ取り早そうですが、その場合は3rd Party JSやXSSからの濫用に対する保護は不可能になります。
最後に、正規のスクリプト以外(3rd Party JSやXSSで投入されたスクリプト)が保護されたAPIリソースを利用できないようにWorkerへのリファレンスはクロージャで隠蔽しているようですが、結局facadeオブジェクトの利用に対する保護責任をアプリケーション開発者に丸投げするという話であり、開発者への負荷的にこれはどうなのでしょうね。オンライン状態で攻撃されたときの保護は無理筋と割り切って考えたほうが無難な気もします。