私たちの新しいREST APIにJWTベースの認証を実装したいです。しかし、有効期限がトークンに設定されているので、自動的に延長することは可能ですか?その間にユーザーがアプリケーションを積極的に使用していたとしても、X分ごとにサインインする必要はありません。それは巨大なUXの失敗になるでしょう。
しかし、有効期限を延長すると新しいトークンが作成されます(そして古いトークンは有効期限が切れるまで有効です)。そして、それぞれのリクエストの後に新しいトークンを生成することは私にとってばかげて聞こえます。複数のトークンが同時に有効な場合、セキュリティ上の問題があるように思われます。もちろん、ブラックリストを使って古いものを無効にすることもできますが、トークンを保存する必要があります。そしてJWTの利点の1つはストレージがないことです。
私はAuth0がそれをどのように解決したかを見つけました。彼らはJWTトークンだけでなく更新トークンも使います: https://docs.auth0.com/refresh-token
ただし、これを実装するには(Auth0なしで)、リフレッシュトークンを保存して有効期限を維持する必要があります。それでは本当の利点は何ですか?トークンを1つだけ(JWT以外)にして、有効期限をサーバーに保持しないのはなぜですか。
他の選択肢はありますか? JWTの使用はこのシナリオには適していませんか?
私はAuth0で働いており、リフレッシュトークン機能の設計に携わっていました。
それはすべてアプリケーションの種類に依存しており、これが私たちの推奨するアプローチです。
有効なパターンは、期限が切れる前にトークンを更新することです。
トークンの有効期限を1週間に設定し、ユーザーがWebアプリケーションを開くたびに1時間ごとにトークンを更新します。ユーザーが1週間以上アプリケーションを開かないと、再度ログインする必要があります。これは許容されるWebアプリケーションUXです。
トークンを更新するには、APIに有効期限が切れていないJWTを受け取り、新しい有効期限フィールドで同じ署名付きJWTを返す新しいエンドポイントが必要です。その後、Webアプリケーションはトークンをどこかに保存します。
ほとんどのネイティブアプリケーションは一度だけログインします。
その考え方は、更新トークンが期限切れになることはなく、常に有効なJWTと交換できることです。
期限切れにならないトークンの問題は、 never がneverを意味することです。携帯を紛失した場合はどうしますか。そのため、ユーザーが何らかの形で識別できる必要があり、アプリケーションはアクセスを取り消す方法を提供する必要があります。デバイスの名前を使用することにしました。 「マリオのiPad」。その後、ユーザーはアプリケーションにアクセスして「maryo's iPad」へのアクセスを取り消すことができます。
別の方法は、特定のイベントで更新トークンを無効にすることです。面白いイベントはパスワードを変更することです。
JWTはこれらのユースケースには役に立ちませんので、ランダムに生成された文字列を使用し、それを私たちの側に保存します。
自分で認証を処理する場合(つまり、Auth0のようなプロバイダを使用しないでください)、次のようになります。
データベースバックエンドの「reauth」フラグは、たとえばユーザーがパスワードをリセットしたときに設定されます。ユーザーが次回ログインしたときにフラグが削除されます。
さらに、ユーザーが少なくとも72時間に1回ログインする必要があるというポリシーがあるとしましょう。その場合、APIトークン更新ロジックは、ユーザーデータベースからのユーザーの最終ログイン日も確認し、それに基づいてトークンの更新を拒否または許可します。
バックエンドに追加の安全な記憶域を持たずにJWTを無効にするための別の解決策は、usersテーブルに新しいjwt_version
integer列を実装することです。ユーザーが既存のトークンをログアウトまたは期限切れにする場合は、単にjwt_version
フィールドを増分します。
新しいJWTを生成するとき、jwt_version
をJWTペイロードにエンコードします。新しいJWTが他のすべてを置き換える必要がある場合は、オプションで事前に値を増やします。
JWTを検証するときに、jwt_version
フィールドがuser_id
と一緒に比較され、一致する場合にのみ許可が付与されます。
バックエンドにRESTful APIを使用してアプリケーションをHTML 5に移行するとき、私はいじっていました。私が思いついた解決策は次のとおりです。
ご覧のとおり、これにより頻繁な更新トークン要求が減ります。トークンの更新呼び出しがトリガーされる前にユーザーがブラウザー/アプリを閉じると、前のトークンは期限内に期限切れになり、ユーザーは再ログインする必要があります。
ユーザの非アクティブ状態に対処するために、より複雑な戦略を実施することができる(例えば開かれたブラウザタブを無視した)。その場合、トークンの更新呼び出しには、定義されたセッション時間を超えてはならないと予想される期限切れ時間を含める必要があります。それに応じて、アプリケーションは最後のユーザー操作を追跡する必要があります。
私は長い有効期限を設定するという考えが嫌いなので、このアプローチはそれほど頻繁に認証を必要としないネイティブアプリケーションではうまく機能しないかもしれません。
良い質問です。質問自体には豊富な情報があります。
トークンの更新:それらをいつ使用するか、およびそれらがJWTとどのように相互作用するか このシナリオのためのよい考えを与えます。いくつかのポイントがあります: -
auth0/angular-jwt angularjsも見てください。
Web APIの場合read ASP .NET Web API 2、およびOwinを使用してAngularJS AppでOAuth Refresh Tokenを有効にする
もしあなたがノード(React/Redux/Universal JS)を使っているなら、npm i -S jwt-autorefresh
をインストールすることができます。
このライブラリーは、(トークンにエンコードされたexpクレームに基づいて)アクセス・トークンが期限切れになる前に、ユーザーが計算した秒数でJWTトークンのリフレッシュをスケジュールします。それは広範囲のテストスイートを持ち、奇妙な活動があなたの環境からの誤設定に関する説明的なメッセージを伴うことを確実にするためにかなりの数の条件をチェックします。
完全なサンプル実装
import autorefresh from 'jwt-autorefresh'
/** Events in your app that are triggered when your user becomes authorized or deauthorized. */
import { onAuthorize, onDeauthorize } from './events'
/** Your refresh token mechanism, returning a promise that resolves to the new access tokenFunction (library does not care about your method of persisting tokens) */
const refresh = () => {
const init = { method: 'POST'
, headers: { 'Content-Type': `application/x-www-form-urlencoded` }
, body: `refresh_token=${localStorage.refresh_token}&grant_type=refresh_token`
}
return fetch('/oauth/token', init)
.then(res => res.json())
.then(({ token_type, access_token, expires_in, refresh_token }) => {
localStorage.access_token = access_token
localStorage.refresh_token = refresh_token
return access_token
})
}
/** You supply a leadSeconds number or function that generates a number of seconds that the refresh should occur prior to the access token expiring */
const leadSeconds = () => {
/** Generate random additional seconds (up to 30 in this case) to append to the lead time to ensure multiple clients dont schedule simultaneous refresh */
const jitter = Math.floor(Math.random() * 30)
/** Schedule autorefresh to occur 60 to 90 seconds prior to token expiration */
return 60 + jitter
}
let start = autorefresh({ refresh, leadSeconds })
let cancel = () => {}
onAuthorize(access_token => {
cancel()
cancel = start(access_token)
})
onDeauthorize(() => cancel())
免責事項:私はメンテナです
私は実際にこれをPHPに実装し、API用のクライアントライブラリを作成するためにGuzzleクライアントを使用しましたが、この概念は他のプラットフォームでも機能するはずです。
基本的に、私は2つのトークンを発行します。1つは短い(5分)もの、もう1つは1週間後に期限切れになるものです。クライアントライブラリは、ミドルウェアを使用して、何らかの要求に対して401の応答を受け取った場合、ショートトークンのリフレッシュを1回試行します。その後、元の要求を再試行し、更新できた場合は正しい応答をユーザーには透過的に取得します。失敗した場合は、401をユーザーに送信します。
ショートトークンの有効期限が切れても認証され、ロングトークンが有効で認証されている場合は、ロングトークンが認証するサービス上の特別なエンドポイントを使用してショートトークンが更新されます。その後、短いトークンを使用して新しい長いトークンを取得し、それによって短いトークンを更新するたびにさらに1週間延長します。
この方法でも、最大5分以内にアクセスを取り消すことができます。これは、トークンのブラックリストを保存しなくても、使用に適しています。
レイト編集:頭の中が新鮮になった今月の記事をもう一度読むと、短いトークンを更新するとアクセスが取り消される可能性があることを指摘しておく必要があります。サービスへの1回の呼び出しごとに料金を支払うことなく禁止されています)。
このアプローチはどうでしょうか。
この場合、トークンを更新するための追加のエンドポイントは必要ありません。どんなfeedackにも感謝します。
トークンデータに変数を追加することで、この問題を解決しました。
softexp - I set this to 5 mins (300 seconds)
expiresIn
オプションを希望の時間に設定してから、ユーザーに再度ログインを強制します。鉱山は30分に設定されています。これはsoftexp
の値より大きくなければなりません。
私のクライアントサイドアプリがサーバーAPI(顧客リストページなどのトークンが必要な場合)にリクエストを送信すると、サーバーは送信されたトークンがまだ有効かどうかを元の有効期限(expiresIn
)の値に基づいてチェックします。有効でない場合、サーバーはこのエラーに特有のステータスで応答します。 INVALID_TOKEN
。
トークンがexpiredIn
値に基づいてまだ有効であるが、それが既にsoftexp
値を超えている場合、サーバーはこのエラーに対して別のステータスで応答します。 EXPIRED_TOKEN
:
(Math.floor(Date.now() / 1000) > decoded.softexp)
クライアントサイドでは、もしそれがEXPIRED_TOKEN
レスポンスを受け取ったら、それは更新要求をサーバに送ることによって自動的にトークンを更新するべきです。これはユーザーには見えず、自動的にクライアントアプリの面倒を見ます。
サーバーの更新メソッドは、トークンがまだ有効かどうかを確認する必要があります。
jwt.verify(token, secret, (err, decoded) => {})
上記の方法に失敗した場合、サーバーはトークンの更新を拒否します。
JWTアクセストークンを無効にする手順は次のとおりです。
1)ログインしたら、クライアントに2つのトークン(アクセストークン、リフレッシュトークン)を送信します。
2)アクセストークンの有効期限が短くなり、更新の有効期限が長くなります。
3)クライアント(フロントエンド)は自分のローカルストレージにリフレッシュトークンを保存し、クッキーにアクセストークンを保存します。
4)クライアントはapisを呼び出すためにアクセストークンを使用します。しかし、期限切れになったら、ローカルストレージからリフレッシュトークンを選び、auth server apiを呼び出して新しいトークンを取得します。
5)あなたの認証サーバーは更新トークンを受け入れてその有効性をチェックし新しいアクセストークンを返すAPIを公開します。
6)更新トークンが期限切れになると、ユーザーはログアウトされます。
あなたがより多くの詳細が必要であれば私に知らせてください、私は同様にコード(Java + Springブート)を共有することができます。