Edited at

Spring Security 5.1でRestTemplateを使ってアクセストークンをリフレッシュする


前提

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


関連記事


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ヘッダーついては詳細には仕様で触れられていないようですが、実質、クライアントの認証は必須です。



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

前出の仕様を素直にRestTemplateで実装すると、下記のようになります。

// POSTするリクエストパラメーターを作成

MultiValueMap<String, String> formParams = new LinkedMultiValueMap<>();
formParams.add("grant_type", "refresh_token");
formParams.add("refresh_token", getRefreshTokenValue());

// リクエストヘッダーを作成
HttpHeaders httpHeaders = new HttpHeaders();
String clientId = registration.getClientId();
String clientSecret = registration.getClientSecret();
String authHeaderValue = Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
httpHeaders.add(HttpHeaders.AUTHORIZATION, "Basic " + authHeaderValue);
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);

// リクエストを作成
RequestEntity<MultiValueMap<String, String>> requestEntity =
new RequestEntity<>(formParams, httpHeaders,
HttpMethod.POST, URI.create(provider.getTokenUri()));

// POSTリクエスト送信(リフレッシュトークン取得)
ResponseEntity<Map<String, String>> responseEntity = restTemplate.exchange(
requestEntity, new ParameterizedTypeReference<Map<String, String>>() {});
Map<String, String> responseJson = responseEntity.getBody();

さて問題は、


  • どうやってリフレッシュトークンを取得するか

  • リフレッシュ後のアクセストークンをどうやってSpring Securityに認識させるか

の2点です。


リフレッシュトークンの取得

OAuth2AuthorizedClient#getRefreshToken() メソッドで、リフレッシュトークンを表す OAuth2RefreshToken が取得できます。

@Autowired

OAuth2AuthorizedClientService authorizedClientService;

...

// OAuth2AuthenticationTokenはAuthenticationインタフェース実装クラス
OAuth2AuthenticationToken authentication =
(OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

// OAuth2AuthorizedClientを取得
OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());

// OAuth2RefreshTokenを取得
OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();

// リフレッシュトークンの値を取得
String refreshTokenValue = refreshToken.getTokenValue();


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

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

そして OAuth2AuthorizedClient は、 OAuth2AuthorizedClientService が管理しています。


OAuth2AuthorizedClientService はインタフェースで、デフォルトの実装はインメモリで保持する InMemoryOAuth2AuthorizedClientService です。


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 を削除する

OAuth2AuthenticationToken authentication =

(OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
authorizedClientService.removeAuthorizedClient(
authentication.getAuthorizedClientRegistrationId(),
authentication.getName());


2. リフレッシュ後に OAuth2AuthorizedClient を新規作成し、登録する

// ClientRegirtrationを作成

ClientRegirtration clientRegirtration = ClientRegistration
.withRegistrationId(authentication.getAuthorizedClientRegistrationId())
.clientId(registration.getClientId())
.clientSecret(registration.getClientSecret())
.clientAuthenticationMethod(new ClientAuthenticationMethod(registration.getClientAuthenticationMethod()))
.authorizationGrantType(new AuthorizationGrantType(registration.getAuthorizationGrantType()))
.redirectUriTemplate(registration.getRedirectUri())
.scope(registration.getScope())
.authorizationUri(provider.getAuthorizationUri())
.tokenUri(provider.getTokenUri())
.userInfoUri(provider.getUserInfoUri())
.userNameAttributeName(provider.getUserNameAttribute())
.jwkSetUri(provider.getJwkSetUri())
.clientName(registration.getClientName())
.build();

// OAuth2AccessTokenを作成
OAuth2AccessToken accessToken = new OAuth2AccessToken(
OAuth2AccessToken.TokenType.BEARER,
responseJson.get("access_token"),
Instant.now(),
Instant.now().plus(Integer.parseInt(responseJson.get("expires_in")), ChronoUnit.SECONDS),
Arrays.stream(responseJson.get("scope").split("\\S")).collect(Collectors.toSet())
);

// OAuth2RefreshTokenを作成
OAuth2RefreshToken refreshToken = new OAuth2RefreshToken(
responseJson.get("refresh_token"),
Instant.now()
);

// OAuth2AuthorizedClientを作成
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
clientRegistration, authentication.getName(),
accessToken, refreshToken);

// OAuth2AuthorizedClientを登録
authorizedClientService.saveAuthorizedClient(authorizedClient, authentication);


コードの全体像

こちらになります。


ちなみに

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

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