OAuth2リフレッシュトークンで記憶する
1. 概要
この記事では、OAuth 2リフレッシュトークンを利用して、OAuth 2で保護されたアプリケーションに「Remember Me」機能を追加します。
この記事は、AngularJSクライアントを介してアクセスされるSpring REST APIを保護するためにOAuth 2を使用するシリーズの続きです。 承認サーバー、リソースサーバー、およびフロントエンドクライアントの設定については、the introductory articleに従うことができます。
次に、Zuulプロキシを使用してhandling the refresh tokenに関する記事を続けることができます。
2. OAuth2アクセストークンと更新トークン
まず、OAuth 2トークンとその使用方法について簡単に要約してみましょう。
password許可タイプを使用した最初の認証試行で、ユーザーは有効なユーザー名とパスワード、およびクライアントIDとシークレットを送信する必要があります。 認証要求が成功すると、サーバーは次の形式の応答を送り返します。
{
"access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
"token_type": "bearer",
"refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
"expires_in": 59,
"scope": "read write",
}
サーバーレスポンスには、アクセストークンと更新トークンの両方が含まれていることがわかります。 アクセストークンは、認証を必要とする後続のAPI呼び出しに使用されますが、the purpose of the refresh token is to obtain a new valid access tokenまたは単に前のAPI呼び出しを取り消します。
refresh_token付与タイプを使用して新しいアクセストークンを受信するために、ユーザーは資格情報を入力する必要がなくなり、クライアントID、シークレット、そしてもちろん更新トークンのみを入力する必要があります。
The goal of using two types of tokens is to enhance user security.通常、アクセストークンの有効期間は短いため、攻撃者がアクセストークンを取得した場合、そのトークンを使用する時間は限られています。 一方、リフレッシュトークンが危険にさらされた場合、クライアントIDとシークレットも必要になるため、これは役に立ちません。
リフレッシュトークンのもう1つの利点は、アクセストークンを取り消すことができ、ユーザーが新しいIPからログインするなどの異常な動作を表示した場合、別のトークンを送り返さないことです。
3. 更新トークンを使用したRemember-Me機能
ユーザーは通常、アプリケーションにアクセスするたびに資格情報を入力する必要がないため、セッションを保持するオプションがあると便利です。
アクセストークンの有効期間は短いため、代わりに更新トークンを使用して新しいアクセストークンを生成し、アクセストークンの有効期限が切れるたびにユーザーに資格情報を要求する必要がなくなります。
次のセクションでは、この機能を実装する2つの方法について説明します。
-
まず、401ステータスコードを返すユーザーリクエストをインターセプトします。これは、アクセストークンが無効であることを意味します。 これが発生した場合、ユーザーが[remember me]オプションをオンにすると、refresh_token付与タイプを使用して新しいアクセストークンのリクエストが自動的に発行され、最初のリクエストが再度実行されます。
-
次に、アクセストークンを事前に更新できます。トークンの有効期限が切れる数秒前に、トークンを更新するリクエストを送信します。
2番目のオプションには、ユーザーのリクエストが遅れないという利点があります。
4. 更新トークンの保存
前回の記事では、OAuthサーバーへのリクエストをインターセプトし、認証時に返送された更新トークンを抽出してサーバー側のCookieに保存するCustomPostZuulFilterを追加しました。
@Component
public class CustomPostZuulFilter extends ZuulFilter {
@Override
public Object run() {
//...
Cookie cookie = new Cookie("refreshToken", refreshToken);
cookie.setHttpOnly(true);
cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
cookie.setMaxAge(2592000); // 30 days
ctx.getResponse().addCookie(cookie);
//...
}
}
次に、loginData.remember変数にデータバインディングするチェックボックスをログインフォームに追加しましょう。
ログインフォームに追加のチェックボックスが表示されます。
loginDataオブジェクトは認証要求とともに送信されるため、rememberパラメータが含まれます。 認証要求が送信される前に、パラメーターに基づいてrememberという名前のCookieを設定します。
function obtainAccessToken(params){
if (params.username != null){
if (params.remember != null){
$cookies.put("remember","yes");
}
else {
$cookies.remove("remember");
}
}
//...
}
結果として、このCookieをチェックして、ユーザーが記憶を希望するかどうかに応じて、アクセストークンの更新を試みる必要があるかどうかを判断します。
5. 401応答をインターセプトしてトークンを更新する
401応答で返されるリクエストをインターセプトするには、AngularJSアプリケーションを変更して、responseError関数を持つインターセプターを追加しましょう。
app.factory('rememberMeInterceptor', ['$q', '$injector', '$httpParamSerializer',
function($q, $injector, $httpParamSerializer) {
var interceptor = {
responseError: function(response) {
if (response.status == 401){
// refresh access token
// make the backend call again and chain the request
return deferred.promise.then(function() {
return $http(response.config);
});
}
return $q.reject(response);
}
};
return interceptor;
}]);
この関数は、ステータスが401かどうかを確認します。これは、アクセストークンが無効であることを意味し、有効な場合は、新しい有効なアクセストークンを取得するためにリフレッシュトークンを使用しようとします。
これが成功した場合、関数は401エラーになった初期要求を再試行し続けます。 これにより、ユーザーのシームレスなエクスペリエンスが保証されます。
アクセストークンを更新するプロセスを詳しく見てみましょう。 まず、必要な変数を初期化します。
var $http = $injector.get('$http');
var $cookies = $injector.get('$cookies');
var deferred = $q.defer();
var refreshData = {grant_type:"refresh_token"};
var req = {
method: 'POST',
url: "oauth/token",
headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
data: $httpParamSerializer(refreshData)
}
パラメータgrant_type=refresh_tokenを使用して/ oauth / tokenエンドポイントにPOSTリクエストを送信するために使用するreq変数を確認できます。
次に、挿入した$httpモジュールを使用してリクエストを送信しましょう。 リクエストが成功すると、新しいアクセストークン値とaccess_token Cookieの新しい値を使用して新しいAuthenticationヘッダーを設定します。 リクエストが失敗した場合、リフレッシュトークンも最終的に期限切れになる可能性があり、ユーザーはログインページにリダイレクトされます。
$http(req).then(
function(data){
$http.defaults.headers.common.Authorization= 'Bearer ' + data.data.access_token;
var expireDate = new Date (new Date().getTime() + (1000 * data.data.expires_in));
$cookies.put("access_token", data.data.access_token, {'expires': expireDate});
window.location.href="index";
},function(){
console.log("error");
$cookies.remove("access_token");
window.location.href = "login";
}
);
更新トークンは、前の記事で実装したCustomPreZuulFilterによってリクエストに追加されます。
@Component
public class CustomPreZuulFilter extends ZuulFilter {
@Override
public Object run() {
//...
String refreshToken = extractRefreshToken(req);
if (refreshToken != null) {
Map param = new HashMap();
param.put("refresh_token", new String[] { refreshToken });
param.put("grant_type", new String[] { "refresh_token" });
ctx.setRequest(new CustomHttpServletRequest(req, param));
}
//...
}
}
インターセプターを定義することに加えて、それを$httpProviderに登録する必要があります。
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('rememberMeInterceptor');
}]);
6. トークンをプロアクティブに更新する
「remember-me」機能を実装する別の方法は、現在のトークンが期限切れになる前に新しいアクセストークンを要求することです。
アクセストークンを受信すると、JSON応答には、トークンが有効になる秒数を指定するexpires_in値が含まれます。
この値を認証ごとにCookieに保存しましょう。
$cookies.put("validity", data.data.expires_in);
次に、更新リクエストを送信するために、AngularJS $timeoutサービスを使用して、トークンの有効期限が切れる10秒前に更新呼び出しをスケジュールします。
if ($cookies.get("remember") == "yes"){
var validity = $cookies.get("validity");
if (validity >10) validity -= 10;
$timeout( function(){ $scope.refreshAccessToken(); }, validity * 1000);
}
7. 結論
このチュートリアルでは、OAuth2アプリケーションとAngularJSフロントエンドを使用して「RememberMe」機能を実装する2つの方法について説明しました。
例の完全なソースコードはover on GitHubにあります。 URL/login_rememberで「rememberme」機能を使用してログインページにアクセスできます。