OAuth2AuthenticatedPrincipal の authorities と BearerTokenAuthentication の authorities

2020.07.08

前回からの続きですが小ネタです。

公式リファレンスに記載されている Resource Server の OpaqueToken のための Minimal Configuration のセクションで以下のように記載されています。

Spring Security - runtime expectations

  1. Query the provided introspection endpoint using the provided credentials and the token
  2. Inspect the response for an { 'active' : true } attribute
  3. Map each scope to an authority with the prefix SCOPE_

authority には SCOPE_ の Prefix を付与して Scope 情報がマッピングされるとあります。

実際に以下のコードで確認してみると

@GetMapping("/hello")
public ResponseEntity<String> hello (Authentication authentication) {
  log.info("principal#getAuthorities: {}", ((DefaultOAuth2AuthenticatedPrincipal)authentication.getPrincipal()).getAuthorities());
  return ResponseEntity.ok("hello springboot world");
}

以下出力されたログ

2020-07-07 23:42:57.656  INFO 21176 --- [nio-8080-exec-2] j.c.sample.controller.RootController     : principal: [SCOPE_root]

OAuth2AuthenticatedPrincipal インタフェースの実体は DefaultOAuth2AuthenticatedPrincipal です。これがどこで生成されてるのかコードを追ってみました。

OAuth2AuthenticatedPrincipal はどこから返却されてるか

OpaqueTokenIntrospector のインタフェースを確認すると以下のとおりです。

@FunctionalInterface
public interface OpaqueTokenIntrospector {
    OAuth2AuthenticatedPrincipal introspect(String token);
}

前回見たとおり、デフォルト設定では OpaqueTokenIntrospector の実体は NimbusOpaqueTokenIntrospector になります。実装はこちらを見ていけばよいはず。

NimbusOpaqueTokenIntrospector

introspect メソッドの実装を確認。

@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
  RequestEntity<?> requestEntity = this.requestEntityConverter.convert(token);
  if (requestEntity == null) {
    throw new OAuth2IntrospectionException("requestEntityConverter returned a null entity");
  }

  ResponseEntity<String> responseEntity = makeRequest(requestEntity);
  HTTPResponse httpResponse = adaptToNimbusResponse(responseEntity);
  TokenIntrospectionResponse introspectionResponse = parseNimbusResponse(httpResponse);
  TokenIntrospectionSuccessResponse introspectionSuccessResponse = castToNimbusSuccess(introspectionResponse);

  // relying solely on the authorization server to validate this token (not checking 'exp', for example)
  if (!introspectionSuccessResponse.isActive()) {
    throw new BadOpaqueTokenException("Provided token isn't active");
  }

  // 1
  return convertClaimsSet(introspectionSuccessResponse);
}

// 1 より前は、Token Introspection の処理フローです。そのため、 AccessToken が有効であることが確認され、 // 1 の箇所ではじめて、 OAuth2AuthenticatedPrincipal の実体を作成する処理を行います。

convertClaimsSet() のメソッド内部を確認します。

NimbusOpaqueTokenIntrospector#convertClaimsSet()

正常に有効であることが確認できた Token Introspection Response を展開してオブジェクトを構築していきます。

private OAuth2AuthenticatedPrincipal convertClaimsSet(TokenIntrospectionSuccessResponse response) {
  Collection<GrantedAuthority> authorities = new ArrayList<>(); // 1
  Map<String, Object> claims = response.toJSONObject();
  if (response.getAudience() != null) {
    List<String> audiences = new ArrayList<>();
    for (Audience audience : response.getAudience()) {
      audiences.add(audience.getValue());
    }
    claims.put(AUDIENCE, Collections.unmodifiableList(audiences));
  }
  if (response.getClientID() != null) {
    claims.put(CLIENT_ID, response.getClientID().getValue());
  }
  if (response.getExpirationTime() != null) {
    Instant exp = response.getExpirationTime().toInstant();
    claims.put(EXPIRES_AT, exp);
  }
  if (response.getIssueTime() != null) {
    Instant iat = response.getIssueTime().toInstant();
    claims.put(ISSUED_AT, iat);
  }
  if (response.getIssuer() != null) {
    claims.put(ISSUER, issuer(response.getIssuer().getValue()));
  }
  if (response.getNotBeforeTime() != null) {
    claims.put(NOT_BEFORE, response.getNotBeforeTime().toInstant());
  }
  if (response.getScope() != null) {   // 2
    List<String> scopes = Collections.unmodifiableList(response.getScope().toStringList());
    claims.put(SCOPE, scopes);

    // 3
    for (String scope : scopes) {
      authorities.add(new SimpleGrantedAuthority(this.authorityPrefix + scope));
    }
  }

  // 4
  return new DefaultOAuth2AuthenticatedPrincipal(claims, authorities);
}
  1. Authority を格納する空Listを作成する
  2. Scope が存在する場合はまずは claims の Map にセット
  3. SCOPE_ の prefix を付ける必要があるので for-each で回しながら List に追加していく
    • GrantedAuthority インタフェースに合わせる必要があるため、 実装クラスの SimpleGrantedAuthority を作りながら追加
  4. インタフェースの実装クラスである DefaultOAuth2AutnenticatedPrincipla インスタンスを、前段までで作成したオブジェクトを引数に設定して作成して返却

ところで authorities ってもう一つなかったっけ

OAuth2AuthenticatedPrincipalBearerTokenAuthentication が保持するオブジェクトです。

BearerTokenAuthentication を見ると実はこれも getAuthorities() を持っています。

public class BearerTokenAuthentication extends AbstractOAuth2TokenAuthenticationToken<OAuth2AccessToken> {
public abstract class AbstractOAuth2TokenAuthenticationToken<T extends AbstractOAuth2Token> extends AbstractAuthenticationToken {
public abstract class AbstractAuthenticationToken implements Authentication,
        CredentialsContainer {
        // ...
        // ~ Methods
    // ========================================================================================================

    public Collection<GrantedAuthority> getAuthorities() {
        return authorities;
    }

ありました。BearerTokenAuthentication#getAuthorities()OAuth2AuthenticatedPrincipal#getAuthorities() の違いは何か。

OpaqueTokenAuthenticationProvider

前回処理をざっくり確認して、ProviderManager が保持している OpaqueTokenAuthenticationProvider で実際の処理が行われてるところまで確認しました。この中の実装に答えがありました。authenticate() メソッド内部で Introspection を実行して Result を作成しています。

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  if (!(authentication instanceof BearerTokenAuthenticationToken)) {
    return null;
  }
  BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;

  OAuth2AuthenticatedPrincipal principal;
  try {
    principal = this.introspector.introspect(bearer.getToken());
  } catch (BadOpaqueTokenException failed) {
    throw new InvalidBearerTokenException(failed.getMessage());
  } catch (OAuth2IntrospectionException failed) {
    throw new AuthenticationServiceException(failed.getMessage());
  }
  // 1. Introspection に成功するとここまで来る
  AbstractAuthenticationToken result = convert(principal, bearer.getToken()); // 2
  result.setDetails(bearer.getDetails());
  return result;
}

Introspection が成功すると OAuth2AuthenticatedPrincipal は作成済みです。2 で返却するオブジェクトへの変換を行っているようです。

private AbstractAuthenticationToken convert(OAuth2AuthenticatedPrincipal principal, String token) {
  Instant iat = principal.getAttribute(ISSUED_AT);
  Instant exp = principal.getAttribute(EXPIRES_AT);
  OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                                                        token, iat, exp);
  return new BearerTokenAuthentication(principal, accessToken, principal.getAuthorities()); // 3
}

3 を見ると principal.getAuthorities() を呼んでインスタンスを作成しています。

つまり Token Introspection の Configuration において、Authentication#getAuthorities()Authentcation#getPrincipal()#getAuthorities() は同じものが設定されてるようです。

まとめ

何気なく getAuthorities() を認識していましたが、ある日ふと同じメソッドがいくつかの箇所で定義されてるのに気づいたため、なんとなく気になったのでコードを追ってみました。

今回は内容があんまりありませんがこれくらいで。

参照