Add support for aud claim in resource server
See gh-29084
This commit is contained in:
committed by
Stephane Nicoll
parent
9025d1d250
commit
ee65627f4c
@@ -31,6 +31,7 @@ import org.springframework.util.StreamUtils;
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
* @author Artsiom Yudovin
|
||||
* @author Mushtaq Ahmed
|
||||
* @since 2.1.0
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.security.oauth2.resourceserver")
|
||||
@@ -71,6 +72,11 @@ public class OAuth2ResourceServerProperties {
|
||||
*/
|
||||
private Resource publicKeyLocation;
|
||||
|
||||
/**
|
||||
* Identifies the recipients that the JWT is intended for.
|
||||
*/
|
||||
private String audience;
|
||||
|
||||
public String getJwkSetUri() {
|
||||
return this.jwkSetUri;
|
||||
}
|
||||
@@ -103,6 +109,14 @@ public class OAuth2ResourceServerProperties {
|
||||
this.publicKeyLocation = publicKeyLocation;
|
||||
}
|
||||
|
||||
public String getAudience() {
|
||||
return this.audience;
|
||||
}
|
||||
|
||||
public void setAudience(String audience) {
|
||||
this.audience = audience;
|
||||
}
|
||||
|
||||
public String readPublicKey() throws IOException {
|
||||
String key = "spring.security.oauth2.resourceserver.public-key-location";
|
||||
Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null");
|
||||
|
||||
@@ -19,7 +19,9 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive
|
||||
import java.security.KeyFactory;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -32,8 +34,14 @@ import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
|
||||
@@ -49,6 +57,7 @@ import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
* @author Artsiom Yudovin
|
||||
* @author HaiTao Zhang
|
||||
* @author Anastasiia Losieva
|
||||
* @author Mushtaq Ahmed
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
@@ -69,10 +78,18 @@ class ReactiveOAuth2ResourceServerJwkConfiguration {
|
||||
NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder
|
||||
.withJwkSetUri(this.properties.getJwkSetUri())
|
||||
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
validators.add(new JwtTimestampValidator());
|
||||
String issuerUri = this.properties.getIssuerUri();
|
||||
if (issuerUri != null) {
|
||||
nimbusReactiveJwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri));
|
||||
validators.add(new JwtIssuerValidator(issuerUri));
|
||||
}
|
||||
String audience = this.properties.getAudience();
|
||||
if (audience != null) {
|
||||
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
|
||||
(aud) -> aud != null && aud.contains(audience)));
|
||||
}
|
||||
nimbusReactiveJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
|
||||
return nimbusReactiveJwtDecoder;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
@@ -33,10 +35,16 @@ import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoders;
|
||||
import org.springframework.security.oauth2.jwt.JwtValidators;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@@ -49,6 +57,7 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||
* @author Madhura Bhave
|
||||
* @author Artsiom Yudovin
|
||||
* @author HaiTao Zhang
|
||||
* @author Mushtaq Ahmed
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class OAuth2ResourceServerJwtConfiguration {
|
||||
@@ -68,10 +77,18 @@ class OAuth2ResourceServerJwtConfiguration {
|
||||
JwtDecoder jwtDecoderByJwkKeySetUri() {
|
||||
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
|
||||
.jwsAlgorithm(SignatureAlgorithm.from(this.properties.getJwsAlgorithm())).build();
|
||||
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
|
||||
validators.add(new JwtTimestampValidator());
|
||||
String issuerUri = this.properties.getIssuerUri();
|
||||
if (issuerUri != null) {
|
||||
nimbusJwtDecoder.setJwtValidator(JwtValidators.createDefaultWithIssuer(issuerUri));
|
||||
validators.add(new JwtIssuerValidator(issuerUri));
|
||||
}
|
||||
String audience = this.properties.getAudience();
|
||||
if (audience != null) {
|
||||
validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
|
||||
(aud) -> aud != null && aud.contains(audience)));
|
||||
}
|
||||
nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
|
||||
return nimbusJwtDecoder;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,9 @@ import org.springframework.security.core.userdetails.MapReactiveUserDetailsServi
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder;
|
||||
@@ -74,6 +76,7 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Artsiom Yudovin
|
||||
* @author HaiTao Zhang
|
||||
* @author Anastasiia Losieva
|
||||
* @author Mushtaq Ahmed
|
||||
*/
|
||||
class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
|
||||
@@ -387,6 +390,56 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
||||
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||
.getField(reactiveJwtDecoder, "jwtValidator");
|
||||
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
||||
.getField(jwtValidator, "tokenValidators");
|
||||
assertThat(tokenValidators).hasExactlyElementsOfTypes(JwtTimestampValidator.class);
|
||||
assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class);
|
||||
assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void autoConfigurationShouldConfigureIssuerAndAudienceJwtValidatorIfPropertyProvided() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
|
||||
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":"
|
||||
+ this.server.getPort() + "/" + path,
|
||||
"spring.security.oauth2.resourceserver.jwt.audience=http://test-audience.com")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
|
||||
ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
|
||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||
.getField(reactiveJwtDecoder, "jwtValidator");
|
||||
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
||||
.getField(jwtValidator, "tokenValidators");
|
||||
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class);
|
||||
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtClaimValidator.class);
|
||||
});
|
||||
}
|
||||
|
||||
private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
|
||||
MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
|
||||
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
|
||||
|
||||
@@ -48,8 +48,10 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.SupplierJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
|
||||
@@ -68,6 +70,7 @@ import static org.mockito.Mockito.mock;
|
||||
* @author Madhura Bhave
|
||||
* @author Artsiom Yudovin
|
||||
* @author HaiTao Zhang
|
||||
* @author Mushtaq Ahmed
|
||||
*/
|
||||
class OAuth2ResourceServerAutoConfigurationTests {
|
||||
|
||||
@@ -404,6 +407,56 @@ class OAuth2ResourceServerAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(JwtDecoder.class);
|
||||
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
|
||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||
.getField(jwtDecoder, "jwtValidator");
|
||||
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
||||
.getField(jwtValidator, "tokenValidators");
|
||||
assertThat(tokenValidators).hasExactlyElementsOfTypes(JwtTimestampValidator.class);
|
||||
assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class);
|
||||
assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void autoConfigurationShouldConfigureAudienceAndIssuerJwtValidatorIfPropertyProvided() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
String path = "test";
|
||||
String issuer = this.server.url(path).toString();
|
||||
String cleanIssuerPath = cleanIssuerPath(issuer);
|
||||
setupMockResponse(cleanIssuerPath);
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
|
||||
"spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":"
|
||||
+ this.server.getPort() + "/" + path,
|
||||
"spring.security.oauth2.resourceserver.jwt.audience=http://test-audience.com")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(JwtDecoder.class);
|
||||
JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
|
||||
DelegatingOAuth2TokenValidator<Jwt> jwtValidator = (DelegatingOAuth2TokenValidator<Jwt>) ReflectionTestUtils
|
||||
.getField(jwtDecoder, "jwtValidator");
|
||||
Collection<OAuth2TokenValidator<Jwt>> tokenValidators = (Collection<OAuth2TokenValidator<Jwt>>) ReflectionTestUtils
|
||||
.getField(jwtValidator, "tokenValidators");
|
||||
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class);
|
||||
assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtClaimValidator.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void jwtSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() {
|
||||
this.contextRunner
|
||||
|
||||
Reference in New Issue
Block a user