23. OAuth2 WebFlux

Spring Security は、リアクティブアプリケーション用の OAuth2 と WebFlux の統合を提供します。

23.1 OAuth 2.0 ログイン

OAuth 2.0 ログイン機能は、OAuth 2.0 プロバイダー(例: GitHub)または OpenID Connect 1.0 プロバイダー(Google など)の既存のアカウントを使用してユーザーにアプリケーションにログインさせる機能をアプリケーションに提供します。OAuth 2.0 Login は、「Google でログイン」または「GitHub でログイン」というユースケースを実装しています。

[Note] メモ

OZZ 2.0 Login は、OAuth 2.0 認証フレームワーク (英語) および OpenID Connect コア 1.0 (英語) で指定されている 認証コード付与を使用して実装されます。

23.1.1 Spring Boot 2.0 サンプル

Spring Boot 2.0 は、OAuth 2.0 ログインの完全な自動構成機能を提供します。

このセクションでは、 Google 認証プロバイダーとして使用して OAuth 2.0 ログイン WebFlux サンプルを構成する方法を示し、次のトピックを扱います。

初期設定

ログインに Google の OAuth 2.0 認証システムを使用するには、Google API Console でプロジェクトを設定して OAuth 2.0 認証情報を取得する必要があります。

[Note] メモ

認証用の Google の OAuth 2.0 の実装 (英語) OpenID Connect 1.0 (英語) 仕様に準拠しており、OpenID 認定 (英語) です。

「OAuth 2.0 のセットアップ」セクションから始まる OpenID Connect (英語) ページの指示に従ってください。

「OAuth 2.0 資格情報の取得」の手順を完了すると、クライアント ID とクライアントシークレットで構成される資格情報を持つ新しい OAuth クライアントが必要になります。

リダイレクト URI の設定

リダイレクト URI は、エンドユーザーのユーザーエージェントが Google で認証 れ、同意ページで OAuth クライアント 前の手順で作成された へのアクセスを許可した後にリダイレクトされるアプリケーション内のパスです。

「リダイレクト URI の設定」サブセクションで、 承認されたリダイレクト URI フィールドが  http://localhost:8080/login/oauth2/code/google に設定されていることを確認します。

[Tip] ヒント

デフォルトのリダイレクト URI テンプレートは  {baseUrl}/login/oauth2/code/{registrationId} です。 registrationId ClientRegistration の一意の識別子です。この例では、 registrationId は  google です。

[Important] 重要

OAuth クライアントがプロキシサーバーの背後で実行されている場合は、プロキシサーバー構成をチェックして、アプリケーションが正しく構成されていることを確認することをお勧めします。また、 redirect-uri でサポートされている URI テンプレート変数も参照してください。

application.yml を構成する

Google で新しい OAuth クライアントを作成したため、 認証フローに OAuth クライアントを使用するようにアプリケーションを構成する必要があります。そうするには:

  1. application.yml に移動して、次の構成を設定します。

    spring:
      security:
        oauth2:
          client:
            registration:   1
              google:   2
                client-id: google-client-id
                client-secret: google-client-secret

    例 23.1: OAuth クライアントのプロパティ

    1

    spring.security.oauth2.client.registration は、OAuth クライアントプロパティの基本プロパティプレフィックスです。

    2

    ベースプロパティプレフィックスの後には、google などの ClientRegistration の ID が続きます。


  2. client-id および  client-secret プロパティの値を、前に作成した OAuth 2.0 資格情報に置き換えます。

アプリケーションを起動する

Spring Boot 2.0 サンプルを起動し、 http://localhost:8080 に移動します。次に、Google へのリンクを表示するデフォルトの 自動生成されたログインページにリダイレクトされます。

Google リンクをクリックすると、認証のために Google にリダイレクトされます。

Google アカウントの認証情報で認証した後、表示される次のページは同意画面です。同意画面では、以前に作成した OAuth クライアントへのアクセスを認可または拒否するように求められます。 許可するをクリックして、OAuth クライアントがメールアドレスと基本的なプロファイル情報にアクセスすることを認可します。

この時点で、OAuth クライアントは UserInfo エンドポイント (英語) からメールアドレスと基本プロファイル情報を取得し、認証済みセッションを確立します。

23.1.2 OpenID プロバイダー構成の使用

よく知られているプロバイダーの場合、Spring Security は OAuth 認証プロバイダーの構成に必要なデフォルトを提供します。 OpenID プロバイダーの構成 (英語) または認可サーバーのメタデータ (英語) をサポートする独自の認証プロバイダーを使用している場合、OpenID プロバイダーの構成レスポンス (英語) の  issuer-uri を使用してアプリケーションを構成できます。

spring:
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: https://idp.example.com/auth/realms/demo
        registration:
          keycloak:
            client-id: spring-security
            client-secret: 6cea952f-10d0-4d00-ac79-cc865820dc2c

issuer-uri は、構成を検出するために、エンドポイント  https://idp.example.com/auth/realms/demo/.well-known/openid-configuration (英語) https://idp.example.com/.well-known/openid-configuration/auth/realms/demo (英語) 、または  https://idp.example.com/.well-known/oauth-authorization-server/auth/realms/demo (英語)  を直列に照会するように Spring Security に指示します。

[Note] メモ

Spring Security は、エンドポイントを一度に 1 つずつ照会し、最初に停止して 200 レスポンスを返します。

keycloak はプロバイダーと登録の両方に使用されるため、 client-id と  client-secret はプロバイダーにリンクされます。

23.1.3 明示的な OAuth2 ログイン構成

最小限の OAuth2 ログイン構成を以下に示します。

@Bean
ReactiveClientRegistrationRepository clientRegistrations() {
    ClientRegistration clientRegistration = ClientRegistrations
            .fromIssuerLocation("https://idp.example.com/auth/realms/demo")
            .clientId("spring-security")
            .clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c")
            .build();
    return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .oauth2Login(withDefaults());
    return http.build();
}

追加の構成オプションは以下のとおりです。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .oauth2Login(oauth2Login ->
            oauth2Login
                .authenticationConverter(converter)
                .authenticationManager(manager)
                .authorizedClientRepository(authorizedClients)
                .clientRegistrationRepository(clientRegistrations)
        );
    return http.build();
}

23.2 OAuth2 クライアント

Spring Security の OAuth サポートにより、認証なしでアクセストークンを取得できます。Spring Boot の基本的な構成は以下のとおりです。

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: replace-with-client-id
            client-secret: replace-with-client-secret
            scope: read:user,public_repo

client-id および  client-secret を GitHub で登録された値に置き換える必要があります。

次のステップでは、アクセストークンを取得できるように、OAuth2 クライアントとして機能することを Spring Security に指示します。

@Bean
SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception {
    http
        // ...
        .oauth2Client(withDefaults());
    return http.build();
}

Spring Security の第 26 章: WebClient または @RegisteredOAuth2AuthorizedClient サポートを活用して、アクセストークンを取得して使用できるようになりました。

23.3 OAuth 2.0 リソースサーバー

Spring Security は、2 つの形式の OAuth 2.0 ベアラートークン (英語) を使用したエンドポイントの保護をサポートしています。

これは、アプリケーションが権限管理を認可サーバー (英語) (Okta や Ping Identity など)に委譲している場合に便利です。この認可サーバーは、リソースサーバーがリクエストを認可するために調べることができます。

23.3.1 依存関係

ほとんどのリソースサーバーサポートは  spring-security-oauth2-resource-server に収集されます。ただし、JWT のデコードと検証のサポートは  spring-security-oauth2-jose にあります。つまり、JWT でエンコードされたベアラートークンをサポートする作業リソースサーバーを使用するには両方が必要です。

23.3.2 JWT の最小構成

Spring Boot を使用する場合、アプリケーションをリソースサーバーとして構成するには、2 つの基本的な手順が必要です。最初に、必要な依存関係を含め、2 番目に認可サーバーの場所を示します。

認可サーバーの指定

Spring Boot アプリケーションで、使用する認可サーバーを指定するには、次のようにします。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com/issuer

ここで、 https://idp.example.com/issuer (英語)  は、認可サーバーが発行する JWT トークンの  iss クレームに含まれる値です。リソースサーバーは、このプロパティを使用して、さらに自己構成を行い、認可サーバーの公開キーを検出し、受信 JWT を検証します。

[Note] メモ

issuer-uri プロパティを使用するには、 https://idp.example.com/issuer/.well-known/openid-configuration (英語) https://idp.example.com/.well-known/openid-configuration/issuer (英語) 、または  https://idp.example.com/.well-known/oauth-authorization-server/issuer (英語)  のいずれかが認可サーバーでサポートされているエンドポイントであることも真である必要があります。このエンドポイントは、プロバイダー構成 (英語) エンドポイントまたは認可サーバーのメタデータ (英語) エンドポイントと呼ばれます。

以上です!

スタートアップの期待

このプロパティとこれらの依存関係を使用すると、Resource Server は自動的に JWT エンコードされたベアラートークンを検証するように自身を構成します。

これは、決定論的な起動プロセスを通じてこれを実現します。

  1. プロバイダー構成または認可サーバーメタデータエンドポイントをヒットし、 jwks_url プロパティのレスポンスを処理する
  2. 有効な公開鍵について  jwks_url を照会するための検証戦略を構成する
  3. https://idp.example.com (英語) に対して各 JWT  iss クレームを検証する検証戦略を構成します。

このプロセスの結果、リソースサーバーが正常に起動するには、認可サーバーが起動してリクエストを受信する必要があります。

[Note] メモ

リソースサーバーがクエリを実行したときに認可サーバーがダウンした場合(適切なタイムアウトが与えられた場合)、起動は失敗します。

ランタイムの期待

アプリケーションが起動すると、Resource Server は  Authorization: Bearer ヘッダーを含むリクエストの処理を試みます。

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

このスキームが示されている限り、Resource Server は Bearer Token 仕様に従ってリクエストの処理を試みます。

整形式の JWT が与えられると、リソースサーバーは次のことを行います。

  1. 起動時に  jwks_url エンドポイントから取得し、JWT ヘッダーと照合した公開鍵に対して署名を検証する
  2. JWT  exp および  nbf タイムスタンプと JWT  iss クレームを検証する
  3. 各スコープを接頭辞  SCOPE_ を持つ機関にマップします。
[Note] メモ

認可サーバーが新しいキーを使用可能にすると、Spring Security は JWT トークンの検証に使用されるキーを自動的に回転させます。

デフォルトでは、結果の  Authentication#getPrincipal は Spring Security  Jwt オブジェクトであり、 Authentication#getName は JWT の  sub プロパティ(存在する場合)にマップします。

ここから、次へのジャンプを検討してください。

リソースサーバーの起動を認可サーバーの可用性に結び付けずに構成する方法

Spring Boot なしで構成する方法

認可サーバー JWK セット Uri を直接指定する

認可サーバーが構成エンドポイントをサポートしていない場合、またはリソースサーバーが認可サーバーから独立して起動できる必要がある場合は、 jwk-set-uri も提供できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          jwk-set-uri: https://idp.example.com/.well-known/jwks.json
[Note] メモ

JWK Set uri は標準化されていませんが、通常は認可サーバーのドキュメントに記載されています

リソースサーバーは起動時に認可サーバーに ping を実行しません。 issuer-uri を引き続き指定して、Resource Server が受信 JWT で  iss クレームを検証するようにします。

[Note] メモ

このプロパティは、DSL で直接指定することもできます。

Boot 自動構成のオーバーライドまたは置換

Spring Boot がリソースサーバーに代わって生成する 2 つの  @Bean があります。

1 つ目は、アプリをリソースサーバーとして構成する  SecurityWebFilterChain です。 spring-security-oauth2-jose を含めると、この  WebSecurityConfigurerAdapter は次のようになります。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange(exchanges ->
            exchanges
                .anyExchange().authenticated()
        )
        .oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
    return http.build();
}

アプリケーションが  SecurityWebFilterChain Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えることは、アプリケーション内で Bean を公開するのと同じくらい簡単です。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange(exchanges ->
            exchanges
                .pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
                .anyExchange().authenticated()
        )
        .oauth2ResourceServer(oauth2ResourceServer ->
            oauth2ResourceServer
                .jwt(withDefaults())
        );
    return http.build();
}

上記では、 /messages/ で始まる URL の  message:read のスコープが必要です。

oauth2ResourceServer DSL のメソッドも自動構成をオーバーライドまたは置き換えます。

例: Spring Boot が 2 番目に作成する  @Bean は、 String トークンを  Jwt の検証済みインスタンスにデコードする  ReactiveJwtDecoder です。

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
[Note] メモ

ReactiveJwtDecoders#fromIssuerLocation (Javadoc)  を呼び出すと、JWK セット Uri を取得するためにプロバイダー構成または認可サーバーメタデータエンドポイントが呼び出されます。アプリケーションが  ReactiveJwtDecoder Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

そして、その構成は  jwkSetUri() を使用してオーバーライドするか、 decoder() を使用して置き換えることができます。

jwkSetUri() を使用する

認可サーバーの JWK Set Uri は 、構成プロパティとして構成することも、DSL で提供することもできます。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange(exchanges ->
            exchanges
                .anyExchange().authenticated()
        )
        .oauth2ResourceServer(oauth2ResourceServer ->
            oauth2ResourceServer
                .jwt(jwt ->
                    jwt
                        .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
                )
        );
    return http.build();
}

jwkSetUri() の使用は、構成プロパティよりも優先されます。

decoder() を使用する

jwkSetUri() よりも強力なのは  decoder() です。これは、 JwtDecoder の Boot 自動構成を完全に置き換えます。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt()
                .decoder(myCustomDecoder());
    return http.build();
}

これは、検証などのより詳細な構成が必要な場合に便利です。

ReactiveJwtDecoder の公開 @Bean

または、 ReactiveJwtDecoder を公開すると @Bean は  decoder() と同じ効果があります。

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}

23.3.3 信頼できるアルゴリズムの構成

デフォルトでは、 NimbusReactiveJwtDecoder、リソースサーバーは、 RS256 を使用したトークンのみを信頼および検証します。

これは、Spring Boot または NimbusJwtDecoder ビルダーを介してカスタマイズできます。

Spring Boot 経由

アルゴリズムを設定する最も簡単な方法は、プロパティとしてです:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jws-algorithm: RS512
          jwk-set-uri: https://idp.example.org/.well-known/jwks.json

ビルダーを使用する

ただし、より強力にするには、 NimbusReactiveJwtDecoder に同梱されているビルダーを使用できます。

@Bean
ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).build();
}

jwsAlgorithm を複数回呼び出すと、 NimbusReactiveJwtDecoder は次のように複数のアルゴリズムを信頼するように構成されます。

@Bean
ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
}

または、 jwsAlgorithms を呼び出すことができます。

@Bean
ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
            .jwsAlgorithms(algorithms -> {
                    algorithms.add(RS512);
                    algorithms.add(EC512);
            }).build();
}

単一の非対称キーを信頼する

JWK Set エンドポイントでリソースサーバーをバッキングするよりも簡単なのは、RSA 公開キーをハードコードすることです。公開鍵は、Spring Boot またはビルダーを使用するを介して提供できます。

Spring Boot 経由

Spring Boot を介したキーの指定は非常に簡単です。キーの場所は次のように指定できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:my-key.pub

または、より洗練されたルックアップを可能にするために、 RsaKeyConversionServicePostProcessor を後処理できます。

@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
    return beanFactory ->
        beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
                .setResourceLoader(new CustomResourceLoader());
}

キーの場所を指定します。

key.location: hfds://my-key.pub

そして、値をオートワイヤーします。

@Value("${key.location}")
RSAPublicKey key;
ビルダーを使用する

RSAPublicKey を直接接続するには、次のように適切な  NimbusReactiveJwtDecoder ビルダーを使用するだけです。

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
}

単一の対称キーを信頼する

単一の対称キーの使用も簡単です。次のように、 SecretKey をロードして適切な  NimbusReactiveJwtDecoder ビルダーを使用するだけです。

@Bean
public ReactiveJwtDecoder jwtDecoder() {
    return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}

認可の構成

OAuth 2.0 認可サーバーから発行される JWT は通常、 scope または  scp 属性のいずれかを持ち、付与されたスコープ(または権限)を示します。例:

{ …​, "scope" : "messages contacts"}

この場合、Resource Server はこれらのスコープを付与された権限のリストに強制し、各スコープの前に文字列「SCOPE_」を付けようとします。

つまり、エンドポイントまたはメソッドを JWT から派生したスコープで保護するには、対応する式に次のプレフィックスを含める必要があります。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange(exchanges ->exchanges
            .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
            .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
            .anyExchange().authenticated()
        )
        .oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
    return http.build();
}

または、同様にメソッドセキュリティで:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
権限の手動抽出

ただし、このデフォルトでは不十分な状況がいくつかあります。例: 一部の認可サーバーは  scope 属性を使用せず、独自のカスタム属性を持っています。または、リソースサーバーは、属性または属性の構成を内部化された機関に適合させる必要がある場合もあります。

このために、DSL は  jwtAuthenticationConverter() を公開します:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt()
                .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
    return http.build();
}

Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
    JwtAuthenticationConverter jwtAuthenticationConverter =
            new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
            (new GrantedAuthoritiesExtractor());
    return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}

Jwt を  Authentication に変換する責任があります。その構成の一部として、 Jwt から付与された権限の  Collection に移行する補助コンバーターを提供できます。

その最終的なコンバーターは、以下の  GrantedAuthoritiesExtractor のようなものです。

static class GrantedAuthoritiesExtractor
        implements Converter<Jwt, Collection<GrantedAuthority>> {

    public Collection<GrantedAuthority> convert(Jwt jwt) {
        Collection<String> authorities = (Collection<String>)
                jwt.getClaims().get("mycustomclaim");

        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

柔軟性を高めるため、DSL はコンバーターを  Converter<Jwt, Mono<AbstractAuthenticationToken>> を実装するクラスに完全に置き換えることをサポートしています。

static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return Mono.just(jwt).map(this::doConversion);
    }
}

検証の構成

リソースサーバーは、認可サーバーの発行者 URI を示す最小限の Spring Boot 構成を使用して、 iss クレームと  exp および  nbf タイムスタンプクレームをデフォルトで検証します。

検証をカスタマイズする必要がある状況では、Resource Server には 2 つの標準バリデーターが付属しており、カスタム  OAuth2TokenValidator インスタンスも受け入れます。

タイムスタンプ検証のカスタマイズ

通常、JWT には有効期間があり、ウィンドウの開始は  nbf クレームで示され、終了は  exp クレームで示されます。

ただし、すべてのサーバーでクロックドリフトが発生する可能性があります。これにより、あるサーバーではトークンが期限切れになり、別のサーバーでは期限切れになります。これにより、分散システムでコラボレーションサーバーの数が増えると、実装の胸焼けが発生する可能性があります。

リソースサーバーは  JwtTimestampValidator を使用してトークンの有効期間を検証し、 clockSkew で構成して上記の問題を軽減できます。

@Bean
ReactiveJwtDecoder jwtDecoder() {
     NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
             ReactiveJwtDecoders.fromIssuerLocation(issuerUri);

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new IssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}
[Note] メモ

デフォルトでは、Resource Server は 30 秒のクロックスキューを設定します。

カスタム検証ツールの構成

aud クレームのチェックの追加は、 OAuth2TokenValidator API を使用すると簡単です。

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

次に、リソースサーバーに追加するには、 ReactiveJwtDecoder インスタンスを指定するだけです。

@Bean
ReactiveJwtDecoder jwtDecoder() {
    NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
            ReactiveJwtDecoders.fromIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}

イントロスペクションの最小構成

通常、Opaque トークンは、認可サーバーによってホストされる OAuth 2.0 イントロスペクションエンドポイント (英語) を介して検証できます。これは、失効が必要な場合に便利です。

Spring Boot を使用する場合、イントロスペクションを使用するリソースサーバーとしてアプリケーションを構成するには、2 つの基本的な手順が必要です。まず、必要な依存関係を含め、次に、イントロスペクションエンドポイントの詳細を示します。

認可サーバーの指定

イントロスペクションエンドポイントの場所を指定するには、次のようにします。

security:
  oauth2:
    resourceserver:
      opaque-token:
        introspection-uri: https://idp.example.com/introspect
        client-id: client
        client-secret: secret

ここで、 https://idp.example.com/introspect (英語)  は認証サーバーによってホストされるイントロスペクションエンドポイントであり、 client-id および  client-secret はそのエンドポイントをヒットするために必要な資格情報です。

リソースサーバーはこれらのプロパティを使用して、さらに自己構成し、受信 JWT を検証します。

[Note] メモ

イントロスペクションを使用する場合、認可サーバーの言葉は法律です。認可サーバーがトークンが有効であるとレスポンスした場合、有効です。

以上です!

スタートアップの期待

このプロパティとこれらの依存関係が使用されると、リソースサーバーは自動的に不透明なベアラートークンを検証するように構成します。

この起動プロセスは、エンドポイントを検出する必要がなく、追加の検証ルールが追加されないため、JWT よりもかなり単純です。

ランタイムの期待

アプリケーションが起動すると、Resource Server は  Authorization: Bearer ヘッダーを含むリクエストの処理を試みます。

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

このスキームが示されている限り、Resource Server は Bearer Token 仕様に従ってリクエストの処理を試みます。

Opaque トークンを指定すると、リソースサーバーは

  1. 指定された資格情報とトークンを使用して、指定されたイントロスペクションエンドポイントを照会する
  2. { 'active' : true } 属性のレスポンスをインスペクションする
  3. 各スコープをプレフィックス  SCOPE_ を持つ機関にマップする

結果として得られる  Authentication#getPrincipal は、デフォルトでは Spring Security  OAuth2AuthenticatedPrincipal (Javadoc)  オブジェクトであり、 Authentication#getName はトークンの  sub プロパティ(存在する場合)にマップします。

ここから、次の場所にジャンプできます。

認証後の属性の検索

トークンが認証されると、 BearerTokenAuthentication のインスタンスが  SecurityContext に設定されます。

つまり、構成で  @EnableWebFlux を使用する場合、 @Controller メソッドで使用できます。

@GetMapping("/foo")
public Mono<String> foo(BearerTokenAuthentication authentication) {
    return Mono.just(authentication.getTokenAttributes().get("sub") + " is the subject");
}

BearerTokenAuthentication は  OAuth2AuthenticatedPrincipal を保持するため、コントローラーメソッドでも使用できることも意味します。

@GetMapping("/foo")
public Mono<String> foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
    return Mono.just(principal.getAttribute("sub") + " is the subject");
}
SpEL を介した属性の検索

もちろん、これは、SpEL を介して属性にアクセスできることも意味します。

例:  @PreAuthorize アノテーションを使用できるように  @EnableReactiveMethodSecurity を使用する場合、次のことができます。

@PreAuthorize("principal?.attributes['sub'] == 'foo'")
public Mono<String> forFoosEyesOnly() {
    return Mono.just("foo");
}

Boot 自動構成のオーバーライドまたは置換

Spring Boot がリソースサーバーに代わって生成する 2 つの  @Bean があります。

1 つ目は、アプリをリソースサーバーとして構成する  SecurityWebFilterChain です。Opaque トークンを使用する場合、この  SecurityWebFilterChain は次のようになります。

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        .authorizeExchange()
            .anyExchange().authenticated()
            .and()
        .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken)
    return http.build();
}

アプリケーションが  SecurityWebFilterChain Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

これを置き換えることは、アプリケーション内で Bean を公開するのと同じくらい簡単です。

@EnableWebFluxSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .pathMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyExchange().authenticated()
                .and()
            .oauth2ResourceServer()
                .opaqueToken()
                    .introspector(myIntrospector());
        return http.build();
    }
}

上記では、 /messages/ で始まる URL の  message:read のスコープが必要です。

oauth2ResourceServer DSL のメソッドも自動構成をオーバーライドまたは置き換えます。

例: Spring Boot が 2 番目に作成する  @Bean は、 String トークンを  OAuth2AuthenticatedPrincipal の検証済みインスタンスにデコードする  ReactiveOpaqueTokenIntrospector です。

@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
    return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

アプリケーションが  ReactiveOpaqueTokenIntrospector Bean を公開しない場合、Spring Boot は上記のデフォルトを公開します。

そして、その構成は、 introspectionUri() および  introspectionClientCredentials() を使用してオーバーライドするか、 introspector() を使用して置き換えることができます。

introspectionUri() を使用する

認可サーバーの Introspection Uri は 、構成プロパティとして構成するか、DSL で提供できます。

@EnableWebFluxSecurity
public class DirectlyConfiguredIntrospectionUri {
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .anyExchange().authenticated()
                .and()
            .oauth2ResourceServer()
                .opaqueToken()
                    .introspectionUri("https://idp.example.com/introspect")
                    .introspectionClientCredentials("client", "secret");
        return http.build();
    }
}

introspectionUri() の使用は、構成プロパティよりも優先されます。

introspector() を使用する

introspectionUri() よりも強力なのは  introspector() です。これは、 ReactiveOpaqueTokenIntrospector の Boot 自動構成を完全に置き換えます。

@EnableWebFluxSecurity
public class DirectlyConfiguredIntrospector {
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .anyExchange().authenticated()
                .and()
            .oauth2ResourceServer()
                .opaqueToken()
                    .introspector(myCustomIntrospector());
        return http.build();
    }
}

これは、権限マッピングJWT の取り消しなどのより深い構成が必要な場合に便利です。

ReactiveOpaqueTokenIntrospector の公開 @Bean

または、 ReactiveOpaqueTokenIntrospector を公開すると @Bean は  introspector() と同じ効果があります。

@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
    return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}

認可の構成

OAuth 2.0 Introspection エンドポイントは、通常、 scope 属性を返し、付与されたスコープ(または権限)を示します。例:

{ …​, "scope" : "messages contacts"}

この場合、Resource Server はこれらのスコープを付与された権限のリストに強制し、各スコープの前に文字列「SCOPE_」を付けようとします。

つまり、Opaque トークンから派生したスコープを持つエンドポイントまたはメソッドを保護するには、対応する式に次のプレフィックスを含める必要があります。

@EnableWebFluxSecurity
public class MappedAuthorities {
    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange(exchange -> exchange
                .pathMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .pathMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyExchange().authenticated()
            )
            .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken);
        return http.build();
    }
}

または、同様にメソッドセキュリティで:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
権限の手動抽出

デフォルトでは、Opaque トークンサポートは、イントロスペクションレスポンスからスコープ要求を抽出し、個々の  GrantedAuthority インスタンスに解析します。

例: イントロスペクションのレスポンスが次の場合:

{
    "active" : true,
    "scope" : "message:read message:write"
}

次に、Resource Server は、 message:read 用と  message:write 用の 2 つの権限を持つ  Authentication を生成します。

これはもちろん、属性セットを見て独自の方法で変換するカスタム  ReactiveOpaqueTokenIntrospector を使用してカスタマイズできます。

public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
    private ReactiveOpaqueTokenIntrospector delegate =
            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");

    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
        return this.delegate.introspect(token)
                .map(principal -> new DefaultOAuth2AuthenticatedPrincipal(
                        principal.getName(), principal.getAttributes(), extractAuthorities(principal)));
    }

    private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
        List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
        return scopes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

その後、このカスタムイントロスペクターは、 @Bean として公開するだけで構成できます。

@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
    return new CustomAuthoritiesOpaqueTokenIntrospector();
}

JWT でのイントロスペクションの使用

よくある質問は、イントロスペクションが JWT と互換性があるかどうかです。Spring Security の Opaque トークンサポートは、トークンの形式を気にしないように設計されています。提供されたイントロスペクションエンドポイントにトークンを喜んで渡します。

JWT が取り消された場合に備えて、リクエストごとに認可サーバーで確認する必要がある要件があるとしましょう。

トークンに JWT 形式を使用している場合でも、検証方法はイントロスペクションです。つまり、次のようにします。

spring:
  security:
    oauth2:
      resourceserver:
        opaque-token:
          introspection-uri: https://idp.example.org/introspection
          client-id: client
          client-secret: secret

この場合、結果の  Authentication は  BearerTokenAuthentication になります。対応する  OAuth2AuthenticatedPrincipal の属性は、イントロスペクションエンドポイントによって返されたものです。

しかし、奇妙なことに、イントロスペクションエンドポイントは、トークンがアクティブであるかどうかのみを返します。それで?

この場合、エンドポイントにヒットするカスタム  ReactiveOpaqueTokenIntrospector を作成できますが、返されたプリンシパルを更新して、属性として JWT クレームを取得します。

public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
    private ReactiveOpaqueTokenIntrospector delegate =
            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private ReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(new ParseOnlyJWTProcessor());

    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
        return this.delegate.introspect(token)
                .flatMap(principal -> this.jwtDecoder.decode(token))
                .map(jwt -> new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES));
    }

    private static class ParseOnlyJWTProcessor implements Converter<JWT, Mono<JWTClaimsSet>> {
        public Mono<JWTClaimsSet> convert(JWT jwt) {
            try {
                return Mono.just(jwt.getJWTClaimsSet());
            } catch (Exception e) {
                return Mono.error(e);
            }
        }
    }
}

その後、このカスタムイントロスペクターは、 @Bean として公開するだけで構成できます。

@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
    return new JwtOpaqueTokenIntropsector();
}

/userinfo エンドポイントの呼び出し

一般的に、リソースサーバーは、基になるユーザーを気にするのではなく、付与された権限を気にします。

ただし、認証ステートメントをユーザーに結び付けることが重要な場合があります。

適切な  ClientRegistrationRepository をセットアップして、アプリケーションが  spring-security-oauth2-client も使用している場合、これはカスタム  OpaqueTokenIntrospector で非常に簡単です。以下のこの実装は、3 つのことを行います。

  • トークンの有効性を確認するために、イントロスペクションエンドポイントに委譲する
  • /userinfo エンドポイントに関連付けられた適切なクライアント登録を検索する
  • /userinfo エンドポイントからレスポンスを呼び出して返する
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
    private final ReactiveOpaqueTokenIntrospector delegate =
            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService =
            new DefaultReactiveOAuth2UserService();

    private final ReactiveClientRegistrationRepository repository;

    // ... constructor

    @Override
    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
        return Mono.zip(this.delegate.introspect(token), this.repository.findByRegistrationId("registration-id"))
                .map(t -> {
                    OAuth2AuthenticatedPrincipal authorized = t.getT1();
                    ClientRegistration clientRegistration = t.getT2();
                    Instant issuedAt = authorized.getAttribute(ISSUED_AT);
                    Instant expiresAt = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT);
                    OAuth2AccessToken accessToken = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
                    return new OAuth2UserRequest(clientRegistration, accessToken);
                })
                .flatMap(this.oauth2UserService::loadUser);
    }
}

spring-security-oauth2-client を使用していない場合でも、非常に簡単です。 WebClient の独自のインスタンスで  /userinfo を呼び出すだけです。

public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
    private final ReactiveOpaqueTokenIntrospector delegate =
            new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
    private final WebClient rest = WebClient.create();

    @Override
    public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
        return this.delegate.introspect(token)
                .map(this::makeUserInfoRequest);
    }
}

いずれにしても、 ReactiveOpaqueTokenIntrospector を作成したら、それを  @Bean として公開してデフォルトをオーバーライドする必要があります。

@Bean
ReactiveOpaqueTokenIntrospector introspector() {
    return new UserInfoOpaqueTokenIntrospector(...);
}

23.3.4 ベアラートークンの伝播

これでベアラートークンを所有しているため、それをダウンストリームサービスに渡すと便利かもしれません。 ServerBearerExchangeFilterFunction (Javadoc) では、これは非常に簡単です。次の例で確認できます。

@Bean
public WebClient rest() {
    return WebClient.builder()
            .filter(new ServerBearerExchangeFilterFunction())
            .build();
}

上記の  WebClient を使用してリクエストを実行すると、Spring Security は現在の  Authentication を検索し、 AbstractOAuth2Token (Javadoc)  資格情報を抽出します。次に、 Authorization ヘッダーでそのトークンを伝搬します。

例:

this.rest.get()
        .uri("https://other-service.example.com/endpoint")
        .retrieve()
        .bodyToMono(String.class)

https://other-service.example.com/endpoint (英語) を呼び出して、ベアラートークン  Authorization ヘッダーを追加します。

この動作をオーバーライドする必要がある場所では、次のようにヘッダーを自分で指定するだけです。

this.rest.get()
        .uri("https://other-service.example.com/endpoint")
        .headers(headers -> headers.setBearerAuth(overridingToken))
        .retrieve()
        .bodyToMono(String.class)

この場合、フィルターはフォールバックし、リクエストを Web フィルターチェーンの残りの部分に単純に転送します。

[Note] メモ

OAuth 2.0 クライアントフィルター機能 (Javadoc) とは異なり、このフィルター関数は、トークンが期限切れになった場合、トークンを更新しようとしません。このレベルのサポートを取得するには、OAuth 2.0 クライアントフィルターを使用してください。

現行バージョンへ切り替える