Add autoconfiguration for JWKTokenStore

If `jwk.key-set-uri` is present.

Closes gh-4437
pull/8317/head
Madhura Bhave 8 years ago
parent 01729cc1d2
commit b4134e239e

@ -35,6 +35,7 @@ import org.springframework.validation.Validator;
* Configuration properties for OAuth2 Resources. * Configuration properties for OAuth2 Resources.
* *
* @author Dave Syer * @author Dave Syer
* @author Madhura Bhave
* @since 1.3.0 * @since 1.3.0
*/ */
@ConfigurationProperties(prefix = "security.oauth2.resource") @ConfigurationProperties(prefix = "security.oauth2.resource")
@ -78,6 +79,8 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
private Jwt jwt = new Jwt(); private Jwt jwt = new Jwt();
private Jwk jwk = new Jwk();
/** /**
* The order of the filter chain used to authenticate tokens. Default puts it after * The order of the filter chain used to authenticate tokens. Default puts it after
* the actuator endpoints and before the default HTTP basic filter chain (catchall). * the actuator endpoints and before the default HTTP basic filter chain (catchall).
@ -158,6 +161,14 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
this.jwt = jwt; this.jwt = jwt;
} }
public Jwk getJwk() {
return this.jwk;
}
public void setJwk(Jwk jwk) {
this.jwk = jwk;
}
public String getClientId() { public String getClientId() {
return this.clientId; return this.clientId;
} }
@ -192,29 +203,40 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
return; return;
} }
ResourceServerProperties resource = (ResourceServerProperties) target; ResourceServerProperties resource = (ResourceServerProperties) target;
if (StringUtils.hasText(this.clientId)) {
if (!StringUtils.hasText(this.clientSecret)) { if ((StringUtils.hasText(this.jwt.getKeyUri())
if (!StringUtils.hasText(resource.getUserInfoUri())) { || StringUtils.hasText(this.jwt.getKeyValue()))
errors.rejectValue("userInfoUri", "missing.userInfoUri", && StringUtils.hasText(this.jwk.getKeySetUri())) {
"Missing userInfoUri (no client secret available)"); errors.reject("ambiguous.keyUri", "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should be configured.");
} }
}
else { else {
if (isPreferTokenInfo() if (StringUtils.hasText(this.clientId)) {
&& !StringUtils.hasText(resource.getTokenInfoUri())) { if (!StringUtils.hasText(this.clientSecret)) {
if (StringUtils.hasText(getJwt().getKeyUri())
|| StringUtils.hasText(getJwt().getKeyValue())) {
// It's a JWT decoder
return;
}
if (!StringUtils.hasText(resource.getUserInfoUri())) { if (!StringUtils.hasText(resource.getUserInfoUri())) {
errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri", errors.rejectValue("userInfoUri", "missing.userInfoUri",
"Missing tokenInfoUri and userInfoUri and there is no " "Missing userInfoUri (no client secret available)");
+ "JWT verifier key"); }
}
else {
if (isPreferTokenInfo()
&& !StringUtils.hasText(resource.getTokenInfoUri())) {
if (StringUtils.hasText(getJwt().getKeyUri())
|| StringUtils.hasText(getJwt().getKeyValue())
|| StringUtils.hasText(getJwk().getKeySetUri())) {
// It's a JWT decoder
return;
}
if (!StringUtils.hasText(resource.getUserInfoUri())) {
errors.rejectValue("tokenInfoUri", "missing.tokenInfoUri",
"Missing tokenInfoUri and userInfoUri and there is no "
+ "JWT verifier key");
}
} }
} }
} }
} }
} }
private int countBeans(Class<?> type) { private int countBeans(Class<?> type) {
@ -269,4 +291,22 @@ public class ResourceServerProperties implements Validator, BeanFactoryAware {
} }
public class Jwk {
/**
* The URI to get verification keys to verify the JWT token.
* This can be set when the authorization server returns a
* set of verification keys.
*/
private String keySetUri;
public String getKeySetUri() {
return this.keySetUri;
}
public void setKeySetUri(String keySetUri) {
this.keySetUri = keySetUri;
}
}
} }

@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.bind.RelaxedPropertyResolver; import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -61,6 +62,7 @@ import org.springframework.security.oauth2.provider.token.ResourceServerTokenSer
import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.support.OAuth2ConnectionFactory; import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
@ -73,6 +75,7 @@ import org.springframework.web.client.RestTemplate;
* Configuration for an OAuth2 resource server. * Configuration for an OAuth2 resource server.
* *
* @author Dave Syer * @author Dave Syer
* @author Madhura Bhave
* @since 1.3.0 * @since 1.3.0
*/ */
@Configuration @Configuration
@ -93,7 +96,7 @@ public class ResourceServerTokenServicesConfiguration {
} }
@Configuration @Configuration
@Conditional(NotJwtTokenCondition.class) @Conditional(RemoteTokenCondition.class)
protected static class RemoteTokenServicesConfiguration { protected static class RemoteTokenServicesConfiguration {
@Configuration @Configuration
@ -214,6 +217,30 @@ public class ResourceServerTokenServicesConfiguration {
} }
@Configuration
@Conditional(JwkCondition.class)
protected static class JwkTokenStoreConfiguration {
private final ResourceServerProperties resource;
public JwkTokenStoreConfiguration(ResourceServerProperties resource) {
this.resource = resource;
}
@Bean
@ConditionalOnMissingBean(ResourceServerTokenServices.class)
public DefaultTokenServices jwkTokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setTokenStore(jwkTokenStore());
return services;
}
@Bean
public TokenStore jwkTokenStore() {
return new JwkTokenStore(this.resource.getJwk().getKeySetUri());
}
}
@Configuration @Configuration
@Conditional(JwtTokenCondition.class) @Conditional(JwtTokenCondition.class)
protected static class JwtTokenServicesConfiguration { protected static class JwtTokenServicesConfiguration {
@ -341,32 +368,56 @@ public class ResourceServerTokenServicesConfiguration {
} }
private static class NotTokenInfoCondition extends SpringBootCondition { private static class JwkCondition extends SpringBootCondition {
private TokenInfoCondition tokenInfoCondition = new TokenInfoCondition();
@Override @Override
public ConditionOutcome getMatchOutcome(ConditionContext context, public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) { AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage
.forCondition("OAuth JWK Condition");
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
context.getEnvironment(), "security.oauth2.resource.jwk.");
String keyUri = resolver.getProperty("key-set-uri");
if (StringUtils.hasText(keyUri)) {
return ConditionOutcome
.match(message.foundExactly("provided jwk key set URI"));
}
return ConditionOutcome return ConditionOutcome
.inverse(this.tokenInfoCondition.getMatchOutcome(context, metadata)); .noMatch(message.didNotFind("key jwk set URI not provided").atAll());
} }
} }
private static class NotJwtTokenCondition extends SpringBootCondition { private static class NotTokenInfoCondition extends SpringBootCondition {
private JwtTokenCondition jwtTokenCondition = new JwtTokenCondition(); private TokenInfoCondition tokenInfoCondition = new TokenInfoCondition();
@Override @Override
public ConditionOutcome getMatchOutcome(ConditionContext context, public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) { AnnotatedTypeMetadata metadata) {
return ConditionOutcome return ConditionOutcome
.inverse(this.jwtTokenCondition.getMatchOutcome(context, metadata)); .inverse(this.tokenInfoCondition.getMatchOutcome(context, metadata));
} }
} }
private static class RemoteTokenCondition extends NoneNestedConditions {
RemoteTokenCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@Conditional(JwtTokenCondition.class)
static class HasJwtConfiguration {
}
@Conditional(JwkCondition.class)
static class HasJwkConfiguration {
}
}
static class AcceptJsonRequestInterceptor implements ClientHttpRequestInterceptor { static class AcceptJsonRequestInterceptor implements ClientHttpRequestInterceptor {
@Override @Override

@ -21,13 +21,21 @@ import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test; import org.junit.Test;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.validation.Errors;
import org.springframework.web.context.support.StaticWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat; 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;
/** /**
* Tests for {@link ResourceServerProperties}. * Tests for {@link ResourceServerProperties}.
* *
* @author Dave Syer * @author Dave Syer
* @author Vedran Pavic * @author Vedran Pavic
* @author Madhura Bhave
*/ */
public class ResourceServerPropertiesTests { public class ResourceServerPropertiesTests {
@ -59,4 +67,36 @@ public class ResourceServerPropertiesTests {
.isEqualTo("http://example.com/token_key"); .isEqualTo("http://example.com/token_key");
} }
@Test
public void validateWhenBothJwtAndJwtKeyConfigurationPresentShouldFail() throws Exception {
this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys");
this.properties.getJwt().setKeyUri("http://my-auth-server/token_key");
setListableBeanFactory();
Errors errors = mock(Errors.class);
this.properties.validate(this.properties, errors);
verify(errors).reject("ambiguous.keyUri", "Only one of jwt.keyUri (or jwt.keyValue) and jwk.keySetUri should be configured.");
}
@Test
public void validateWhenKeySetUriProvidedShouldSucceed() throws Exception {
this.properties.getJwk().setKeySetUri("http://my-auth-server/token_keys");
setListableBeanFactory();
Errors errors = mock(Errors.class);
this.properties.validate(this.properties, errors);
verifyZeroInteractions(errors);
}
private void setListableBeanFactory() {
ListableBeanFactory beanFactory = new StaticWebApplicationContext() {
@Override
public String[] getBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
if (type.isAssignableFrom(ResourceServerTokenServicesConfiguration.class)) {
return new String[]{"ResourceServerTokenServicesConfiguration"};
}
return new String[0];
}
};
this.properties.setBeanFactory(beanFactory);
}
} }

@ -21,8 +21,11 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.After; import org.junit.After;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
@ -61,6 +64,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link ResourceServerTokenServicesConfiguration}. * Tests for {@link ResourceServerTokenServicesConfiguration}.
* *
* @author Dave Syer * @author Dave Syer
* @author Madhura Bhave
*/ */
public class ResourceServerTokenServicesConfigurationTests { public class ResourceServerTokenServicesConfigurationTests {
@ -76,6 +80,9 @@ public class ResourceServerTokenServicesConfigurationTests {
private ConfigurableEnvironment environment = new StandardEnvironment(); private ConfigurableEnvironment environment = new StandardEnvironment();
@Rule
public ExpectedException thrown = ExpectedException.none();
@After @After
public void close() { public void close() {
if (this.context != null) { if (this.context != null) {
@ -186,6 +193,8 @@ public class ResourceServerTokenServicesConfigurationTests {
.environment(this.environment).web(false).run(); .environment(this.environment).web(false).run();
DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class); DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class);
assertThat(services).isNotNull(); assertThat(services).isNotNull();
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(RemoteTokenServices.class);
} }
@Test @Test
@ -198,6 +207,18 @@ public class ResourceServerTokenServicesConfigurationTests {
assertThat(services).isNotNull(); assertThat(services).isNotNull();
} }
@Test
public void jwkConfiguration() throws Exception {
EnvironmentTestUtils.addEnvironment(this.environment,
"security.oauth2.resource.jwk.key-set-uri=http://my-auth-server/token_keys");
this.context = new SpringApplicationBuilder(ResourceConfiguration.class)
.environment(this.environment).web(false).run();
DefaultTokenServices services = this.context.getBean(DefaultTokenServices.class);
assertThat(services).isNotNull();
this.thrown.expect(NoSuchBeanDefinitionException.class);
this.context.getBean(RemoteTokenServices.class);
}
@Test @Test
public void springSocialUserInfo() { public void springSocialUserInfo() {
EnvironmentTestUtils.addEnvironment(this.environment, EnvironmentTestUtils.addEnvironment(this.environment,

@ -168,7 +168,7 @@
<spring-retry.version>1.2.0.RELEASE</spring-retry.version> <spring-retry.version>1.2.0.RELEASE</spring-retry.version>
<spring-security.version>4.2.1.RELEASE</spring-security.version> <spring-security.version>4.2.1.RELEASE</spring-security.version>
<spring-security-jwt.version>1.0.7.RELEASE</spring-security-jwt.version> <spring-security-jwt.version>1.0.7.RELEASE</spring-security-jwt.version>
<spring-security-oauth.version>2.0.12.RELEASE</spring-security-oauth.version> <spring-security-oauth.version>2.0.13.BUILD-SNAPSHOT</spring-security-oauth.version>
<spring-session.version>1.3.0.RELEASE</spring-session.version> <spring-session.version>1.3.0.RELEASE</spring-session.version>
<spring-social.version>1.1.4.RELEASE</spring-social.version> <spring-social.version>1.1.4.RELEASE</spring-social.version>
<spring-social-facebook.version>2.0.3.RELEASE</spring-social-facebook.version> <spring-social-facebook.version>2.0.3.RELEASE</spring-social-facebook.version>

@ -21,29 +21,34 @@ import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
/** /**
* An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a
* {@link BindException}. * {@link BindException}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave
*/ */
class BindFailureAnalyzer extends AbstractFailureAnalyzer<BindException> { class BindFailureAnalyzer extends AbstractFailureAnalyzer<BindException> {
@Override @Override
protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) { protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
if (CollectionUtils.isEmpty(cause.getFieldErrors())) { if (CollectionUtils.isEmpty(cause.getAllErrors())) {
return null; return null;
} }
StringBuilder description = new StringBuilder( StringBuilder description = new StringBuilder(
String.format("Binding to target %s failed:%n", cause.getTarget())); String.format("Binding to target %s failed:%n", cause.getTarget()));
for (FieldError fieldError : cause.getFieldErrors()) { for (ObjectError error : cause.getAllErrors()) {
description.append(String.format("%n Property: %s", if (error instanceof FieldError) {
cause.getObjectName() + "." + fieldError.getField())); FieldError fieldError = (FieldError) error;
description.append(String.format("%n Property: %s",
cause.getObjectName() + "." + fieldError.getField()));
description.append(
String.format("%n Value: %s", fieldError.getRejectedValue()));
}
description.append( description.append(
String.format("%n Value: %s", fieldError.getRejectedValue())); String.format("%n Reason: %s%n", error.getDefaultMessage()));
description.append(
String.format("%n Reason: %s%n", fieldError.getDefaultMessage()));
} }
return new FailureAnalysis(description.toString(), return new FailureAnalysis(description.toString(),
"Update your application's configuration", cause); "Update your application's configuration", cause);

@ -32,6 +32,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -40,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link BindFailureAnalyzer}. * Tests for {@link BindFailureAnalyzer}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Madhura Bhave
*/ */
public class BindFailureAnalyzerTests { public class BindFailureAnalyzerTests {
@ -54,8 +57,8 @@ public class BindFailureAnalyzerTests {
} }
@Test @Test
public void bindExceptionDueToValidationFailure() { public void bindExceptionWithFieldErrorsDueToValidationFailure() {
FailureAnalysis analysis = performAnalysis(ValidationFailureConfiguration.class); FailureAnalysis analysis = performAnalysis(FieldValidationFailureConfiguration.class);
assertThat(analysis.getDescription()) assertThat(analysis.getDescription())
.contains(failure("test.foo.foo", "null", "may not be null")); .contains(failure("test.foo.foo", "null", "may not be null"));
assertThat(analysis.getDescription()) assertThat(analysis.getDescription())
@ -64,6 +67,13 @@ public class BindFailureAnalyzerTests {
.contains(failure("test.foo.nested.bar", "null", "may not be null")); .contains(failure("test.foo.nested.bar", "null", "may not be null"));
} }
@Test
public void bindExceptionWithObjectErrorsDueToValidationFailure() throws Exception {
FailureAnalysis analysis = performAnalysis(ObjectValidationFailureConfiguration.class);
assertThat(analysis.getDescription())
.contains("Reason: This object could not be bound.");
}
private static String failure(String property, String value, String reason) { private static String failure(String property, String value, String reason) {
return String.format("Property: %s%n Value: %s%n Reason: %s", property, return String.format("Property: %s%n Value: %s%n Reason: %s", property,
value, reason); value, reason);
@ -85,14 +95,19 @@ public class BindFailureAnalyzerTests {
} }
} }
@EnableConfigurationProperties(ValidationFailureProperties.class) @EnableConfigurationProperties(FieldValidationFailureProperties.class)
static class ValidationFailureConfiguration { static class FieldValidationFailureConfiguration {
}
@EnableConfigurationProperties(ObjectErrorFailureProperties.class)
static class ObjectValidationFailureConfiguration {
} }
@ConfigurationProperties("test.foo") @ConfigurationProperties("test.foo")
@Validated @Validated
static class ValidationFailureProperties { static class FieldValidationFailureProperties {
@NotNull @NotNull
private String foo; private String foo;
@ -144,4 +159,18 @@ public class BindFailureAnalyzerTests {
} }
@ConfigurationProperties("foo.bar")
static class ObjectErrorFailureProperties implements Validator {
@Override
public void validate(Object target, Errors errors) {
errors.reject("my.objectError", "This object could not be bound.");
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
} }

Loading…
Cancel
Save