OAuth 2.0 リソースサーバー JWT
JWT の最小依存関係
ほとんどのリソースサーバーサポートは spring-security-oauth2-resource-server
に収集されます。ただし、JWT のデコードと検証のサポートは spring-security-oauth2-jose
にあります。つまり、JWT でエンコードされたベアラートークンをサポートするリソースサーバーが機能するためには、両方が必要です。
JWT の最小構成
Spring Boot を使用する場合、アプリケーションをリソースサーバーとして構成することは、2 つの基本的な手順で構成されます。まず、必要な依存関係を含めます。次に、認可サーバーの場所を示します。
認可サーバーの指定
Spring Boot アプリケーションでは、使用する認証サーバーを指定する必要があります。
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
ここで、idp.example.com/issuer (英語)
は、認証サーバーが発行する JWT トークンの iss
クレームに含まれる値です。このリソースサーバーは、このプロパティを使用して、さらに自己構成し、認可サーバーの公開鍵を検出し、その後、受信 JWT を検証します。
|
スタートアップの期待
このプロパティとこれらの依存関係を使用すると、ResourceServer は JWT でエンコードされたベアラートークンを検証するように自動的に構成されます。
これは、決定論的な起動プロセスを通じてこれを実現します。
プロバイダー構成または認可サーバーのメタデータエンドポイントにアクセスし、
jwks_url
プロパティのレスポンスを処理します。有効な公開鍵を
jwks_url
に照会するように検証戦略を構成します。検証戦略を構成して、各 JWT の
iss
クレームをidp.example.com (英語)
に対して検証します。
このプロセスの結果、リソースサーバーが正常に起動するには、認可サーバーがリクエストを受信している必要があります。
リソースサーバーが認証サーバーにクエリを実行したときに認証サーバーがダウンしている場合(適切なタイムアウトが指定されている場合)、起動は失敗します。 |
ランタイムの期待
アプリケーションが起動すると、ResourceServer は Authorization: Bearer
ヘッダーを含むすべてのリクエストを処理しようとします。
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
このスキームが示されている限り、ResourceServer は BearerToken 仕様に従ってリクエストを処理しようとします。
整形式の JWT、リソースサーバーを考えると:
起動時に
jwks_url
エンドポイントから取得され、JWT ヘッダーと照合された公開鍵に対して署名を検証します。JWT の
exp
とnbf
のタイムスタンプと JWT のiss
クレームを検証します。各スコープを接頭辞
SCOPE_
の権限にマップします。
認可サーバーが新しいキーを利用できるようになると、Spring Security は JWT トークンの検証に使用されるキーを自動的にローテーションします。 |
デフォルトでは、結果の Authentication#getPrincipal
は Spring Security Jwt
オブジェクトであり、Authentication#getName
は JWT の sub
プロパティ(存在する場合)にマップされます。
ここから、次へのジャンプを検討してください。
認可サーバー 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 を実行しません。リソースサーバーが受信 JWT に対する iss
クレームを検証するように、引き続き issuer-uri
を指定します。
このプロパティは、DSL で直接指定できます。 |
Boot 自動構成のオーバーライドまたは置換
Spring Boot は、ResourceServer に代わって 2 つの @Bean
オブジェクトを生成します。
最初の Bean は、アプリケーションをリソースサーバーとして構成する SecurityWebFilterChain
です。spring-security-oauth2-jose
を含めると、この SecurityWebFilterChain
は次のようになります。
Java
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
アプリケーションが SecurityWebFilterChain
Bean を公開しない場合、Spring Boot はデフォルトのもの(前のリストに示されている)を公開します。
これを置き換えるには、アプリケーション内で @Bean
を公開します。
Java
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/message/**").access(hasScope("message:read"))
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(withDefaults())
);
return http.build();
}
import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/message/**", hasScope("message:read"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
上記の構成では、/messages/
で始まる URL に対して message:read
のスコープが必要です。
oauth2ResourceServer
DSL のメソッドも、自動構成をオーバーライドまたは置換します。
例: Spring Boot が 2 番目に作成する @Bean
は、String
トークンを Jwt
の検証済みインスタンスにデコードする ReactiveJwtDecoder
です。
Java
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri)
}
|
その構成は、jwkSetUri()
を使用してオーバーライドするか、decoder()
を使用して置き換えることができます。
jwkSetUri()
を使用する
認可サーバーの JWK セット URI を構成プロパティとして構成するか、DSL で提供することができます。
Java
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = "https://idp.example.com/.well-known/jwks.json"
}
}
}
}
jwkSetUri()
の使用は、構成プロパティよりも優先されます。
decoder()
を使用する
decoder()
は、JwtDecoder
の Spring Boot 自動構成を完全に置き換えるため、jwkSetUri()
よりも強力です。
Java
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(myCustomDecoder())
)
);
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtDecoder = myCustomDecoder()
}
}
}
}
これは、検証など、より詳細な構成が必要な場合に便利です。
ReactiveJwtDecoder
の公開 @Bean
代わりに、ReactiveJwtDecoder
@Bean
を公開すると、decoder()
と同じ効果があります: 次のように jwkSetUri
で構築できます:
Java
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
}
または、次のように、発行者を使用して、build()
が呼び出されたときに NimbusReactiveJwtDecoder
に jwkSetUri
を検索させることもできます。
Java
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(issuer).build()
}
または、デフォルトで問題がなければ、JwtDecoders
を使用することもできます。これは、デコーダーのバリデーターの構成に加えて上記を行います。
Java
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuer);
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return ReactiveJwtDecoders.fromIssuerLocation(issuer)
}
信頼できるアルゴリズムの構成
デフォルトでは、NimbusReactiveJwtDecoder
、つまりリソースサーバーは、RS256
を使用するトークンのみを信頼および検証します。
この動作は、Spring Boot を使用するか、NimbusJwtDecoder ビルダーを使用してカスタマイズできます。
Spring Boot を使用した信頼できるアルゴリズムのカスタマイズ
アルゴリズムを設定する最も簡単な方法は、プロパティとしてです:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithms: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
Builder を使用した信頼できるアルゴリズムのカスタマイズ
ただし、より強力にするには、NimbusReactiveJwtDecoder
に同梱されているビルダーを使用できます。
Java
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).build()
}
jwsAlgorithm
を複数回呼び出すと、NimbusReactiveJwtDecoder
は複数のアルゴリズムを信頼するように構成されます。
Java
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.issuer)
.jwsAlgorithm(RS512).jwsAlgorithm(ES512).build()
}
または、jwsAlgorithms
を呼び出すこともできます。
Java
Kotlin
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(ES512);
}).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withIssuerLocation(this.jwkSetUri)
.jwsAlgorithms {
it.add(RS512)
it.add(ES512)
}
.build()
}
単一の非対称キーを信頼する
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: ConfigurableListableBeanFactory ->
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
を直接接続するには、適切な NimbusReactiveJwtDecoder
ビルダーを使用します。
Java
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withPublicKey(this.key).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withPublicKey(key).build()
}
単一の対称キーを信頼する
単一の対称鍵を使用することもできます。SecretKey
をロードして、適切な NimbusReactiveJwtDecoder
ビルダーを使用できます。
Java
Kotlin
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build()
}
認可の構成
OAuth 2.0 認可サーバーから発行される JWT には、通常、scope
属性または scp
属性のいずれかがあり、付与されたスコープ(または権限)を示します。たとえば、次のようになります。
{ ..., "scope" : "messages contacts"}
この場合、リソースサーバーは、これらのスコープを許可された権限のリストに強制的に入れ、各スコープの前に文字列 SCOPE_
を付けます。
これは、JWT から派生したスコープでエンドポイントまたはメソッドを保護するために、対応する式に次のプレフィックスを含める必要があることを意味します。
Java
Kotlin
import static org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.mvcMatchers("/contacts/**").access(hasScope("contacts"))
.mvcMatchers("/messages/**").access(hasScope("messages"))
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
return http.build();
}
import org.springframework.security.oauth2.core.authorization.OAuth2ReactiveAuthorizationManagers.hasScope
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize("/contacts/**", hasScope("contacts"))
authorize("/messages/**", hasScope("messages"))
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt { }
}
}
}
メソッドセキュリティと同様のことができます。
Java
Kotlin
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
@PreAuthorize("hasAuthority('SCOPE_messages')")
fun getMessages(): Flux<Message> { }
権限の手動抽出
ただし、このデフォルトでは不十分な状況がいくつかあります。例: 一部の認証サーバーは scope
属性を使用しません。代わりに、独自のカスタム属性があります。また、リソースサーバーは、属性または属性の構成を内部化された権限に適合させる必要がある場合があります。
このために、DSL は jwtAuthenticationConverter()
を公開します:
Java
Kotlin
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(grantedAuthoritiesExtractor())
)
);
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2ResourceServer {
jwt {
jwtAuthenticationConverter = grantedAuthoritiesExtractor()
}
}
}
}
fun grantedAuthoritiesExtractor(): Converter<Jwt, Mono<AbstractAuthenticationToken>> {
val jwtAuthenticationConverter = JwtAuthenticationConverter()
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor())
return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter)
}
jwtAuthenticationConverter()
は、Jwt
を Authentication
に変換するロールを果たします。その構成の一部として、Jwt
から付与された権限の Collection
に移行するための補助コンバーターを提供できます。
その最終的なコンバーターは、次の GrantedAuthoritiesExtractor
のようなものになる可能性があります。
Java
Kotlin
static class GrantedAuthoritiesExtractor
implements Converter<Jwt, Collection<GrantedAuthority>> {
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<?> authorities = (Collection<?>)
jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList());
return authorities.stream()
.map(Object::toString)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
internal class GrantedAuthoritiesExtractor : Converter<Jwt, Collection<GrantedAuthority>> {
override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
val authorities: List<Any> = jwt.claims
.getOrDefault("mycustomclaim", emptyList<Any>()) as List<Any>
return authorities
.map { it.toString() }
.map { SimpleGrantedAuthority(it) }
}
}
柔軟性を高めるため、DSL はコンバーターを Converter<Jwt, Mono<AbstractAuthenticationToken>>
を実装するクラスに完全に置き換えることをサポートしています。
Java
Kotlin
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return Mono.just(jwt).map(this::doConversion);
}
}
internal class CustomAuthenticationConverter : Converter<Jwt, Mono<AbstractAuthenticationToken>> {
override fun convert(jwt: Jwt): Mono<AbstractAuthenticationToken> {
return Mono.just(jwt).map(this::doConversion)
}
}
検証の構成
リソースサーバーは、認可サーバーの発行者 URI を示す最小限の Spring Boot 構成を使用して、デフォルトで iss
クレームと exp
および nbf
タイムスタンプクレームを検証します。
検証のニーズをカスタマイズする必要がある状況では、リソースサーバーには 2 つの標準バリデーターが付属しており、カスタム OAuth2TokenValidator
インスタンスも受け入れます。
タイムスタンプ検証のカスタマイズ
JWT インスタンスには通常、有効期間があり、ウィンドウの開始は nbf
クレームで示され、終了は exp
クレームで示されます。
ただし、すべてのサーバーでクロックドリフトが発生する可能性があります。これにより、あるサーバーではトークンの有効期限が切れているように見えますが、別のサーバーでは有効期限が切れていないように見えます。分散システムで協調するサーバーの数が増えるため、これにより実装の胸焼けが発生する可能性があります。
リソースサーバーは JwtTimestampValidator
を使用してトークンの有効性ウィンドウを検証し、clockSkew
を使用してトークンを構成して、クロックドリフトの問題を軽減できます。
Java
Kotlin
@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;
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
val withClockSkew: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(
JwtTimestampValidator(Duration.ofSeconds(60)),
JwtIssuerValidator(issuerUri))
jwtDecoder.setJwtValidator(withClockSkew)
return jwtDecoder
}
デフォルトでは、ResourceServer は 60 秒のクロックスキューを構成します。 |
カスタム検証ツールの構成
OAuth2TokenValidator
API を使用して、aud
クレームのチェックを追加できます。
Java
Kotlin
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);
}
}
}
class AudienceValidator : OAuth2TokenValidator<Jwt> {
var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null)
override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
return if (jwt.audience.contains("messaging")) {
OAuth2TokenValidatorResult.success()
} else {
OAuth2TokenValidatorResult.failure(error)
}
}
}
次に、リソースサーバーに追加するために、ReactiveJwtDecoder
インスタンスを指定できます。
Java
Kotlin
@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;
}
@Bean
fun jwtDecoder(): ReactiveJwtDecoder {
val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder
val audienceValidator: OAuth2TokenValidator<Jwt> = AudienceValidator()
val withIssuer: OAuth2TokenValidator<Jwt> = JwtValidators.createDefaultWithIssuer(issuerUri)
val withAudience: OAuth2TokenValidator<Jwt> = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator)
jwtDecoder.setJwtValidator(withAudience)
return jwtDecoder
}