From f3e630933a5bcdfc4c8a83284b9dfe68e775618a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 16 May 2016 19:21:52 -0700 Subject: [PATCH] Add OAuth PrincipalExtractor strategy interface Update `UserInfoTokenServices` to use a PrincipalExtractor interface to extract the principal. Fixes gh-5186 --- .../resource/FixedPrincipalExtractor.java | 43 +++++++++++++++++++ .../oauth2/resource/PrincipalExtractor.java | 37 ++++++++++++++++ ...ourceServerTokenServicesConfiguration.java | 22 ++++++++-- .../resource/UserInfoTokenServices.java | 22 +++++----- ...ServerTokenServicesConfigurationTests.java | 30 +++++++++++++ 5 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/FixedPrincipalExtractor.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/PrincipalExtractor.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/FixedPrincipalExtractor.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/FixedPrincipalExtractor.java new file mode 100644 index 00000000000..ca85d14bf6f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/FixedPrincipalExtractor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import java.util.Map; + +/** + * Default implementation of {@link PrincipalExtractor}. Extracts the principal from the + * map with well known keys. + * + * @author Phillip Webb + * @since 1.4.0 + */ +public class FixedPrincipalExtractor implements PrincipalExtractor { + + private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username", + "userid", "user_id", "login", "id", "name" }; + + @Override + public Object extractPrincipal(Map map) { + for (String key : PRINCIPAL_KEYS) { + if (map.containsKey(key)) { + return map.get(key); + } + } + return null; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/PrincipalExtractor.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/PrincipalExtractor.java new file mode 100644 index 00000000000..224b20ce581 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/PrincipalExtractor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource; + +import java.util.Map; + +/** + * Strategy used by {@link UserInfoTokenServices} to extract the principal from the + * resource server's response. + * + * @author Phillip Webb + * @since 1.4.0 + */ +public interface PrincipalExtractor { + + /** + * Extract the principal that should be used for the token. + * @param map the source map + * @return the extracted principal or {@code null} + */ + Object extractPrincipal(Map map); + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java index a724d6d6257..35f47ba844d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration.java @@ -129,14 +129,18 @@ public class ResourceServerTokenServicesConfiguration { private final AuthoritiesExtractor authoritiesExtractor; + private final PrincipalExtractor principalExtractor; + public SocialTokenServicesConfiguration(ResourceServerProperties sso, ObjectProvider> connectionFactoryProvider, UserInfoRestTemplateFactory restTemplateFactory, - ObjectProvider authoritiesExtractorProvider) { + ObjectProvider authoritiesExtractor, + ObjectProvider principalExtractor) { this.sso = sso; this.connectionFactory = connectionFactoryProvider.getIfAvailable(); this.restTemplate = restTemplateFactory.getUserInfoRestTemplate(); - this.authoritiesExtractor = authoritiesExtractorProvider.getIfAvailable(); + this.authoritiesExtractor = authoritiesExtractor.getIfAvailable(); + this.principalExtractor = principalExtractor.getIfAvailable(); } @Bean @@ -158,6 +162,9 @@ public class ResourceServerTokenServicesConfiguration { if (this.authoritiesExtractor != null) { services.setAuthoritiesExtractor(this.authoritiesExtractor); } + if (this.principalExtractor != null) { + services.setPrincipalExtractor(this.principalExtractor); + } return services; } @@ -174,12 +181,16 @@ public class ResourceServerTokenServicesConfiguration { private final AuthoritiesExtractor authoritiesExtractor; + private final PrincipalExtractor principalExtractor; + public UserInfoTokenServicesConfiguration(ResourceServerProperties sso, UserInfoRestTemplateFactory restTemplateFactory, - ObjectProvider authoritiesExtractorProvider) { + ObjectProvider authoritiesExtractor, + ObjectProvider principalExtractor) { this.sso = sso; this.restTemplate = restTemplateFactory.getUserInfoRestTemplate(); - this.authoritiesExtractor = authoritiesExtractorProvider.getIfAvailable(); + this.authoritiesExtractor = authoritiesExtractor.getIfAvailable(); + this.principalExtractor = principalExtractor.getIfAvailable(); } @Bean @@ -192,6 +203,9 @@ public class ResourceServerTokenServicesConfiguration { if (this.authoritiesExtractor != null) { services.setAuthoritiesExtractor(this.authoritiesExtractor); } + if (this.principalExtractor != null) { + services.setPrincipalExtractor(this.principalExtractor); + } return services; } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java index f09c60234ad..1fcdd44d415 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java @@ -35,6 +35,7 @@ import org.springframework.security.oauth2.common.exceptions.InvalidTokenExcepti import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.OAuth2Request; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; +import org.springframework.util.Assert; /** * {@link ResourceServerTokenServices} that uses a user info REST service. @@ -46,9 +47,6 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { protected final Log logger = LogFactory.getLog(getClass()); - private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username", - "userid", "user_id", "login", "id", "name" }; - private final String userInfoEndpointUrl; private final String clientId; @@ -59,6 +57,8 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor(); + private PrincipalExtractor principalExtractor = new FixedPrincipalExtractor(); + public UserInfoTokenServices(String userInfoEndpointUrl, String clientId) { this.userInfoEndpointUrl = userInfoEndpointUrl; this.clientId = clientId; @@ -73,9 +73,15 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { } public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) { + Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null"); this.authoritiesExtractor = authoritiesExtractor; } + public void setPrincipalExtractor(PrincipalExtractor principalExtractor) { + Assert.notNull(principalExtractor, "PrincipalExtractor must not be null"); + this.principalExtractor = principalExtractor; + } + @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { @@ -101,17 +107,13 @@ public class UserInfoTokenServices implements ResourceServerTokenServices { /** * Return the principal that should be used for the token. The default implementation - * looks for well know {@code user*} keys in the map. + * delegates to the {@link PrincipalExtractor}. * @param map the source map * @return the principal or {@literal "unknown"} */ protected Object getPrincipal(Map map) { - for (String key : PRINCIPAL_KEYS) { - if (map.containsKey(key)) { - return map.get(key); - } - } - return "unknown"; + Object principal = this.principalExtractor.extractPrincipal(map); + return (principal == null ? "unknown" : principal); } @Override diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java index 8870d7ee6be..68d071b6614 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfigurationTests.java @@ -125,6 +125,19 @@ public class ResourceServerTokenServicesConfigurationTests { .containsExactly(this.context.getBean(AuthoritiesExtractor.class)); } + @Test + public void userInfoWithPrincipal() { + EnvironmentTestUtils.addEnvironment(this.environment, + "security.oauth2.resource.userInfoUri:http://example.com"); + this.context = new SpringApplicationBuilder(PrincipalConfiguration.class) + .environment(this.environment).web(false).run(); + UserInfoTokenServices services = this.context + .getBean(UserInfoTokenServices.class); + assertThat(services).isNotNull(); + assertThat(services).extracting("principalExtractor") + .containsExactly(this.context.getBean(PrincipalExtractor.class)); + } + @Test public void userInfoWithClient() { EnvironmentTestUtils.addEnvironment(this.environment, @@ -228,6 +241,23 @@ public class ResourceServerTokenServicesConfigurationTests { } + @Configuration + protected static class PrincipalConfiguration extends ResourceConfiguration { + + @Bean + PrincipalExtractor authoritiesExtractor() { + return new PrincipalExtractor() { + + @Override + public Object extractPrincipal(Map map) { + return "boot"; + } + + }; + } + + } + @Import({ OAuth2RestOperationsConfiguration.class }) protected static class ResourceNoClientConfiguration extends ResourceConfiguration {