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」機能を使用してログインページにアクセスできます。