1. 概要

このチュートリアルでは、以前の記事でまとめ始めたOAuthパスワードフローの調査を続け、AngularJSアプリで更新トークンを処理する方法に焦点を当てます。

:この記事ではSpringOAuthレガシープロジェクトを使用しています。  新しいSpringSecurity5スタックを使用したこの記事のバージョンについては、Spring RESTAPIのOAuth2–Angularでの更新トークンの処理に関する記事をご覧ください。

2. アクセストークンの有効期限

まず、ユーザーがアプリケーションにログインしているときに、クライアントがアクセストークンを取得していたことを思い出してください。

function obtainAccessToken(params) {
    var req = {
        method: 'POST',
        url: "oauth/token",
        headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
        data: $httpParamSerializer(params)
    }
    $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");
            window.location.href = "login";
        });   
}

アクセストークンがCookieにどのように保存されているかに注意してください。このCookieは、トークン自体の有効期限に基づいて有効期限が切れます。

理解しておくべき重要なことは、 Cookie自体はストレージにのみ使用され、OAuthフローで他に何も駆動しないということです。 たとえば、ブラウザがリクエストとともにCookieをサーバーに自動的に送信することはありません。

このobtainAccessToken()関数を実際に呼び出す方法にも注意してください。

$scope.loginData = {
    grant_type:"password", 
    username: "", 
    password: "", 
    client_id: "fooClientIdPassword"
};

$scope.login = function() {   
    obtainAccessToken($scope.loginData);
}

3. プロキシ

これで、Zuulプロキシをフロントエンドアプリケーションで実行し、基本的にフロントエンドクライアントと承認サーバーの間に配置します。

プロキシのルートを設定しましょう。

zuul:
  routes:
    oauth:
      path: /oauth/**
      url: http://localhost:8081/spring-security-oauth-server/oauth

ここで興味深いのは、トラフィックを承認サーバーにプロキシするだけで、他には何もプロキシしないことです。 クライアントが新しいトークンを取得しているときにプロキシが入る必要があるだけです。

ズールの基本を学びたい場合は、ズールのメイン記事をざっと読んでください。

4. 基本認証を行うZuulフィルター

プロキシの最初の使用は簡単です。JavaScriptでアプリ「clientsecret」を表示する代わりに、Zuulプレフィルターを使用して、トークンリクエストにアクセスするためのAuthorizationヘッダーを追加します。

@Component
public class CustomPreZuulFilter extends ZuulFilter {
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (ctx.getRequest().getRequestURI().contains("oauth/token")) {
            byte[] encoded;
            try {
                encoded = Base64.encode("fooClientIdPassword:secret".getBytes("UTF-8"));
                ctx.addZuulRequestHeader("Authorization", "Basic " + new String(encoded));
            } catch (UnsupportedEncodingException e) {
                logger.error("Error occured in pre filter", e);
            }
        }
        return null;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public int filterOrder() {
        return -2;
    }

    @Override
    public String filterType() {
        return "pre";
    }
}

これによってセキュリティが追加されることはありません。これを行う唯一の理由は、トークンエンドポイントがクライアントの資格情報を使用した基本認証で保護されているためです。

実装の観点から、フィルターのタイプは特に注目に値します。 リクエストを渡す前に処理するために、「pre」のフィルタータイプを使用しています。

5. 更新トークンをCookieに入れます

楽しいものに移りましょう。

ここで計画しているのは、クライアントに更新トークンをCookieとして取得させることです。 通常のCookieだけでなく、パスが非常に制限された安全なHTTPのみのCookie( / oauth / token )。

Zuulポストフィルターを設定して、応答のJSON本文からRefresh Tokenを抽出し、Cookieに設定します。

@Component
public class CustomPostZuulFilter extends ZuulFilter {
    private ObjectMapper mapper = new ObjectMapper();

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        try {
            InputStream is = ctx.getResponseDataStream();
            String responseBody = IOUtils.toString(is, "UTF-8");
            if (responseBody.contains("refresh_token")) {
                Map<String, Object> responseMap = mapper.readValue(
                  responseBody, new TypeReference<Map<String, Object>>() {});
                String refreshToken = responseMap.get("refresh_token").toString();
                responseMap.remove("refresh_token");
                responseBody = mapper.writeValueAsString(responseMap);

                Cookie cookie = new Cookie("refreshToken", refreshToken);
                cookie.setHttpOnly(true);
                cookie.setSecure(true);
                cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
                cookie.setMaxAge(2592000); // 30 days
                ctx.getResponse().addCookie(cookie);
            }
            ctx.setResponseBody(responseBody);
        } catch (IOException e) {
            logger.error("Error occured in zuul post filter", e);
        }
        return null;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public String filterType() {
        return "post";
    }
}

ここで理解すべきいくつかの興味深いこと:

  • Zuulポストフィルターを使用して応答を読み取り、更新トークンを抽出しました
  • refresh_token の値をJSON応答から削除して、Cookieの外部のフロントエンドにアクセスできないようにしました。
  • Cookieの最大有効期間を30日に設定しました。これはトークンの有効期限と一致するためです。

CSRF攻撃に対する保護の層を追加するために、すべてのCookieにSame-SiteCookieヘッダーを追加します。

そのために、構成クラスを作成します。

@Configuration
public class SameSiteConfig implements WebMvcConfigurer {
    @Bean
    public TomcatContextCustomizer sameSiteCookiesConfig() {
        return context -> {
            final Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
            cookieProcessor.setSameSiteCookies(SameSiteCookies.STRICT.getValue());
            context.setCookieProcessor(cookieProcessor);
        };
    }
}

ここでは、属性を strict に設定しているため、Cookieのサイト間転送は厳密に保留されます。

6. Cookieから更新トークンを取得して使用する

Cookieに更新トークンが含まれているので、フロントエンドのAngularJSアプリケーションがトークンの更新をトリガーしようとすると、 / oauth / token にリクエストが送信されるため、ブラウザーは次のようになります。もちろん、そのクッキーを送ってください。

そのため、プロキシに別のフィルターがあり、Cookieから更新トークンを抽出してHTTPパラメーターとして転送します。これにより、リクエストが有効になります。

public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ...
    HttpServletRequest req = ctx.getRequest();
    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));
    }
    ...
}

private String extractRefreshToken(HttpServletRequest req) {
    Cookie[] cookies = req.getCookies();
    if (cookies != null) {
        for (int i = 0; i < cookies.length; i++) {
            if (cookies[i].getName().equalsIgnoreCase("refreshToken")) {
                return cookies[i].getValue();
            }
        }
    }
    return null;
}

そして、これが CustomHttpServletRequest –リフレッシュトークンパラメータ注入するために使用されます。

public class CustomHttpServletRequest extends HttpServletRequestWrapper {
    private Map<String, String[]> additionalParams;
    private HttpServletRequest request;

    public CustomHttpServletRequest(
      HttpServletRequest request, Map<String, String[]> additionalParams) {
        super(request);
        this.request = request;
        this.additionalParams = additionalParams;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        Map<String, String[]> map = request.getParameterMap();
        Map<String, String[]> param = new HashMap<String, String[]>();
        param.putAll(map);
        param.putAll(additionalParams);
        return param;
    }
}

繰り返しになりますが、ここには多くの重要な実装上の注意があります。

  • プロキシはCookieから更新トークンを抽出しています
  • 次に、それをrefresh_tokenパラメーターに設定します
  • また、grant_typerefresh_tokenに設定しています。
  • refreshToken cookieがない場合(期限切れまたは最初のログイン)–アクセストークン要求は変更なしでリダイレクトされます

7. AngularJSからのアクセストークンの更新

最後に、単純なフロントエンドアプリケーションを変更して、実際にトークンの更新を利用しましょう。

関数refreshAccessToken()は次のとおりです。

$scope.refreshAccessToken = function() {
    obtainAccessToken($scope.refreshData);
}

そしてここに私たちの$scope.refreshData

$scope.refreshData = {grant_type:"refresh_token"};

既存のobtainAccessToken関数を単純に使用しており、さまざまな入力を関数に渡すことに注意してください。

また、 refresh_token を自分で追加していないことにも注意してください。これは、Zuulフィルターによって処理されるためです。

8. 結論

このOAuthチュートリアルでは、更新トークンをAngularJSクライアントアプリケーションに保存する方法、期限切れのアクセストークンを更新する方法、およびそれらすべてにZuulプロキシを活用する方法を学習しました。

このチュートリアルの完全な実装は、githubプロジェクトにあります。