私が取り組んでいる新しいnode.jsプロジェクトでは、Cookieベースのセッションアプローチからの切り替えを考えています(つまり、ユーザーのブラウザでユーザーセッションを含むKey-ValueストアにIDを保存することを意味します)。 JSON Web Tokens(jwt)を使用したトークンベースのセッションアプローチ(Key-Valueストアなし)へのアプローチ。
プロジェクトはsocket.ioを利用するゲームです - トークンベースのセッションを持つことは単一セッション(webとsocket.io)に複数の通信チャンネルがあるようなシナリオで役に立つでしょう
Jwtアプローチを使用して、サーバーからトークン/セッションの無効化をどのように提供するのですか?
また、この種のパラダイムで注目すべき一般的な(または一般的でない)落とし穴や攻撃を理解したいと思いました。たとえば、このパラダイムがセッションストア/ Cookieベースのアプローチと同じ/異なる種類の攻撃に対して脆弱であるとします。
それで、私が次のものを持っていると言う( this と this から改作された)
セッションストアログイン:
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
// Create session token
var token= createSessionToken();
// Add to a key-value database
KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});
// The client should save this session token in a cookie
response.json({sessionToken: token});
});
}
トークンベースのログイン:
var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
var user = {username: request.body.username, password: request.body.password };
// Validate somehow
validate(user, function(isValid, profile) {
var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
response.json({token: token});
});
}
-
セッションストアアプローチのログアウト(または無効化)には、指定されたトークンを使用してKeyValueStoreデータベースを更新する必要があります。
トークン自体には通常Key-Valueストアに存在する情報が含まれているため、このようなメカニズムはトークンベースのアプローチには存在しないようです。
私もこの問題を研究してきました、そして、以下の考えのどれも完全な解決策ではありませんが、それらは他のものが考えを除外するのを助けるか、またはさらなる考えを提供するかもしれません。
1)単にクライアントからトークンを削除する
明らかにこれはサーバ側のセキュリティには何もしませんが、それは存在からトークンを削除することによって攻撃者を阻止します(すなわち彼らはログアウトの前にトークンを盗まれなければならないでしょう)。
2)トークンブラックリストを作成する
無効なトークンを最初の有効期限まで保存して、受信したリクエストと比較することができます。ただし、リクエストごとにデータベースにアクセスする必要があるため、これは最初の段階で完全にトークンベースになる理由を否定するようです。ただし、ログアウトと有効期限の間にあるトークンを格納するだけでよいため、ストレージサイズはもっと小さくなる可能性があります(これは直感であり、間違いなくコンテキストに依存します)。
3)トークンの有効期限を短くして頻繁に入れ替える
トークンの有効期限を十分な間隔で保ち、実行中のクライアントに必要に応じて更新を追跡して要求させると、1番が完全なログアウトシステムとして効果的に機能します。この方法の問題点は、クライアントのコードが閉じられるまでユーザーをログインさせ続けることが不可能になることです(有効期限をどれくらい長くするかによって異なります)。
危機管理計画
緊急事態が発生した場合、またはユーザートークンが危険にさらされた場合は、ユーザーが自分のログイン資格情報を使用して基になるユーザー検索IDを変更できるようにすることができます。関連付けられているユーザーが見つからなくなるため、これにより、関連付けられているすべてのトークンが無効になります。
また、最後のログイン日をトークンに含めることをお勧めします。そうすれば、しばらくしても再ログインを強制できるようになります。
トークンを使用した攻撃に関する類似点/相違点については、この投稿で質問に答えます。 http://blog.auth0.com/2014/01/07/angularjs-authentication-with-cookies-vs-token/
上記のアイデアは良いですが、既存のJWTをすべて無効にするための非常に単純で簡単な方法は、単に秘密を変更することです。
サーバーがJWTを作成し、それにシークレット(JWS)を付けてそれをクライアントに送信すると、単にシークレットを変更すると、既存のすべてのトークンが無効になり、サーバーに。
実際のトークンの内容(または検索ID)を変更する必要はありません。
明らかにこれはあなたがすべての既存のトークンを期限切れにしたいときに緊急の場合にのみうまくいきます。
これは主に長いコメントで、@ mattwayによる回答を支持し、その上に構築されています。
与えられた:
このページで提案されている他の解決策の中には、リクエストごとにデータストアをヒットすることを推奨するものがあります。すべての認証要求を検証するためにメインデータストアにアクセスした場合は、他の確立されたトークン認証メカニズムの代わりにJWTを使用する理由がわかりません。毎回データストアにアクセスする場合は、ステートレスではなく、JWTを本質的にステートフルにしました。
(あなたのサイトが大量の不正なリクエストを受信した場合、JWTはデータストアにアクセスせずにそれらを拒否します。これは役に立ちます。おそらくそのような他のユースケースがあります。)
与えられた:
ステートレスJWTには、次のような重要なユースケースに 即時 および 安全な サポートを提供する方法がないため、真のステートレスJWT認証は、一般的な現実のWebアプリケーションでは実現できません。
ユーザーのアカウントが削除/ブロック/一時停止されています。
ユーザーのパスワードが変更されました。
ユーザーの役割または権限が変更されました。
ユーザーは管理者によってログアウトされています。
JWTトークン内の他のアプリケーションの重要データは、サイト管理者によって変更されます。
このような場合、トークンの有効期限を待つことはできません。トークンの無効化はただちに発生する必要があります。また、悪意を持っていてもいなくても、古いトークンのコピーを保持して使用しないようにクライアントを信頼することはできません。
そのため、@ matt-wayからの回答、#2 TokenBlackListが、JWTベースの認証に必要な状態を追加する最も効率的な方法になると思います。
有効期限が切れるまでこれらのトークンを保持するブラックリストがあります。有効期限が切れるまでブラックリストに登録されたトークンを保存するだけでよいため、トークンのリストはユーザーの総数に比べてかなり少なくなります。無効化されたトークンをredis、memcached、またはキーの有効期限の設定をサポートする他のメモリ内データストアに配置することで実装します。
最初のJWT認証を通過するすべての認証要求に対して、メモリ内のdbを呼び出す必要がありますが、そこにユーザーのセット全体のキーを格納する必要はありません。 (これは特定のサイトにとって大したことではないかもしれません。)
私はjwtのバージョン番号をユーザーモデルに記録しておくでしょう。新しいjwtトークンはそれらのバージョンをこれに設定します。
Jwtを検証するときには、ユーザーの現在のjwtバージョンと同じバージョン番号であることを確認してください。
古いjwtsを無効にしたいときはいつでも、単にユーザjwtのバージョン番号を上げてください。
まだ試していませんが、他のいくつかの回答に基づいて多くの情報を使用しています。ここでの複雑さは、ユーザー情報の要求ごとにサーバー側のデータストア呼び出しを回避することです。他のほとんどのソリューションでは、ユーザーセッションストアへの要求ごとにdbルックアップが必要です。特定のシナリオではこれで問題ありませんが、これはそのような呼び出しを回避し、必要なサーバー側の状態を非常に小さくするために作成されました。 サーバー側のセッションを再作成することになりますが、すべての強制無効化機能を提供するには小さすぎます。あなたがそれをしたいのであればここで要旨です:
目標:
解決策:
これには、ユーザーテーブルに禁止されたユーザー情報が含まれていると仮定して、サーバー上でブラックリスト(状態)を管理する必要があります。無効なセッションブラックリスト - ユーザーIDのリストです。このブラックリストは、リフレッシュトークン要求中にのみチェックされます。更新トークンTTLである限り、エントリーはその上に存在する必要があります。更新トークンが期限切れになると、ユーザーはログインし直す必要があります。
短所:
長所:
このソリューションでは、Reddisのようなインメモリデータストアは必要ありません。少なくとも15分ごとにサーバーがdb呼び出しを行うだけなので、ユーザー情報は不要です。 reddisを使用している場合、そこに有効/無効なセッションリストを保存することは非常に速くて簡単な解決策でしょう。更新トークンは必要ありません。各認証トークンはセッションIDとデバイスIDを持ち、作成時にはReddisテーブルに格納され、適切な場合は無効化されます。それからそれらはすべてのリクエストでチェックされ、無効なときは拒否されます。
私が検討してきたアプローチは、JWTで常にiat
(issu at)値を持つことです。その後、ユーザーがログアウトしたら、そのタイムスタンプをユーザーレコードに保存します。 JWTを検証するときは、iat
を最後にログアウトしたタイムスタンプと比較するだけです。 iat
が古ければ、それは無効です。はい、あなたはDBに行かなければなりませんが、JWTが有効であれば私はとにかくユーザーレコードを引っ張ります。
私の考えている主な欠点は、複数のブラウザを使用している場合、またはモバイルクライアントを使用している場合は、すべてのセッションからログアウトすることです。
これは、システム内のすべてのJWTを無効にするための優れたメカニズムにもなります。チェックの一部は、最後の有効なiat
時刻のグローバルタイムスタンプに対するものかもしれません。
私はここで少し遅れていますが、私はまともな解決策を持っていると思います。
データベースに "last_password_change"列があり、パスワードが最後に変更された日時が格納されています。発行日時もJWTに格納しています。トークンを検証するときは、トークンが発行された後にパスワードが変更されていて、まだ期限切れになっていなくてもトークンが拒否されたかどうかを確認します。
あなたのDBの "last_key_used"フィールドをあなたのユーザーの文書/レコードに置くことができます。
ユーザーがuserでログインしてパスしたら、新しいランダムな文字列を生成し、それをlast_key_usedフィールドに格納し、トークンに署名するときにペイロードに追加します。
ユーザーがトークンを使用してログインしたら、トークン内のものと一致するようにDB内のlast_key_usedを確認します。
次に、ユーザーがたとえばログアウトをしたとき、またはトークンを無効にしたい場合は、その "last_key_used"フィールドを別のランダムな値に変更するだけで、以降のチェックはすべて失敗します。
パーティーに遅れて、私の2セントはいくつかの研究の後に下に与えられています。ログアウト中に、次のことが起こっていることを確認してください。
クライアントストレージ/セッションをクリアする
ログインまたはログアウトが発生するたびに、ユーザーテーブルの最終ログイン日時およびログアウト日時を更新します。そのため、ログイン日時は常にログアウト日時よりも長くする必要があります(または、現在のステータスがloginで、まだログアウトされていない場合は、ログアウト日時をnullにしてください)
これは、ブラックリストの追加表を保持して定期的にパージするよりもはるかに簡単です。複数のデバイスをサポートするには、OSまたはクライアントの詳細など、ログアウト日、ログアウト日を保存するための追加のテーブルが必要です。
トークンを検証するには、まずトークンの有効期限をチェックし、次にトークンが期限切れでない場合はブラックリストをチェックします。
セッションのニーズが長い場合は、トークンの有効期限を延長するためのメカニズムが必要です。
このようなメモリ内のリストを保管してください
user_id revoke_tokens_issued_before
-------------------------------------
123 2018-07-02T15:55:33
567 2018-07-01T12:34:21
トークンが1週間で期限切れになる場合は、それより古いレコードを消去するか無視してください。また、各ユーザーの最新の記録のみを保管してください。リストのサイズは、トークンを保持する期間と、ユーザーが自分のトークンを取り消す頻度によって異なります。テーブルが変更された場合にのみdbを使用してください。アプリケーションの起動時にメモリにテーブルをロードします。
「すべてのデバイスからログアウトする」オプションが許容される場合(ほとんどの場合それはそれです):
いずれにしても、ほとんどの場合、ユーザーレコードを取得するためのdbトリップが必要になるので、これによって検証プロセスに大きなオーバーヘッドがかかることはありません。ブラックリストを維持するのとは異なり、結合や別の呼び出しを使用したり、古いレコードを削除したりする必要があるため、DBの負荷が大きくなります。
ユーザートークンを無効にしたい場合は、DB上で発行されたすべてのトークンを追跡し、それらがセッションのようなテーブルで有効かどうかを確認できます。欠点は、リクエストごとにDBにアクセスすることです。
試したことはありませんが、DBのヒットを最小限に抑えながらトークンの失効を許可するには、次の方法をお勧めします -
データベースチェック率を下げるために、いくつかの決定論的な関連付けに従って、発行されたすべてのJWTトークンをX個のグループに分割します(たとえば、ユーザーIDの最初の数字で10個のグループ)。
各JWTトークンは、グループ作成時に作成されたグループIDとタイムスタンプを保持します。例:{ "group_id": 1, "timestamp": 1551861473716 }
サーバーはすべてのグループIDをメモリに保持し、各グループにはそのグループに属するユーザーの最後のログアウトイベントがいつ発生したかを示すタイムスタンプが付けられます。例:{ "group1": 1551861473714, "group2": 1551861487293, ... }
古いグループタイムスタンプを持つJWTトークンを持つ要求は、有効性(DBヒット)についてチェックされ、有効な場合は、クライアントが将来使用するために新しいタイムスタンプを持つ新しいJWTトークンが発行されます。トークンのグループタイムスタンプが新しい場合は、JWTを信頼します(DBヒットなし)。
そう -
単にjtiクレーム(nonce)を使用し、それをユーザレコードフィールドとしてリストに格納してはいけません(dbに依存しますが、少なくともカンマ区切りのリストで問題ありません)。他のユーザがおそらくユーザレコードを取得したいと指摘しているので、個別に検索する必要はありません。これにより、さまざまなクライアントインスタンスに対して複数の有効なトークンを設定できます( "logout everywhere"でリストを空にリセットできます)。
私はそれを次のようにしました:
unique hash
を生成し、それを redis とあなたの _ jwt _ に保存します。これは session と呼ぶことができます。そのため、ユーザーがログインすると、一意のハッシュが作成され、redisに格納されて _ jwt _ に挿入されます。
ユーザーが保護されたエンドポイントにアクセスしようとすると、 _ jwt _ から一意のセッションハッシュを取得し、redisを照会してそれが一致するかどうかを確認します。
これを拡張して、 _ jwt _ をさらに安全にすることができます。
_ x _ が特定の _ jwt _ を要求するたびに、新しい一意のセッションを生成し、それを _ jwt _ に格納してから、ブラックリストに追加します。前回のもの。
これは、 _ jwt _ が絶えず変化し、古くなっている _ jwt _ がハッキングされたり、盗まれたり、その他のことを阻止されたことを意味します。