Spring WebFluxでSpring Security OAuth2で作った認証サーバを使って認証するライブラリを書いた
Spring Security 5.2.0に、OAuth 2.0 Token Introspectionで認証が可能なモジュールが実装されています。 Provide support for OAuth 2.0 Token Introspection · Issue #5200 · spring-projects/spring-security · GitHub
また、このReactive版も提供されており、WebFluxでOAuth2の文脈における、リソースサーバを書くのが簡単になっていくことでしょう。
この記事では、Spring Security OAuth2で使われる check_token
エンドポイントを使って
WebFluxで認証してみます。
以下の構成で解説をします。
- はじめに
check_token
エンドポイントとはcheck_token
エンドポイントを使って認証するReactiveAuthenticationProviderを実装する- 使い方
- まとめ
はじめに
現在、Spring Security OAuth2は maintenance modeになっており
これ以降の開発は望めない形になっています。
また、Spring Framework 5からはWebFluxという、従来のサーブレットのモデルとは違い
Reactiveに処理をするサーバを書くためのモジュールが追加されています。
同様に、Spring Security 5にはReactiveに認証する仕組みが整っており
その中にReactiveAuthenticationManagerというインターフェースがあります。
今回は Spring Security OAuth2で書かれた認証サーバにある check_token
エンドポイントを使って
認証を行うReactiveAuthenticationManagerの実装を書いてみます。
check_token
エンドポイントとは
大体この辺がメインです。
private AccessTokenConverter accessTokenConverter = new CheckTokenAccessTokenConverter() // 省略... @RequestMapping(value = "/oauth/check_token") @ResponseBody public Map<String, ?> checkToken(@RequestParam("token") String value) { OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value); // 省略... OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue()); return accessTokenConverter.convertAccessToken(token, authentication); // このエンドポイントのレスポンスとして重要なのはここだけ }
レスポンスとして重要なところだけ見てみます。 CheckTokenAccessTokenConverterの実装を覗いてみましょう。
static class CheckTokenAccessTokenConverter implements AccessTokenConverter { private final AccessTokenConverter accessTokenConverter; CheckTokenAccessTokenConverter() { this(new DefaultAccessTokenConverter()); } CheckTokenAccessTokenConverter(AccessTokenConverter accessTokenConverter) { this.accessTokenConverter = accessTokenConverter; } @Override public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { Map<String, Object> claims = (Map<String, Object>) this.accessTokenConverter.convertAccessToken(token, authentication); // gh-1070 claims.put("active", true); // Always true if token exists and not expired return claims; } // 省略... }
なるほど、DefaultAccessTokenConverterに移譲されているようですね。 そちらも覗いてみます。
public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { Map<String, Object> response = new HashMap<String, Object>(); OAuth2Request clientToken = authentication.getOAuth2Request(); if (!authentication.isClientOnly()) { response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication())); } else { if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) { response.put(UserAuthenticationConverter.AUTHORITIES, AuthorityUtils.authorityListToSet(clientToken.getAuthorities())); } } if (token.getScope()!=null) { response.put(scopeAttribute, token.getScope()); } if (token.getAdditionalInformation().containsKey(JTI)) { response.put(JTI, token.getAdditionalInformation().get(JTI)); } if (token.getExpiration() != null) { response.put(EXP, token.getExpiration().getTime() / 1000); } if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) { response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType()); } response.putAll(token.getAdditionalInformation()); response.put(clientIdAttribute, clientToken.getClientId()); if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) { response.put(AUD, clientToken.getResourceIds()); } return response; }
userTokenConverterまで追いかけていくと
user_name
, authorities
, scope
, exp
, aud
, client_id
, grant_type
, active
のフィールドを持つことがあるresponseが返ってくることがあるようです。
大体使いそうなレスポンスだけ書くと以下のような感じでしょうか。
{ "user_name": "foo" // client onlyじゃないなら "client_id": "test_client" "authorities": [] // client onlyじゃないなら、userのauthority, client onlyならclient authority "scope": [], "active": true }
これらの情報を使って ReactiveAuthenticationManagerを実装してみます。
check_token
エンドポイントを使って認証するReactiveAuthenticationProviderを実装する
実装は、Spring Security OAuth2を使ってリソースサーバを実装する際に使う、RemoteTokenServicesを参考にします。 spring-security-oauth/RemoteTokenServices.java at d2f5401f5789c36e42a51995d61ca4daf74fa8c3 · spring-projects/spring-security-oauth · GitHub
WebClientを使って、check_token
エンドポイントにリクエストを送ります。
Mono<ClientResponse> checkToken(String token) { return webClient.post() .uri(checkTokenUri) .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .body(BodyInserters.fromFormData("token", token)) .exchange(); }
RemoteTokenServicesでも使われているようにWebClientには、clientId, clientSecretのBasic認証のAuthorizationヘッダをつける必要があります。 (実際は認証サーバの仕様次第なんですが・・・・) 以下のような形でWebClientを組み立てると楽です。
WebClient.builder()
.defaultHeader(HttpHeaders.AUTHORIZATION, basicHeaderValue(clientId, clientSecret))
.build())
次はReactiveAuthenticationManagerで実装すべきメソッドを実装します。 BearerTokenAuthenticationTokenが来た時に渡されたtokenを使って authenticateTokenで認証します。その他エラーは認証失敗という形でOAuth2AuthenticationExceptionに落とします。
@Override public Mono<Authentication> authenticate(@Nullable Authentication authentication) { return Mono.justOrEmpty(authentication) .flatMap(authentication1 -> { if (authentication instanceof BearerTokenAuthenticationToken) { return Mono.just((BearerTokenAuthenticationToken) authentication); } return Mono.empty(); }) .map(BearerTokenAuthenticationToken::getToken) .flatMap(this::authenticateToken) .onErrorMap(throwable -> !(throwable instanceof OAuth2AuthenticationException), this::onError); }
メインのauthenticateTokenは以下のような形です。
こちらもRemoteTokenServicesを参考に書きました。
Mono<Authentication> authenticateToken(String token) { return checkToken(token).flatMap(clientResponse -> clientResponse.bodyToMono(TYPE_REFERENCE)) .map(map -> { if (map.containsKey("error")) { throw new OAuth2AuthenticationException(invalidToken("contains error: " + map.get("error"))); } // comment out for compatibility. // if (!map.containsKey("active")) { // throw new OAuth2AuthenticationException(invalidToken("This token is not active")); // } else { Object active = map.get("active"); if (active instanceof Boolean && !((Boolean) active)) { throw new OAuth2AuthenticationException(invalidToken("This token is not active")); } } Instant expiresAt = null; if (map.containsKey("exp")) { Object exp = map.get("exp"); if (exp instanceof Long) { expiresAt = Instant.ofEpochMilli((Long) exp); } else if (exp instanceof Integer) { expiresAt = Instant.ofEpochMilli(((Integer) exp).longValue()); } } List<GrantedAuthority> authorities = new ArrayList<>(); if (map.containsKey("authorities")) { Object list = map.get("authorities"); if (list instanceof Collection) { authorities = AuthorityUtils.createAuthorityList(((Collection<String>) list).toArray(new String[0])); } } Set<String> scopes = new HashSet<>(); if (map.containsKey("scope")) { Collection<String> scope = (Collection<String>) map.get("scope"); scopes.addAll(scope); } OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, null, expiresAt, scopes); String client_id = (String) map.get("client_id"); String userName = (String) map.get("user_name"); CheckTokenAuthenticationToken checkTokenAuthenticationToken = new CheckTokenAuthenticationToken(accessToken, map, authorities, client_id); checkTokenAuthenticationToken.setUserName(userName); return checkTokenAuthenticationToken; }); }
使い方
こんな感じで設定すると使える。
@Bean SecurityWebFilterChain configure(ServerHttpSecurity http) { webClient = WebClient.builder() .defaultHeader(HttpHeaders.AUTHORIZATION, basicHeaderValue(clientId, clientSecret)) .build() CheckTokenReactiveAuthenticationManager authenticationManager = new CheckTokenReactiveAuthenticationManager(checkTokenUri, webClient); AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager) oauth2.setServerAuthenticationConverter(ServerBearerTokenAuthenticationConverter()) http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION) return http .authorizeExchange() .anyExchange().authenticated() .and() .httpBasic().disable() .build(); }
まとめ
今回はSpring Security OAuth2の実装を見ながら、Spring Security OAuth2で使われる check_token
エンドポイントを使って
Spring Security 5のReactiveAuthenticationManagerの実装を書きました。
実装はこの辺に置いてあります。
bricks/check-token-reactive-authenticator at master · wreulicke/bricks · GitHub