diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java index 4da99ade0cd..c4fbdd7bc6e 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource; +import javax.annotation.PostConstruct; + import com.fasterxml.jackson.annotation.JsonIgnore; import org.springframework.beans.BeansException; @@ -28,8 +30,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; import org.springframework.util.StringUtils; -import org.springframework.validation.Errors; -import org.springframework.validation.Validator; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; /** * Configuration properties for OAuth2 Resources. @@ -39,7 +42,7 @@ import org.springframework.validation.Validator; * @since 1.3.0 */ @ConfigurationProperties(prefix = "security.oauth2.resource") -public class ResourceServerProperties implements Validator, BeanFactoryAware { +public class ResourceServerProperties implements BeanFactoryAware { @JsonIgnore private final String clientId; @@ -185,13 +188,8 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { this.filterOrder = filterOrder; } - @Override - public boolean supports(Class clazz) { - return ResourceServerProperties.class.isAssignableFrom(clazz); - } - - @Override - public void validate(Object target, Errors errors) { + @PostConstruct + public void validate() throws Exception { if (countBeans(AuthorizationServerEndpointsConfiguration.class) > 0) { // If we are an authorization server we don't need remote resource token // services @@ -202,14 +200,25 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { // resource token services return; } - ResourceServerProperties resource = (ResourceServerProperties) target; - validate(resource, errors); - } - private void validate(ResourceServerProperties target, Errors errors) { if (!StringUtils.hasText(this.clientId)) { return; } + + BindingResult result = getBindingResult(); + if (result.hasErrors()) { + throw new BindException(result); + } + } + + private int countBeans(Class type) { + return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, type, + true, false).length; + } + + private BindingResult getBindingResult() { + BindingResult errors = new BeanPropertyBindingResult(this, "resourceServerProperties"); + boolean jwtConfigPresent = StringUtils.hasText(this.jwt.getKeyUri()) || StringUtils.hasText(this.jwt.getKeyValue()); boolean jwkConfigPresent = StringUtils.hasText(this.jwk.getKeySetUri()); @@ -219,29 +228,21 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware { "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should" + " be configured."); } - else { - if (jwtConfigPresent || jwkConfigPresent) { - // It's a JWT decoder - return; - } - if (!StringUtils.hasText(target.getUserInfoUri()) - && !StringUtils.hasText(target.getTokenInfoUri())) { + if (!jwtConfigPresent && !jwkConfigPresent) { + if (!StringUtils.hasText(this.userInfoUri) + && !StringUtils.hasText(this.tokenInfoUri)) { errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri", "Missing tokenInfoUri and userInfoUri and there is no " + "JWT verifier key"); } - if (StringUtils.hasText(target.getTokenInfoUri()) && isPreferTokenInfo()) { + if (StringUtils.hasText(this.tokenInfoUri) && this.isPreferTokenInfo()) { if (!StringUtils.hasText(this.clientSecret)) { errors.rejectValue("clientSecret", "missing.clientSecret", "Missing client secret"); } } } - } - - private int countBeans(Class type) { - return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, type, - true, false).length; + return errors; } public class Jwt { diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java index d78cfe3ef01..bcc6c7378d9 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerPropertiesTests.java @@ -19,15 +19,21 @@ package org.springframework.boot.autoconfigure.security.oauth2.resource; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.validation.BindException; import org.springframework.validation.Errors; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; import org.springframework.web.context.support.StaticWebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; /** @@ -44,6 +50,9 @@ public class ResourceServerPropertiesTests { private Errors errors = mock(Errors.class); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test @SuppressWarnings("unchecked") public void json() throws Exception { @@ -59,7 +68,7 @@ public class ResourceServerPropertiesTests { public void validateWhenClientIdNullShouldNotFail() throws Exception { this.properties = new ResourceServerProperties(null, "secret"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -68,9 +77,10 @@ public class ResourceServerPropertiesTests { this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys"); this.properties.getJwt().setKeyUri("http://my-auth-server/token_key"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); - verify(this.errors).reject("ambiguous.keyUri", - "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should be configured."); + this.thrown.expect(BindException.class); + this.thrown.expect(getMatcher("Only one of jwt.keyUri (or jwt.keyValue) " + + "and jwk.keySetUri should be configured.", null)); + this.properties.validate(); } @Test @@ -79,16 +89,18 @@ public class ResourceServerPropertiesTests { this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys"); this.properties.getJwt().setKeyValue("my-key"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); - verify(this.errors).reject("ambiguous.keyUri", - "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should be configured."); + + this.thrown.expect(BindException.class); + this.thrown.expect(getMatcher("Only one of jwt.keyUri (or jwt.keyValue) " + + "and jwk.keySetUri should be configured.", null)); + this.properties.validate(); } @Test public void validateWhenJwkKeySetUriProvidedShouldSucceed() throws Exception { this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -96,7 +108,7 @@ public class ResourceServerPropertiesTests { public void validateWhenKeyValuePresentShouldSucceed() throws Exception { this.properties.getJwt().setKeyValue("my-key"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -106,7 +118,7 @@ public class ResourceServerPropertiesTests { this.properties = new ResourceServerProperties("client", ""); this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -114,16 +126,17 @@ public class ResourceServerPropertiesTests { public void validateWhenKeyConfigAbsentAndInfoUrisNotConfiguredShouldFail() throws Exception { setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); - verify(this.errors).rejectValue("tokenInfoUri", "missing.tokenInfoUri", - "Missing tokenInfoUri and userInfoUri and there is no JWT verifier key"); + this.thrown.expect(BindException.class); + this.thrown.expect(getMatcher("Missing tokenInfoUri and userInfoUri and there" + + " is no JWT verifier key", "tokenInfoUri")); + this.properties.validate(); } @Test public void validateWhenTokenUriConfiguredShouldNotFail() throws Exception { this.properties.setTokenInfoUri("http://my-auth-server/userinfo"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -131,7 +144,7 @@ public class ResourceServerPropertiesTests { public void validateWhenUserInfoUriConfiguredShouldNotFail() throws Exception { this.properties.setUserInfoUri("http://my-auth-server/userinfo"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -142,9 +155,9 @@ public class ResourceServerPropertiesTests { this.properties.setTokenInfoUri("http://my-auth-server/check_token"); this.properties.setUserInfoUri("http://my-auth-server/userinfo"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); - verify(this.errors).rejectValue("clientSecret", "missing.clientSecret", - "Missing client secret"); + this.thrown.expect(BindException.class); + this.thrown.expect(getMatcher("Missing client secret", "clientSecret")); + this.properties.validate(); } @Test @@ -153,7 +166,7 @@ public class ResourceServerPropertiesTests { this.properties = new ResourceServerProperties("client", ""); this.properties.setUserInfoUri("http://my-auth-server/userinfo"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -165,7 +178,7 @@ public class ResourceServerPropertiesTests { this.properties.setTokenInfoUri("http://my-auth-server/check_token"); this.properties.setUserInfoUri("http://my-auth-server/userinfo"); setListableBeanFactory(); - this.properties.validate(this.properties, this.errors); + this.properties.validate(); verifyZeroInteractions(this.errors); } @@ -186,4 +199,25 @@ public class ResourceServerPropertiesTests { this.properties.setBeanFactory(beanFactory); } + private BaseMatcher getMatcher(String message, String field) { + return new BaseMatcher() { + @Override + public void describeTo(Description description) { + + } + + @Override + public boolean matches(Object item) { + BindException ex = (BindException) item; + ObjectError error = ex.getAllErrors().get(0); + boolean messageMatches = message.equals(error.getDefaultMessage()); + if (field == null) { + return messageMatches; + } + String fieldErrors = ((FieldError) error).getField(); + return messageMatches && fieldErrors.equals(field); + } + }; + } + }