OAuth 2.0 リソースサーバー JWT

JWT の最小依存関係

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

JWT の最小構成

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

認可サーバーの指定

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

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

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

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

以上です!

スタートアップの期待

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

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

  1. プロバイダー構成または認可サーバーのメタデータエンドポイントに jwks_url プロパティを照会します

  2. サポートされているアルゴリズムについて jwks_url エンドポイントを照会します

  3. 見つかったアルゴリズムの有効な公開鍵を jwks_url に照会するように検証戦略を構成します

  4. idp.example.com (英語) に対して各 JWT iss クレームを検証する検証戦略を構成します。

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

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

ランタイムの期待

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

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

このスキームが示されている限り、リソースサーバーはベアラートークン仕様に従ってリクエストの処理を試みます。

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

  1. 起動時に jwks_url エンドポイントから取得され、JWT と照合される公開鍵に対して署名を検証します

  2. JWT の exp および nbf タイムスタンプと、JWT の iss クレームを検証します。

  3. 各スコープを接頭辞 SCOPE_ を持つオーソリティにマップします。

認可サーバーが新しい鍵を使用できるようになると、Spring Security は JWT の検証に使用される鍵を自動的にローテーションします。

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

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

JWT 認証の仕組み

次に、今見たような、サーブレットベースのアプリケーションで Spring Security が JWT [IETF] (英語) 認証をサポートするために使用するアーキテクチャコンポーネントを見てみましょう。

JwtAuthenticationProvider (Javadoc) は、JwtDecoder および JwtAuthenticationConverter を利用して JWT を認証する AuthenticationProvider 実装です。

JwtAuthenticationProvider が Spring Security 内でどのように機能するかを見てみましょう。この図は、ベアラートークンの読み取りの図で AuthenticationManager がどのように機能するかの詳細を説明しています。

jwtauthenticationprovider
図 1: JwtAuthenticationProvider の使用箇所

number 1 ベアラートークンの読み取りからの認証 Filter は、ProviderManager によって実装される AuthenticationManager に BearerTokenAuthenticationToken を渡します。

number 2ProviderManager は、型 JwtAuthenticationProviderAuthenticationProvider を使用するように構成されています。

number 3JwtAuthenticationProvider は、JwtDecoder を使用して Jwt をデコード、検証、検証します。

number 4 次に、JwtAuthenticationProvider は JwtAuthenticationConverter を使用して、Jwt を許可された権限の Collection に変換します。

number 5 認証が成功すると、返される Authentication は型 JwtAuthenticationToken であり、構成された JwtDecoder によって返される Jwt であるプリンシパルを持ちます。最終的に、返された JwtAuthenticationToken は認証 Filter によって SecurityContextHolder に設定されます。

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

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

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

ビューアーを供給する

すでに見たように、issuer-uri プロパティは iss クレームを検証します ; これが JWT の送信者です。

Boot には、aud クレームを検証するための audiences プロパティもあります。これが JWT の送信先です。

リソースサーバーの対象者は次のように指定できます。

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://idp.example.com
          audiences: https://my-resource-server.example.com
必要に応じて、aud 検証をプログラムで追加することもできます。

その結果、JWT の iss クレームが idp.example.com (英語) ではなく、その aud クレームのリストに my-resource-server.example.com (英語)  が含まれていない場合、検証は失敗します。

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

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

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

デフォルトの JWT 設定
  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2ResourceServer {
            jwt { }
        }
    }
    return http.build()
}

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

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

カスタム JWT 設定
  • Java

  • Kotlin

import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/messages/**").access(hasScope("message:read"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(myConverter())
                )
            );
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope

@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasScope("message:read"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtAuthenticationConverter = myConverter()
                }
            }
        }
        return http.build()
    }
}

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

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

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

JWT デコーダー
  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return JwtDecoders.fromIssuerLocation(issuerUri)
}
JwtDecoders#fromIssuerLocation (Javadoc)  を呼び出すと、JWK セット Uri を取得するためにプロバイダー構成または認可サーバーメタデータエンドポイントが呼び出されます。

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

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

または、Spring Boot をまったく使用していない場合は、これらのコンポーネント(フィルターチェーンと JwtDecoder)の両方を XML で指定できます。

フィルターチェーンは次のように指定されます。

デフォルトの JWT 設定
  • XML

<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="jwtDecoder"/>
    </oauth2-resource-server>
</http>

JwtDecoder は次のようになります。

JWT デコーダー
  • XML

<bean id="jwtDecoder"
        class="org.springframework.security.oauth2.jwt.JwtDecoders"
        factory-method="fromIssuerLocation">
    <constructor-arg value="${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}"/>
</bean>

jwkSetUri() を使用する

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

JWK Set Uri Configuration
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
                )
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
                }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.com/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

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

decoder() を使用する

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

JWT デコーダー構成
  • Java

  • Kotlin

  • XML

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(myCustomDecoder())
                )
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwtDecoder {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt {
                    jwtDecoder = myCustomDecoder()
                }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/**" access="authenticated"/>
    <oauth2-resource-server>
        <jwt decoder-ref="myCustomDecoder"/>
    </oauth2-resource-server>
</http>

これは、validationmapping、または request timeouts のようなより詳細な構成が必要な場合に便利です。

JwtDecoder の公開 @Bean

または、JwtDecoder @Bean を公開すると、decoder() と同じ効果があります。次のように jwkSetUri を使用して構築できます。

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build()
}

または、次のように、発行者を使用して、build() が呼び出されたときに NimbusJwtDecoder に jwkSetUri を検索させることもできます。

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(issuer).build()
}

または、デフォルトで問題がなければ、JwtDecoders を使用することもできます。これは、デコーダーのバリデーターの構成に加えて上記を行います。

  • Java

  • Kotlin

@Bean
public JwtDecoders jwtDecoder() {
    return JwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): JwtDecoders {
    return JwtDecoders.fromIssuerLocation(issuer)
}

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

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

これは、Spring BootNimbusJwtDecoder ビルダー、または JWK セットレスポンスからカスタマイズできます。

Spring Boot 経由

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

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

ビルダーを使用する

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

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).build()
}

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

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}

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

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithms(algorithms -> {
                    algorithms.add(RS512);
                    algorithms.add(ES512);
            }).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(this.issuer)
            .jwsAlgorithms {
                it.add(RS512)
                it.add(ES512)
            }.build()
}

JWK Set レスポンスから

Spring Security の JWT サポートは Nimbus に基づいているため、その優れた機能もすべて使用できます。

例: Nimbus には、JWK Set URI レスポンスに基づいてアルゴリズムのセットを選択する JWSKeySelector 実装があります。これを使用して、NimbusJwtDecoder を次のように生成できます。

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    // makes a request to the JWK Set endpoint
    JWSKeySelector<SecurityContext> jwsKeySelector =
            JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);

    DefaultJWTProcessor<SecurityContext> jwtProcessor =
            new DefaultJWTProcessor<>();
    jwtProcessor.setJWSKeySelector(jwsKeySelector);

    return new NimbusJwtDecoder(jwtProcessor);
}
@Bean
fun jwtDecoder(): JwtDecoder {
    // makes a request to the JWK Set endpoint
    val jwsKeySelector: JWSKeySelector<SecurityContext> = JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL<SecurityContext>(this.jwkSetUrl)
    val jwtProcessor: DefaultJWTProcessor<SecurityContext> = DefaultJWTProcessor()
    jwtProcessor.jwsKeySelector = jwsKeySelector
    return NimbusJwtDecoder(jwtProcessor)
}

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

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

Spring Boot 経由

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

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

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

  • Java

  • Kotlin

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

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

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

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

  • Java

  • Kotlin

@Value("${key.location}")
RSAPublicKey key;
@Value("\${key.location}")
val key: RSAPublicKey? = null

ビルダーを使用する

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

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withPublicKey(this.key).build()
}

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

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

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): JwtDecoder {
    return NimbusJwtDecoder.withSecretKey(key).build()
}

認可の構成

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

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

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

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

認可設定
  • Java

  • Kotlin

  • XML

import static org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/contacts/**").access(hasScope("contacts"))
                .requestMatchers("/messages/**").access(hasScope("messages"))
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }
}
import org.springframework.security.oauth2.core.authorization.OAuth2AuthorizationManagers.hasScope;

@Configuration
@EnableWebSecurity
class DirectlyConfiguredJwkSetUri {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/contacts/**", hasScope("contacts"))
                authorize("/messages/**", hasScope("messages"))
                authorize(anyRequest, authenticated)
            }
            oauth2ResourceServer {
                jwt { }
            }
        }
        return http.build()
    }
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"/>
    </oauth2-resource-server>
</http>

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

  • Java

  • Kotlin

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): List<Message> { }

権限の手動抽出

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

このため、Spring Security には Jwt を Authentication に変換する JwtAuthenticationConverter が同梱されています。デフォルトでは、Spring Security は JwtAuthenticationProvider を JwtAuthenticationConverter のデフォルトインスタンスにワイヤリングします。

JwtAuthenticationConverter の構成の一部として、Jwt から認可された権限の Collection に移行するための補助コンバーターを提供できます。

認可サーバーが authorities と呼ばれるカスタムクレームで権限と通信するとします。その場合、JwtAuthenticationConverter がインスペクションする必要があるクレームを次のように構成できます。

権限クレーム構成
  • Java

  • Kotlin

  • XML

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
    grantedAuthoritiesConverter.setAuthoritiesClaimName("authorities")

    val jwtAuthenticationConverter = JwtAuthenticationConverter()
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
    return jwtAuthenticationConverter
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authoritiesClaimName" value="authorities"/>
</bean>

権限のプレフィックスを異なるように構成することもできます。各権限の前に SCOPE_ を付ける代わりに、次のように ROLE_ に変更できます。

オーソリティのプレフィックス設定
  • Java

  • Kotlin

  • XML

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
    return jwtAuthenticationConverter;
}
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
    val grantedAuthoritiesConverter = JwtGrantedAuthoritiesConverter()
    grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_")

    val jwtAuthenticationConverter = JwtAuthenticationConverter()
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter)
    return jwtAuthenticationConverter
}
<http>
    <intercept-uri pattern="/contacts/**" access="hasAuthority('SCOPE_contacts')"/>
    <intercept-uri pattern="/messages/**" access="hasAuthority('SCOPE_messages')"/>
    <oauth2-resource-server>
        <jwt jwk-set-uri="https://idp.example.org/.well-known/jwks.json"
                jwt-authentication-converter-ref="jwtAuthenticationConverter"/>
    </oauth2-resource-server>
</http>

<bean id="jwtAuthenticationConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter">
    <property name="jwtGrantedAuthoritiesConverter" ref="jwtGrantedAuthoritiesConverter"/>
</bean>

<bean id="jwtGrantedAuthoritiesConverter"
        class="org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter">
    <property name="authorityPrefix" value="ROLE_"/>
</bean>

または、JwtGrantedAuthoritiesConverter#setAuthorityPrefix("") を呼び出して、プレフィックスを完全に削除できます。

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

  • Java

  • Kotlin

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return new CustomAuthenticationToken(jwt);
    }
}

// ...

@Configuration
@EnableWebSecurity
public class CustomAuthenticationConverterConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(new CustomAuthenticationConverter())
                )
            );
        return http.build();
    }
}
internal class CustomAuthenticationConverter : Converter<Jwt, AbstractAuthenticationToken> {
    override fun convert(jwt: Jwt): AbstractAuthenticationToken {
        return CustomAuthenticationToken(jwt)
    }
}

// ...

@Configuration
@EnableWebSecurity
class CustomAuthenticationConverterConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
       http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
           oauth2ResourceServer {
               jwt {
                   jwtAuthenticationConverter = CustomAuthenticationConverter()
               }
           }
        }
        return http.build()
    }
}

検証の構成

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

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

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

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

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

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

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
     NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
             JwtDecoders.fromIssuerLocation(issuerUri);

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

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder

    val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
            JwtTimestampValidator(Duration.ofSeconds(60)),
            JwtIssuerValidator(issuerUri))

    jwtDecoder.setJwtValidator(withClockSkew)

    return jwtDecoder
}
デフォルトでは、ResourceServer は 60 秒のクロックスキューを構成します。

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

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

  • Java

  • Kotlin

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new JwtClaimValidator<List<String>>(AUD, aud -> aud.contains("messaging"));
}
fun audienceValidator(): OAuth2TokenValidator<Jwt?> {
    return JwtClaimValidator<List<String>>(AUD) { aud -> aud.contains("messaging") }
}

または、より制御するために、独自の OAuth2TokenValidator を実装できます。

  • Java

  • Kotlin

static class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("custom_code", "Custom error message", null);

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

// ...

OAuth2TokenValidator<Jwt> audienceValidator() {
    return new AudienceValidator();
}
internal class AudienceValidator : OAuth2TokenValidator<Jwt> {
    var error: OAuth2Error = OAuth2Error("custom_code", "Custom error message", null)

    override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
        return if (jwt.audience.contains("messaging")) {
            OAuth2TokenValidatorResult.success()
        } else {
            OAuth2TokenValidatorResult.failure(error)
        }
    }
}

// ...

fun audienceValidator(): OAuth2TokenValidator<Jwt> {
    return AudienceValidator()
}

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

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
        JwtDecoders.fromIssuerLocation(issuerUri);

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

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri) as NimbusJwtDecoder

    val audienceValidator = audienceValidator()
    val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
    val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)

    jwtDecoder.setJwtValidator(withAudience)

    return jwtDecoder
}
前述したように、代わりに Boot で aud 検証を構成できます。

クレームセットマッピングの構成

Spring Security は、Nimbus (英語) ライブラリを使用して、JWT の構文解析と署名の検証を行います。Spring Security は、各フィールド値の Nimbus の解釈と、それぞれを Java 型に強制する方法の対象となります。

例: Nimbus は Java 7 と互換性があるため、タイムスタンプフィールドを表すために Instant を使用しません。

また、別のライブラリを使用したり、JWT 処理に使用したりすることもできます。これにより、調整が必要な独自の強制決定を行うことができます。

または、非常に単純に、リソースサーバーはドメイン固有の理由で JWT にクレームを追加または削除したい場合があります。

これらの目的のために、リソースサーバーは MappedJwtClaimSetConverter を使用した JWT クレームセットのマッピングをサポートしています。

単一クレームの変換のカスタマイズ

デフォルトでは、MappedJwtClaimSetConverter はクレームを次の型に強制しようとします。

クレーム

Java 型

aud

Collection<String>

exp

Instant

iat

Instant

iss

String

jti

String

nbf

Instant

sub

String

MappedJwtClaimSetConverter.withDefaults を使用して、個々のクレームの変換戦略を構成できます。

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();

    MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
            .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
    jwtDecoder.setClaimSetConverter(converter);

    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()

    val converter = MappedJwtClaimSetConverter
            .withDefaults(mapOf("sub" to this::lookupUserIdBySub))
    jwtDecoder.setClaimSetConverter(converter)

    return jwtDecoder
}

これにより、sub のデフォルトクレームコンバーターがオーバーライドされることを除き、すべてのデフォルトが保持されます。

クレームを追加する

MappedJwtClaimSetConverter は、たとえば既存のシステムに適応するために、カスタムクレームを追加するためにも使用できます。

  • Java

  • Kotlin

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
MappedJwtClaimSetConverter.withDefaults(mapOf("custom" to Converter<Any, String> { "value" }))

クレームの削除

また、同じ API を使用して、クレームを削除することも簡単です。

  • Java

  • Kotlin

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
MappedJwtClaimSetConverter.withDefaults(mapOf("legacyclaim" to Converter<Any, Any> { null }))

クレームの名前を変更する

一度に複数のクレームを参照したり、クレームの名前を変更したりするような、より洗練されたシナリオでは、リソースサーバーは Converter<Map<String, Object>, Map<String,Object>> を実装するクラスを受け入れます。

  • Java

  • Kotlin

public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);

        String username = (String) convertedClaims.get("user_name");
        convertedClaims.put("sub", username);

        return convertedClaims;
    }
}
class UsernameSubClaimAdapter : Converter<Map<String, Any?>, Map<String, Any?>> {
    private val delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap())
    override fun convert(claims: Map<String, Any?>): Map<String, Any?> {
        val convertedClaims = delegate.convert(claims)
        val username = convertedClaims["user_name"] as String
        convertedClaims["sub"] = username
        return convertedClaims
    }
}

そして、インスタンスは通常のように提供できます:

  • Java

  • Kotlin

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build();
    jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}
@Bean
fun jwtDecoder(): JwtDecoder {
    val jwtDecoder: NimbusJwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).build()
    jwtDecoder.setClaimSetConverter(UsernameSubClaimAdapter())
    return jwtDecoder
}

タイムアウトの構成

デフォルトでは、リソースサーバーは認可サーバーとの調整にそれぞれ 30 秒の接続およびソケットタイムアウトを使用します。

これはいくつかのシナリオでは短すぎるかもしれません。さらに、バックオフや発見などのより高度なパターンは考慮されません。

リソースサーバーが認可サーバーに接続する方法を調整するために、NimbusJwtDecoder は RestOperations のインスタンスを受け入れます。

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build();

    NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build();
    return jwtDecoder;
}
@Bean
fun jwtDecoder(builder: RestTemplateBuilder): JwtDecoder {
    val rest: RestOperations = builder
            .setConnectTimeout(Duration.ofSeconds(60))
            .setReadTimeout(Duration.ofSeconds(60))
            .build()
    return NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(rest).build()
}

また、デフォルトでは、リソースサーバーは認可サーバーの JWK セットを 5 分間メモリにキャッシュします。これは、調整する必要がある場合があります。さらに、エビクションや共有キャッシュの使用など、より高度なキャッシュパターンは考慮されません。

リソースサーバーが JWK セットをキャッシュする方法を調整するために、NimbusJwtDecoder は Cache のインスタンスを受け入れます。

  • Java

  • Kotlin

@Bean
public JwtDecoder jwtDecoder(CacheManager cacheManager) {
    return NimbusJwtDecoder.withIssuerLocation(issuer)
            .cache(cacheManager.getCache("jwks"))
            .build();
}
@Bean
fun jwtDecoder(cacheManager: CacheManager): JwtDecoder {
    return NimbusJwtDecoder.withIssuerLocation(issuer)
            .cache(cacheManager.getCache("jwks"))
            .build()
}

Cache を指定すると、リソースサーバーは JWK Set Uri をキーとして使用し、JWK Set JSON を値として使用します。

Spring はキャッシュプロバイダーではないため、spring-boot-starter-cache やお気に入りのキャッシュプロバイダーなどの適切な依存関係を含める必要があります。
ソケットタイムアウトかキャッシュタイムアウトかに関係なく、代わりに Nimbus を直接操作したい場合があります。そのためには、NimbusJwtDecoder に Nimbus の JWTProcessor を使用するコンストラクターが同梱されていることを覚えておいてください。