OAuth2更新トークンで私を覚えている

1概要

この記事では、 AAuth 2 Refresh Tokenを利用して、 OAuth 2 保護アプリケーションに「Remember Me」機能を追加します。

この記事は、 AngularJS クライアントを介してアクセスされるSpring REST APIを保護するための OAuth 2 の使用に関するシリーズの続きです。 Authorization Server、Resource Server、およびフロントエンドクライアントを設定するには、 入門記事 を参照してください。

それから、 Zuul プロキシを使用して、リンク:/spring-security-oauth2-refresh-token-angular-js[更新トークンの処理]に関する記事を続けることができます。

2 OAuth 2アクセストークンと更新トークン

まず、 AAuth 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呼び出しに使用されますが、リフレッシュトークンの目的は新しい有効なアクセストークンを取得すること、または直前のトークンを取り消すことです。

refresh token__付与タイプを使用して新しいアクセストークンを受信するために、ユーザーは資格情報を入力する必要はなくなりましたが、クライアントID、シークレット、およびもちろん更新トークンのみを入力しました。

  • 2種類のトークンを使用することの目的は、ユーザーのセキュリティを強化することです** 通常、アクセストークンは有効期間が短いため、攻撃者がアクセストークンを取得した場合、使用時間は限られます。一方、更新トークンが危険にさらされると、クライアントIDとシークレットも必要になるため、これは役に立ちません。

更新トークンのもう1つの利点は、アクセストークンを取り消して、新しいIPからログインするなどの異常な動作がユーザーに表示された場合に別のトークンを返送しないことです。

** 3更新トークンによるRemember-Me機能

ユーザーはアプリケーションにアクセスするたびに資格情報を入力する必要がないため、通常はセッションを保存するオプションがあると便利です。

アクセストークンは有効期間が短いため、代わりに更新トークンを使用して新しいアクセストークンを生成し、アクセストークンの有効期限が切れるたびにユーザーに資格情報を問い合わせる必要がなくなります。

次のセクションでは、この機能を実装する2つの方法について説明します。

  • 最初に、401ステータスを返すユーザーリクエストをインターセプトする

アクセストークンが無効であることを意味します。このとき、ユーザーが[記憶する]オプションをオンにした場合は、 refresh token__ grantタイプを使用して新しいアクセストークンの要求が自動的に発行され、その後で最初の要求が再度実行されます。

  • 2番目に、アクセストークンを積極的に更新することができます。

期限切れになる数秒前にトークンの更新を要求する

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 変数へのデータバインディングを含むチェックボックスを追加しましょう。

<input type="checkbox"  ng-model="loginData.remember" id="remember"/>
<label for="remember">Remeber me</label>

ログインフォームに追加のチェックボックスが表示されます。

リンク:/uploads/remember.png%20539w[]

loginData オブジェクトは認証要求とともに送信されるため、 remember パラメータが含まれます。認証要求が送信される前に、パラメータに基づいて remember という名前のクッキーを設定します。

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 を使用して、POSTリクエストを/oauth/tokenエンドポイントに送信するために使用する 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<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. 積極的にトークンを更新

「記憶」機能を実装するもう1つの方法は、現在のものが期限切れになる前に新しいアクセストークンを要求することです。

アクセストークンを受信すると、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 フロントエンドを使用して「Remember Me」機能を実装する2つの方法について説明しました。

例の完全なソースコードはhttps://github.com/Baeldung/spring-security-oauth[over on GitHub]にあります。あなたは、URL /login remember__で「私を覚えている」機能でログインページにアクセスすることができます。