Help us understand the problem. What is going on with this article?

Spring Security 5.xでOAuth2アクセストークンをリフレッシュする

  • 2019/12/03 Spring Security 5.2リリースに伴い、記事を大幅に修正しました
  • 2020/09/04 RefreshTokenOAuth2AuthorizedClientProvider ではなく DefaultRefreshTokenTokenResponseClient を使う方法に全面的に書き換えました。併せて、Spring Security 5.3にアップデートしました

前提

認可サーバーはKeycloak、クライアントやリソースサーバーをSpring Security 5.3で作成しています。

関連記事

OAuth 2.0でアクセストークンをリフレッシュする方法

OAuth 2.0の仕様では、下記のように書かれています。

POST /token HTTP/1.1
Host: server.example.com
Authorization: client_idとclient_secretによるBasic認証
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=リフレッシュトークン

つまり、

  • トークンエンドポイントにPOSTでリクエストを送信
  • パラメーターは grant_type=refresh_tokenrefresh_token=リフレッシュトークン
  • client_idclient_secret によるBasic認証が必要

正確に言うと、Authorizationヘッダーついては詳細には仕様で触れられていないようです。しかし、多くの認可サーバーではクライアントのBasic認証は必須です。

DefaultRefreshTokenTokenResponseClient でアクセストークンをリフレッシュ

Spring Security 5.1以前でリフレッシュするためには、 RestTemplate などを使ってトークンエンドポイントにアクセスするコードを自分で書く必要がありました。

5.2で導入された DefaultRefreshTokenTokenResponseClient クラスの getTokenResponse() メソッドを使うと、簡単にリフレッシュ処理を記述できます。

DefaultRefreshTokenTokenResponseClient クラスを使うには、普通に new でインスタンスを生成します。

DefaultRefreshTokenTokenResponseClientの初期化
DefaultRefreshTokenTokenResponseClient tokenResponseClient =
    new DefaultRefreshTokenTokenResponseClient();

このクラスはフィールドとして RestTemplate を持っています。リフレッシュ処理時には、この RestTemplate が認可サーバーのトークンエンドポイントにアクセスします。

トークンエンドポイントのURLは、Spring Bootの場合はapllication.yamlに指定されたものが使われます( spring.security.oauth2.client.provider.プロバイダー名.token-uri プロパティ、またはIssuer URIから返ってくる値に含まれるURL)

この RestTemplate にはタイムアウトなどが設定されていませんので、タイムアウトが設定された RestTemplate を別途作成して、 setRestOperations() で代入したほうが良いでしょう。併せて、必要な HttpMessageConverter 実装なども設定します。

RestTemplateへのタイムアウト設定
RestTemplate restTemplate = restTemplateBuilder
    // 接続確立までのタイムアウト設定
    .setConnectTimeout(Duration.ofMillis(300))
    // レスポンスが返ってくるまでのタイムアウト設定
    .setReadTimeout(Duration.ofMillis(300))
    // 必要なエラーハンドラーの設定
    .errorHandler(new OAuth2ErrorResponseErrorHandler())
    // 必要なHttpMessageConverterの設定
    .messageConverters(
        new OAuth2AccessTokenResponseHttpMessageConverter(),
        new OAuth2ErrorHttpMessageConverter(),
        new FormHttpMessageConverter())
    .build();
tokenResponseClient.setRestOperations(restTemplate);
  • 以前この記事で紹介していた RefreshTokenOAuth2AuthorizedClientProvider には、 RestTemplate にタイムアウトを設定する手段がありません。なので、このクラスは使わない方がよいと思い、記事を修正しました。
  • 今回設定しているタイムアウト時間(300ms)は、サンプルとしてのいい加減な時間です。実務では、プロジェクトの状況に応じて適切な時間を設定してください。

getTokenResponse() メソッドは、引数として OAuth2RefreshTokenGrantRequest を必要とします(これもSpring Security 5.2で新規に追加されたクラスです)。

OAuth2RefreshTokenGrantRequestの作成
OAuth2AuthorizedClient currentAuthorizedClient = getAuthorizedClient();
ClientRegistration clientRegistration = currentAuthorizedClient.getClientRegistration();
OAuth2RefreshTokenGrantRequest tokenRequest =
    new OAuth2RefreshTokenGrantRequest(clientRegistration,
        currentAuthorizedClient.getAccessToken(),
        currentAuthorizedClient.getRefreshToken());

getTokenResponse() メソッドを実行すると、新しいアクセストークンを保持する OAuth2AccessTokenResponse が返ってきます。

リフレッシュ処理の実行
OAuth2AccessTokenResponse tokenResponse = 
    tokenResponseClient.getTokenResponse(tokenRequest);

リフレッシュ後のアクセストークンをSpring Securityに認識させる

アクセストークンを表す OAuth2AccessToken は、 OAuth2AuthorizedClient が保持しています(OAuth2AuthorizedClient#getAccessToken() メソッドで取得可能)。
そして OAuth2AuthorizedClient は、 OAuth2AuthorizedClientService が管理しています。

OAuth2AuthorizedClientService はインタフェースで、デフォルトの実装はインメモリで保持する InMemoryOAuth2AuthorizedClientService です。Spring Boot環境では、Auto ConfigurationによりBean定義済みになっています。

スクリーンショット 2018-10-21 19.19.04.png

OAuth2AuthorizedClientService には3つメソッドがあり、それぞれ OAuth2AuthorizedClient を取得・登録・削除を行います。

操作 メソッド
取得 <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName)
登録 void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal)
削除 void removeAuthorizedClient(String clientRegistrationId, String principalName)

ということで、リフレッシュ後のアクセストークンをSpring Securityに認識させるには

  1. 既存の OAuth2AuthorizedClient を削除する
  2. リフレッシュ後の OAuth2AuthorizedClient を登録する

という手順になります。

1. 既存の OAuth2AuthorizedClient を削除する

OAuth2AuthorizedClient currentAuthorizedClient = ...;
authorizedClientService.removeAuthorizedClient(
    currentAuthorizedClient.getClientRegistration().getRegistrationId(),
    currentAuthorizedClient.getPrincipalName());

2. リフレッシュ後の OAuth2AuthorizedClient を登録する

OAuth2AuthenticationToken authentication = getAuthentication();
OAuth2AuthorizedClient newAuthorizedClient = new OAuth2AuthorizedClient(
    clientRegistration, authentication.getName(),
    tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
authorizedClientService.saveAuthorizedClient(newAuthorizedClient, authentication);

コードの全体像

こちらになります。

ちなみに

Spring WebFluxの WebClient を使うと、トークンのリフレッシュなどは自動でやってくれるらしいです(未検証)。

リファレンス -> https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#servlet-webclient

suke_masa
Java / Spring / Microservices / Kubernetes(CKAD) / IntelliJ IDEA
https://www.casareal.co.jp/ls
casareal
システム開発/評価・検証支援/品質改善支援サービスと現場に即した実践的なIT研修サービスを提供しています。
https://www.casareal.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした