OAuth2リフレッシュトークンで私を覚えてください(SpringSecurityOAuthレガシースタックを使用)
1. 概要
この記事では、OAuth 2更新トークンを利用して、OAuth2で保護されたアプリケーションに「RememberMe」機能を追加します。
この記事は、AngularJSクライアントを介してアクセスされるSpringRESTAPIを保護するためのOAuth2の使用に関するシリーズの続きです。 承認サーバー、リソースサーバー、およびフロントエンドクライアントの設定については、紹介記事に従ってください。
注:この記事ではSpringOAuthレガシープロジェクトを使用しています。
2. OAuth2アクセストークンと更新トークン
まず、 OAuth2トークンとその使用方法について簡単にまとめてみましょう。
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呼び出しに使用されますが、更新トークンの目的は、新しい有効なアクセストークンを取得するか、前のトークンを取り消すことです。
refresh_token 付与タイプを使用して新しいアクセストークンを受信するために、ユーザーは資格情報を入力する必要がなくなり、クライアントID、シークレット、そしてもちろん更新トークンのみを入力する必要があります。
2種類のトークンを使用する目的は、ユーザーのセキュリティを強化することです。通常、アクセストークンの有効期間は短いため、攻撃者がアクセストークンを取得した場合、使用できる時間は限られています。 一方、更新トークンが危険にさらされている場合、クライアントIDとシークレットも必要になるため、これは役に立ちません。
更新トークンのもう1つの利点は、アクセストークンを取り消すことができ、ユーザーが新しいIPからのログインなどの異常な動作を示した場合に、別のトークンを返送しないことです。
3. 更新トークンを使用したRemember-Me機能
ユーザーは通常、アプリケーションにアクセスするたびに資格情報を入力する必要がないため、セッションを保持するオプションがあると便利です。
アクセストークンの有効期間は短いため、代わりに更新トークンを使用して新しいアクセストークンを生成し、アクセストークンの有効期限が切れるたびにユーザーに資格情報を要求する必要をなくすことができます。
次のセクションでは、この機能を実装する2つの方法について説明します。
- まず、401ステータスコードを返すユーザーリクエストをインターセプトします。これは、アクセストークンが無効であることを意味します。 これが発生した場合、ユーザーが[remember me]オプションをオンにすると、 refresh_token 付与タイプを使用して新しいアクセストークンのリクエストが自動的に発行され、最初のリクエストが再度実行されます。
- 次に、アクセストークンを事前に更新できます。トークンの有効期限が切れる数秒前に、トークンを更新するリクエストを送信します。
2番目のオプションには、ユーザーの要求が遅れないという利点があります。
4. 更新トークンの保存
更新トークンに関する以前の記事で、 CustomPostZuulFilter を追加しました。これは、 OAuth サーバーへのリクエストをインターセプトし、認証時に返送された更新トークンを抽出して保存します。サーバー側のCookie:
@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変数にデータバインディングするチェックボックスをログインフォームに追加しましょう。
<input type="checkbox" ng-model="loginData.remember" id="remember"/>
<label for="remember">Remeber me</label>
ログインフォームに追加のチェックボックスが表示されます。
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モジュールを使用してリクエストを送信しましょう。 リクエストが成功すると、新しい Authentication ヘッダーに、新しいアクセストークン値と access_tokenCookieの新しい値が設定されます。 要求が失敗した場合(これは、更新トークンも最終的に期限切れになる場合に発生する可能性があります)、ユーザーはログインページにリダイレクトされます。
$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<String, String[]> param = new HashMap<String, String[]>();
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つの方法について説明しました。
例の完全なソースコードは、GitHubのにあります。 URL /login_rememberで「rememberme」機能を使用してログインページにアクセスできます。