Skip to content

Validate the aud claim in OAuth2 resource server #29084

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
*
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author Mushtaq Ahmed
* @since 2.1.0
*/
@ConfigurationProperties(prefix = "spring.security.oauth2.resourceserver")
Expand Down Expand Up @@ -71,6 +72,11 @@ public static class Jwt {
*/
private Resource publicKeyLocation;

/**
* Identifies the recipients that the JWT is intended for.
*/
private String audience;
Copy link
Contributor

@chenrujun chenrujun Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the spring-cloud-gateway scenario, I suggest to use List<String> instead of String.

client -> spring-cloud-gateway -> resource-server-1
                               -> resource-server-2
                               -> etc..

When spring-cloud-gateway validate the access-token, the valid audience is a list of string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And rename variable from audience to audiences.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @chenrujun. This makes sense to me. The validation should then use a predicate that checks that at least one audience in the JWT matches one of the configured audiences. Does that makes sense to you, @mbhave?


public String getJwkSetUri() {
return this.jwkSetUri;
}
Expand Down Expand Up @@ -103,6 +109,14 @@ public void setPublicKeyLocation(Resource publicKeyLocation) {
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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
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;
Expand All @@ -32,8 +34,14 @@
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;
Expand All @@ -49,6 +57,7 @@
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Anastasiia Losieva
* @author Mushtaq Ahmed
*/
@Configuration(proxyBeanMethods = false)
class ReactiveOAuth2ResourceServerJwkConfiguration {
Expand All @@ -69,10 +78,18 @@ ReactiveJwtDecoder jwtDecoder() {
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
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;
Expand All @@ -34,10 +36,16 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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;
Expand All @@ -50,6 +58,7 @@
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Mushtaq Ahmed
*/
@Configuration(proxyBeanMethods = false)
class OAuth2ResourceServerJwtConfiguration {
Expand All @@ -69,10 +78,18 @@ static class JwtDecoderConfiguration {
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
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;
Expand All @@ -74,6 +76,7 @@
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Anastasiia Losieva
* @author Mushtaq Ahmed
*/
class ReactiveOAuth2ResourceServerAutoConfigurationTests {

Expand Down Expand Up @@ -387,6 +390,56 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri()
});
}

@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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@
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;
Expand All @@ -68,6 +70,7 @@
* @author Madhura Bhave
* @author Artsiom Yudovin
* @author HaiTao Zhang
* @author Mushtaq Ahmed
*/
class OAuth2ResourceServerAutoConfigurationTests {

Expand Down Expand Up @@ -404,6 +407,56 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri()
});
}

@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
Expand Down