diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java index 295abeda5f6..fac1ca580d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/ClientsConfiguredCondition.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.security.oauth2.client; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @@ -37,29 +38,44 @@ import org.springframework.core.type.AnnotatedTypeMetadata; */ public class ClientsConfiguredCondition extends SpringBootCondition { - private static final Bindable> STRING_REGISTRATION_MAP = Bindable - .mapOf(String.class, OAuth2ClientProperties.Registration.class); + private static final Bindable> STRING_LOGIN_REGISTRATION_MAP = Bindable + .mapOf(String.class, OAuth2ClientProperties.LoginClientRegistration.class); + + private static final Bindable> STRING_AUTHORIZATIONCODE_REGISTRATION_MAP = Bindable + .mapOf(String.class, + OAuth2ClientProperties.AuthorizationCodeClientRegistration.class); @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConditionMessage.Builder message = ConditionMessage .forCondition("OAuth2 Clients Configured Condition"); - Map registrations = getRegistrations( + Map registrations = getRegistrations( context.getEnvironment()); if (!registrations.isEmpty()) { - return ConditionOutcome.match(message - .foundExactly("registered clients " + registrations.values().stream() - .map(OAuth2ClientProperties.Registration::getClientId) + return ConditionOutcome.match(message.foundExactly( + "registered clients " + registrations.values().stream().map( + OAuth2ClientProperties.BaseClientRegistration::getClientId) .collect(Collectors.joining(", ")))); } return ConditionOutcome.noMatch(message.notAvailable("registered clients")); } - private Map getRegistrations( + private Map getRegistrations( Environment environment) { - return Binder.get(environment).bind("spring.security.oauth2.client.registration", - STRING_REGISTRATION_MAP).orElse(Collections.emptyMap()); + Map registrations = new HashMap(); + Map loginClientRegistrations = Binder + .get(environment).bind("spring.security.oauth2.client.registration.login", + STRING_LOGIN_REGISTRATION_MAP) + .orElse(Collections.emptyMap()); + Map authCodeClientRegistrations = Binder + .get(environment) + .bind("spring.security.oauth2.client.registration.authorizationcode", + STRING_AUTHORIZATIONCODE_REGISTRATION_MAP) + .orElse(Collections.emptyMap()); + registrations.putAll(loginClientRegistrations); + registrations.putAll(authCodeClientRegistrations); + return registrations; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java index 42f98ae73cb..3c83728e459 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java @@ -44,32 +44,106 @@ public class OAuth2ClientProperties { /** * OAuth client registrations. */ - private final Map registration = new HashMap<>(); + private final Registration registration = new Registration(); public Map getProvider() { return this.provider; } - public Map getRegistration() { + public Registration getRegistration() { return this.registration; } @PostConstruct public void validate() { - this.getRegistration().values().forEach(this::validateRegistration); + this.getRegistration().getLogin().values().forEach(this::validateRegistration); + this.getRegistration().getAuthorizationCode().values() + .forEach(this::validateRegistration); } - private void validateRegistration(Registration registration) { + private void validateRegistration(BaseClientRegistration registration) { if (!StringUtils.hasText(registration.getClientId())) { throw new IllegalStateException("Client id must not be empty."); } } - /** - * A single client registration. - */ public static class Registration { + /** + * OpenID Connect client registrations. + */ + private Map login = new HashMap<>(); + + /** + * OAuth2 authorization_code client registrations. + */ + private Map authorizationCode = new HashMap<>(); + + public Map getLogin() { + return this.login; + } + + public void setLogin(Map login) { + this.login = login; + } + + public Map getAuthorizationCode() { + return this.authorizationCode; + } + + public void setAuthorizationCode( + Map authorizationCode) { + this.authorizationCode = authorizationCode; + } + + } + + /** + * A single client registration for OpenID Connect login. + */ + public static class LoginClientRegistration extends BaseClientRegistration { + + /** + * Redirect URI. May be left blank when using a pre-defined provider. + */ + private String redirectUriTemplate; + + public String getRedirectUriTemplate() { + return this.redirectUriTemplate; + } + + public void setRedirectUriTemplate(String redirectUriTemplate) { + this.redirectUriTemplate = redirectUriTemplate; + } + + } + + /** + * A single client registration for OAuth2 authorization_code flow. + */ + public static class AuthorizationCodeClientRegistration + extends BaseClientRegistration { + + /** + * Redirect URI for the registration. + */ + private String redirectUri; + + public String getRedirectUri() { + return this.redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + } + + /** + * Base class for a single client registration. + */ + public static class BaseClientRegistration { + /** * Reference to the OAuth 2.0 provider to use. May reference an element from the * 'provider' property or used one of the commonly used providers (google, github, @@ -98,11 +172,6 @@ public class OAuth2ClientProperties { */ private String authorizationGrantType; - /** - * Redirect URI. May be left blank when using a pre-defined provider. - */ - private String redirectUriTemplate; - /** * Authorization scopes. May be left blank when using a pre-defined provider. */ @@ -153,14 +222,6 @@ public class OAuth2ClientProperties { this.authorizationGrantType = authorizationGrantType; } - public String getRedirectUriTemplate() { - return this.redirectUriTemplate; - } - - public void setRedirectUriTemplate(String redirectUriTemplate) { - this.redirectUriTemplate = redirectUriTemplate; - } - public Set getScope() { return this.scope; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesEnvironmentPostProcessor.java new file mode 100644 index 00000000000..05a1a5ce71c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesEnvironmentPostProcessor.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2018 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.client; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigFileApplicationListener; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.ConfigurationPropertyName; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; +import org.springframework.boot.context.properties.source.ConfigurationPropertySources; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; + +/** + * {@link EnvironmentPostProcessor} that migrates legacy OAuth2 login client properties + * under the `spring.security.oauth2.client.login` prefix. + * + * @author Madhura Bhave + * @since 2.1.0 + */ +public class OAuth2ClientPropertiesEnvironmentPostProcessor + implements EnvironmentPostProcessor, Ordered { + + private static final Bindable> STRING_LEGACY_REGISTRATION_MAP = Bindable + .mapOf(String.class, OAuth2ClientProperties.LoginClientRegistration.class); + + private static final String PREFIX = "spring.security.oauth2.client.registration"; + + private static final String LOGIN_REGISTRATION_PREFIX = PREFIX + ".login."; + + private static final String UPDATED_PROPERTY_SOURCE_SUFFIX = "-updated-oauth-client"; + + private int order = ConfigFileApplicationListener.DEFAULT_ORDER + 1; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, + SpringApplication application) { + environment.getPropertySources().forEach((propertySource) -> { + String name = propertySource.getName(); + Iterable sources = ConfigurationPropertySources + .from(propertySource); + ConfigurationPropertySource source = sources.iterator().next(); + Binder binder = new Binder(sources); + Map map = new LinkedHashMap<>(); + MapPropertySource updatedPropertySource = new MapPropertySource( + name + UPDATED_PROPERTY_SOURCE_SUFFIX, map); + Map registrations = binder + .bind(PREFIX, STRING_LEGACY_REGISTRATION_MAP) + .orElse(Collections.emptyMap()); + registrations.entrySet() + .forEach((entry) -> addProperties(entry, source, map)); + if (!map.isEmpty()) { + environment.getPropertySources().addBefore(name, updatedPropertySource); + } + }); + } + + private void addProperties( + Map.Entry entry, + ConfigurationPropertySource source, Map map) { + OAuth2ClientProperties.LoginClientRegistration registration = entry.getValue(); + String registrationId = entry.getKey(); + addProperty(registrationId, "client-id", registration::getClientId, map, source); + addProperty(registrationId, "client-secret", registration::getClientSecret, map, + source); + addProperty(registrationId, "client-name", registration::getClientName, map, + source); + addProperty(registrationId, "redirect-uri-template", + registration::getRedirectUriTemplate, map, source); + addProperty(registrationId, "authorization-grant-type", + registration::getAuthorizationGrantType, map, source); + addProperty(registrationId, "client-authentication-method", + registration::getClientAuthenticationMethod, map, source); + addProperty(registrationId, "provider", registration::getProvider, map, source); + addProperty(registrationId, "scope", registration::getScope, map, source); + } + + private void addProperty(String registrationId, String property, + Supplier valueSupplier, Map map, + ConfigurationPropertySource source) { + String registrationKey = PREFIX + "." + registrationId + "."; + String loginRegistrationKey = LOGIN_REGISTRATION_PREFIX + registrationId + "."; + if (source.getConfigurationProperty( + ConfigurationPropertyName.of(registrationKey + property)) != null) { + map.put(loginRegistrationKey + property, valueSupplier.get()); + } + } + + @Override + public int getOrder() { + return this.order; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java index d1ce147a3fc..59b433b215b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.Map; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.core.convert.ConversionException; @@ -51,19 +50,35 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { public static Map getClientRegistrations( OAuth2ClientProperties properties) { Map clientRegistrations = new HashMap<>(); - properties.getRegistration().forEach((key, value) -> clientRegistrations.put(key, - getClientRegistration(key, value, properties.getProvider()))); + properties.getRegistration().getLogin() + .forEach((key, value) -> clientRegistrations.put(key, + getLoginClientRegistration(key, value, + properties.getProvider()))); + properties.getRegistration().getAuthorizationCode() + .forEach((key, value) -> clientRegistrations.put(key, + getAuthorizationCodeClientRegistration(key, value, + properties.getProvider()))); return clientRegistrations; } - private static ClientRegistration getClientRegistration(String registrationId, - Registration properties, Map providers) { + private static ClientRegistration getAuthorizationCodeClientRegistration( + String registrationId, + OAuth2ClientProperties.AuthorizationCodeClientRegistration properties, + Map providers) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Builder builder = getBuilder(map, registrationId, properties, providers); + map.from(properties::getRedirectUri).to(builder::redirectUriTemplate); + return builder.build(); + } + + private static Builder getBuilder(PropertyMapper map, String registrationId, + OAuth2ClientProperties.BaseClientRegistration properties, + Map providers) { Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers); if (builder == null) { builder = getBuilder(registrationId, properties.getProvider(), providers); } - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties::getClientId).to(builder::clientId); map.from(properties::getClientSecret).to(builder::clientSecret); map.from(properties::getClientAuthenticationMethod) @@ -71,10 +86,18 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { .to(builder::clientAuthenticationMethod); map.from(properties::getAuthorizationGrantType).as(AuthorizationGrantType::new) .to(builder::authorizationGrantType); - map.from(properties::getRedirectUriTemplate).to(builder::redirectUriTemplate); map.from(properties::getScope).as((scope) -> StringUtils.toStringArray(scope)) .to(builder::scope); map.from(properties::getClientName).to(builder::clientName); + return builder; + } + + private static ClientRegistration getLoginClientRegistration(String registrationId, + OAuth2ClientProperties.LoginClientRegistration properties, + Map providers) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + Builder builder = getBuilder(map, registrationId, properties, providers); + map.from(properties::getRedirectUriTemplate).to(builder::redirectUriTemplate); return builder.build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java index ffd1f484b7a..caf65cae9e3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfiguration.java @@ -60,7 +60,8 @@ class OAuth2WebSecurityConfiguration { @Override protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests().anyRequest().authenticated().and().oauth2Login(); + http.authorizeRequests().anyRequest().authenticated().and().oauth2Login() + .and().oauth2Client().authorizationCodeGrant(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 5a45e710ae0..85dc74583d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -546,6 +546,15 @@ "name": "spring.session.hazelcast.flush-mode", "defaultValue": "on-save" }, + { + "name" : "spring.security.oauth2.client.registration", + "type" : "java.util.Map", + "description" : "Maps client registration-id to a client registration.", + "deprecation" : { + "replacement" : "spring.security.oauth2.client.registration.login", + "level" : "warning" + } + }, { "name": "spring.session.servlet.filter-dispatcher-types", "defaultValue": [ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 1070f995bd4..4cc1a3bf9fd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -7,6 +7,10 @@ org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingL org.springframework.context.ApplicationListener=\ org.springframework.boot.autoconfigure.BackgroundPreinitializer +# Environment Post Processors +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesEnvironmentPostProcessor + # Auto Configuration Import Listeners org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\ org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesEnvironmentPostProcessorTests.java new file mode 100644 index 00000000000..620d35ef382 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesEnvironmentPostProcessorTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2012-2018 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.client; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2ClientPropertiesEnvironmentPostProcessor}. + * + * @author Madhura Bhave + */ +public class OAuth2ClientPropertiesEnvironmentPostProcessorTests { + + private OAuth2ClientPropertiesEnvironmentPostProcessor postProcessor = new OAuth2ClientPropertiesEnvironmentPostProcessor(); + + private MockEnvironment environment; + + private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration.github-client."; + + private static final String ENVIRONMENT_REGISTRATION_PREFIX = "SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_GITHUB-CLIENT_"; + + private static final String LOGIN_REGISTRATION_PREFIX = "spring.security.oauth2.client.registration.login.github-client."; + + @Before + public void setup() { + this.environment = new MockEnvironment(); + } + + @Test + public void postProcessorWhenLegacyPropertiesShouldConvert() { + Map properties = new HashMap<>(); + properties.put(REGISTRATION_PREFIX + "client-id", "my-client-id"); + properties.put(REGISTRATION_PREFIX + "client-secret", "my-client-secret"); + properties.put(REGISTRATION_PREFIX + "redirect-uri-template", + "http://my-redirect-uri.com"); + properties.put(REGISTRATION_PREFIX + "provider", "github"); + properties.put(REGISTRATION_PREFIX + "scope", "user"); + properties.put(REGISTRATION_PREFIX + "client-name", "my-client-name"); + properties.put(REGISTRATION_PREFIX + "authorization-grant-type", + "authorization_code"); + properties.put(REGISTRATION_PREFIX + "client-authentication-method", "FORM"); + MapPropertySource source = new MapPropertySource("test", properties); + this.environment.getPropertySources().addFirst(source); + this.postProcessor.postProcessEnvironment(this.environment, null); + assertPropertyMigration(); + } + + @Test + public void postProcessorDoesNotCopyMissingProperties() { + Map properties = new HashMap<>(); + properties.put(REGISTRATION_PREFIX + "client-id", "my-client-id"); + MapPropertySource source = new MapPropertySource("test", properties); + this.environment.getPropertySources().addFirst(source); + this.postProcessor.postProcessEnvironment(this.environment, null); + assertThat(this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "client-id")) + .isEqualTo("my-client-id"); + assertThat( + this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "client-secret")) + .isNull(); + } + + @Test + public void postProcessorWhenLegacyEnvironmentVariablesPropertiesShouldConvert() { + Map properties = new HashMap<>(); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "CLIENTID", "my-client-id"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "CLIENTSECRET", + "my-client-secret"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "REDIRECTURITEMPLATE", + "http://my-redirect-uri.com"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "PROVIDER", "github"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "SCOPE", "user"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "CLIENTNAME", "my-client-name"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "AUTHORIZATIONGRANTTYPE", + "authorization_code"); + properties.put(ENVIRONMENT_REGISTRATION_PREFIX + "CLIENTAUTHENTICATIONMETHOD", + "FORM"); + SystemEnvironmentPropertySource source = new SystemEnvironmentPropertySource( + "test-" + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + properties); + this.environment.getPropertySources().addFirst(source); + this.postProcessor.postProcessEnvironment(this.environment, null); + assertPropertyMigration(); + } + + @Test + public void postProcessorWhenNewPropertiesShouldDoNothing() { + Map properties = new HashMap<>(); + properties.put(LOGIN_REGISTRATION_PREFIX + "client-id", "my-client-id"); + properties.put(LOGIN_REGISTRATION_PREFIX + "client-secret", "my-client-secret"); + properties.put(LOGIN_REGISTRATION_PREFIX + "redirect-uri-template", + "http://my-redirect-uri.com"); + properties.put(LOGIN_REGISTRATION_PREFIX + "provider", "github"); + properties.put(LOGIN_REGISTRATION_PREFIX + "scope", "user"); + properties.put(LOGIN_REGISTRATION_PREFIX + "client-name", "my-client-name"); + properties.put(LOGIN_REGISTRATION_PREFIX + "authorization-grant-type", + "authorization_code"); + properties.put(LOGIN_REGISTRATION_PREFIX + "client-authentication-method", + "FORM"); + MapPropertySource source = new MapPropertySource("test", properties); + this.environment.getPropertySources().addFirst(source); + MutablePropertySources propertySources = new MutablePropertySources( + this.environment.getPropertySources()); + this.postProcessor.postProcessEnvironment(this.environment, null); + assertPropertyMigration(); + assertThat(this.environment.getPropertySources()) + .containsExactlyElementsOf(propertySources); + } + + private void assertPropertyMigration() { + assertThat(this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "client-id")) + .isEqualTo("my-client-id"); + assertThat( + this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "client-secret")) + .isEqualTo("my-client-secret"); + assertThat(this.environment + .getProperty(LOGIN_REGISTRATION_PREFIX + "redirect-uri-template")) + .isEqualTo("http://my-redirect-uri.com"); + assertThat(this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "provider")) + .isEqualTo("github"); + assertThat(this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "scope")) + .isEqualTo("user"); + assertThat( + this.environment.getProperty(LOGIN_REGISTRATION_PREFIX + "client-name")) + .isEqualTo("my-client-name"); + assertThat(this.environment + .getProperty(LOGIN_REGISTRATION_PREFIX + "authorization-grant-type")) + .isEqualTo("authorization_code"); + assertThat(this.environment + .getProperty(LOGIN_REGISTRATION_PREFIX + "client-authentication-method")) + .isEqualTo("FORM"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java index 8e16a767fdc..4e6b1cfac80 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java @@ -28,8 +28,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.LoginClientRegistration; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -72,17 +72,17 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { provider.setUserInfoAuthenticationMethod("form"); provider.setUserNameAttribute("sub"); provider.setJwkSetUri("http://example.com/jwk"); - Registration registration = new Registration(); - registration.setProvider("provider"); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - registration.setClientAuthenticationMethod("post"); - registration.setAuthorizationGrantType("authorization_code"); - registration.setRedirectUriTemplate("http://example.com/redirect"); - registration.setScope(Collections.singleton("scope")); - registration.setClientName("clientName"); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setProvider("provider"); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + login.setClientAuthenticationMethod("post"); + login.setAuthorizationGrantType("authorization_code"); + login.setRedirectUriTemplate("http://example.com/redirect"); + login.setScope(Collections.singleton("scope")); + login.setClientName("clientName"); + properties.getRegistration().getLogin().put("registration", login); properties.getProvider().put("provider", provider); - properties.getRegistration().put("registration", registration); Map registrations = OAuth2ClientPropertiesRegistrationAdapter .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("registration"); @@ -114,11 +114,11 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { @Test public void getClientRegistrationsWhenUsingCommonProviderShouldAdapt() { OAuth2ClientProperties properties = new OAuth2ClientProperties(); - Registration registration = new Registration(); - registration.setProvider("google"); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - properties.getRegistration().put("registration", registration); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setProvider("google"); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + properties.getRegistration().getLogin().put("registration", login); Map registrations = OAuth2ClientPropertiesRegistrationAdapter .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("registration"); @@ -149,16 +149,16 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { @Test public void getClientRegistrationsWhenUsingCommonProviderWithOverrideShouldAdapt() { OAuth2ClientProperties properties = new OAuth2ClientProperties(); - Registration registration = new Registration(); - registration.setProvider("google"); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - registration.setClientAuthenticationMethod("post"); - registration.setAuthorizationGrantType("authorization_code"); - registration.setRedirectUriTemplate("http://example.com/redirect"); - registration.setScope(Collections.singleton("scope")); - registration.setClientName("clientName"); - properties.getRegistration().put("registration", registration); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setProvider("google"); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + login.setClientAuthenticationMethod("post"); + login.setAuthorizationGrantType("authorization_code"); + login.setRedirectUriTemplate("http://example.com/redirect"); + login.setScope(Collections.singleton("scope")); + login.setClientName("clientName"); + properties.getRegistration().getLogin().put("registration", login); Map registrations = OAuth2ClientPropertiesRegistrationAdapter .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("registration"); @@ -192,9 +192,9 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { @Test public void getClientRegistrationsWhenUnknownProviderShouldThrowException() { OAuth2ClientProperties properties = new OAuth2ClientProperties(); - Registration registration = new Registration(); - registration.setProvider("missing"); - properties.getRegistration().put("registration", registration); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setProvider("missing"); + properties.getRegistration().getLogin().put("registration", login); this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage("Unknown provider ID 'missing'"); OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties); @@ -203,10 +203,10 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { @Test public void getClientRegistrationsWhenProviderNotSpecifiedShouldUseRegistrationId() { OAuth2ClientProperties properties = new OAuth2ClientProperties(); - Registration registration = new Registration(); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - properties.getRegistration().put("google", registration); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + properties.getRegistration().getLogin().put("google", login); Map registrations = OAuth2ClientPropertiesRegistrationAdapter .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("google"); @@ -235,11 +235,47 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { assertThat(adapted.getClientName()).isEqualTo("Google"); } + @Test + public void getClientRegistrationsWhenAuhtorizationCodeClientShouldAdapt() { + OAuth2ClientProperties properties = new OAuth2ClientProperties(); + OAuth2ClientProperties.AuthorizationCodeClientRegistration registration = new OAuth2ClientProperties.AuthorizationCodeClientRegistration(); + registration.setClientId("clientId"); + registration.setClientSecret("clientSecret"); + registration.setRedirectUri("http://my-redirect-uri.com"); + properties.getRegistration().getAuthorizationCode().put("google", registration); + Map registrations = OAuth2ClientPropertiesRegistrationAdapter + .getClientRegistrations(properties); + ClientRegistration adapted = registrations.get("google"); + ProviderDetails adaptedProvider = adapted.getProviderDetails(); + assertThat(adaptedProvider.getAuthorizationUri()) + .isEqualTo("https://accounts.google.com/o/oauth2/v2/auth"); + assertThat(adaptedProvider.getTokenUri()) + .isEqualTo("https://www.googleapis.com/oauth2/v4/token"); + assertThat(adaptedProvider.getUserInfoEndpoint().getUri()) + .isEqualTo("https://www.googleapis.com/oauth2/v3/userinfo"); + assertThat(adaptedProvider.getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo( + org.springframework.security.oauth2.core.AuthenticationMethod.HEADER); + assertThat(adaptedProvider.getJwkSetUri()) + .isEqualTo("https://www.googleapis.com/oauth2/v3/certs"); + assertThat(adapted.getRegistrationId()).isEqualTo("google"); + assertThat(adapted.getClientId()).isEqualTo("clientId"); + assertThat(adapted.getClientSecret()).isEqualTo("clientSecret"); + assertThat(adapted.getRedirectUriTemplate()) + .isEqualTo("http://my-redirect-uri.com"); + assertThat(adapted.getClientAuthenticationMethod()).isEqualTo( + org.springframework.security.oauth2.core.ClientAuthenticationMethod.BASIC); + assertThat(adapted.getAuthorizationGrantType()).isEqualTo( + org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(adapted.getScopes()).containsExactly("openid", "profile", "email"); + assertThat(adapted.getClientName()).isEqualTo("Google"); + } + @Test public void getClientRegistrationsWhenProviderNotSpecifiedAndUnknownProviderShouldThrowException() { OAuth2ClientProperties properties = new OAuth2ClientProperties(); - Registration registration = new Registration(); - properties.getRegistration().put("missing", registration); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + properties.getRegistration().getLogin().put("missing", login); this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage( "Provider ID must be specified for client registration 'missing'"); @@ -249,20 +285,20 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { @Test public void oidcProviderConfigurationWhenProviderNotSpecifiedOnRegistration() throws Exception { - Registration registration = new Registration(); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - testOidcConfiguration(registration, "okta"); + LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + testOidcConfiguration(login, "okta"); } @Test public void oidcProviderConfigurationWhenProviderSpecifiedOnRegistration() throws Exception { - Registration registration = new Registration(); - registration.setProvider("okta-oidc"); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - testOidcConfiguration(registration, "okta-oidc"); + OAuth2ClientProperties.LoginClientRegistration login = new LoginClientRegistration(); + login.setProvider("okta-oidc"); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + testOidcConfiguration(login, "okta-oidc"); } @Test @@ -273,13 +309,13 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { String issuer = this.server.url("").toString(); String cleanIssuerPath = cleanIssuerPath(issuer); setupMockResponse(cleanIssuerPath); - Registration registration = new Registration(); - registration.setProvider("okta-oidc"); - registration.setClientId("clientId"); - registration.setClientSecret("clientSecret"); - registration.setClientAuthenticationMethod("post"); - registration.setRedirectUriTemplate("http://example.com/redirect"); - registration.setScope(Collections.singleton("user")); + OAuth2ClientProperties.LoginClientRegistration login = new OAuth2ClientProperties.LoginClientRegistration(); + login.setProvider("okta-oidc"); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + login.setClientAuthenticationMethod("post"); + login.setRedirectUriTemplate("http://example.com/redirect"); + login.setScope(Collections.singleton("user")); Provider provider = new Provider(); provider.setIssuerUri(issuer); provider.setAuthorizationUri("http://example.com/auth"); @@ -289,7 +325,7 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { provider.setJwkSetUri("http://example.com/jwk"); OAuth2ClientProperties properties = new OAuth2ClientProperties(); properties.getProvider().put("okta-oidc", provider); - properties.getRegistration().put("okta", registration); + properties.getRegistration().getLogin().put("okta", login); Map registrations = OAuth2ClientPropertiesRegistrationAdapter .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("okta"); @@ -313,8 +349,9 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { .isEqualTo("sub"); } - private void testOidcConfiguration(Registration registration, String providerId) - throws Exception { + private void testOidcConfiguration( + OAuth2ClientProperties.LoginClientRegistration registration, + String providerId) throws Exception { this.server = new MockWebServer(); this.server.start(); String issuer = this.server.url("").toString(); @@ -324,7 +361,7 @@ public class OAuth2ClientPropertiesRegistrationAdapterTests { Provider provider = new Provider(); provider.setIssuerUri(issuer); properties.getProvider().put(providerId, provider); - properties.getRegistration().put("okta", registration); + properties.getRegistration().getLogin().put("okta", registration); Map registrations = OAuth2ClientPropertiesRegistrationAdapter .getClientRegistrations(properties); ClientRegistration adapted = registrations.get("okta"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java index 75960d497f1..0b8f9cbe271 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesTests.java @@ -20,6 +20,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.AuthorizationCodeClientRegistration; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.LoginClientRegistration; + /** * Tests for {@link OAuth2ClientProperties}. * @@ -34,11 +37,11 @@ public class OAuth2ClientPropertiesTests { public ExpectedException thrown = ExpectedException.none(); @Test - public void clientIdAbsentThrowsException() { - OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration(); + public void clientIdAbsentForLoginClientsThrowsException() { + LoginClientRegistration registration = new LoginClientRegistration(); registration.setClientSecret("secret"); registration.setProvider("google"); - this.properties.getRegistration().put("foo", registration); + this.properties.getRegistration().getLogin().put("foo", registration); this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage("Client id must not be empty."); this.properties.validate(); @@ -46,10 +49,30 @@ public class OAuth2ClientPropertiesTests { @Test public void clientSecretAbsentShouldNotThrowException() { - OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration(); + LoginClientRegistration registration = new LoginClientRegistration(); registration.setClientId("foo"); registration.setProvider("google"); - this.properties.getRegistration().put("foo", registration); + this.properties.getRegistration().getLogin().put("foo", registration); + this.properties.validate(); + } + + @Test + public void clientIdAbsentForAuthorizationCodeClientsThrowsException() { + AuthorizationCodeClientRegistration registration = new AuthorizationCodeClientRegistration(); + registration.setClientSecret("secret"); + registration.setProvider("google"); + this.properties.getRegistration().getAuthorizationCode().put("foo", registration); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Client id must not be empty."); + this.properties.validate(); + } + + @Test + public void clientSecretAbsentForAuthorizationCodeClientDoesNotThrowException() { + AuthorizationCodeClientRegistration registration = new AuthorizationCodeClientRegistration(); + registration.setClientId("foo"); + registration.setProvider("google"); + this.properties.getRegistration().getAuthorizationCode().put("foo", registration); this.properties.validate(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java index 99c69ba4c9e..f6c391317df 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java @@ -37,7 +37,7 @@ public class ReactiveOAuth2ClientAutoConfigurationTests { .withConfiguration( AutoConfigurations.of(ReactiveOAuth2ClientAutoConfiguration.class)); - private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration"; + private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration.login"; @Test public void autoConfigurationShouldImportConfigurations() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientRegistrationRepositoryConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientRegistrationRepositoryConfigurationTests.java index 4ca71fc738f..749d7b3b9dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientRegistrationRepositoryConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientRegistrationRepositoryConfigurationTests.java @@ -33,7 +33,7 @@ public class ReactiveOAuth2ClientRegistrationRepositoryConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration"; + private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration.login"; @Test public void clientRegistrationRepositoryBeanShouldNotBeCreatedWhenPropertiesAbsent() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java index dcea65025c5..74dfe19e144 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java @@ -33,7 +33,7 @@ public class OAuth2ClientRegistrationRepositoryConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); - private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration"; + private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration.login"; @Test public void clientRegistrationRepositoryBeanShouldNotBeCreatedWhenPropertiesAbsent() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java index 4c5fd746cba..7e8466d9086 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java @@ -39,7 +39,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -68,7 +68,28 @@ public class OAuth2WebSecurityConfigurationTests { ClientRegistrationRepository expected = context .getBean(ClientRegistrationRepository.class); ClientRegistrationRepository actual = (ClientRegistrationRepository) ReflectionTestUtils - .getField(getAuthCodeFilters(context).get(0), + .getField( + getFilters(context, + OAuth2LoginAuthenticationFilter.class).get(0), + "clientRegistrationRepository"); + assertThat(isEqual(expected.findByRegistrationId("first"), + actual.findByRegistrationId("first"))).isTrue(); + assertThat(isEqual(expected.findByRegistrationId("second"), + actual.findByRegistrationId("second"))).isTrue(); + }); + } + + @Test + public void securityConfigurerConfiguresAuthorizationCode() { + this.contextRunner + .withUserConfiguration(ClientRegistrationRepositoryConfiguration.class, + OAuth2WebSecurityConfiguration.class) + .run((context) -> { + ClientRegistrationRepository expected = context + .getBean(ClientRegistrationRepository.class); + ClientRegistrationRepository actual = (ClientRegistrationRepository) ReflectionTestUtils + .getField(getFilters(context, + OAuth2AuthorizationCodeGrantFilter.class).get(0), "clientRegistrationRepository"); assertThat(isEqual(expected.findByRegistrationId("first"), actual.findByRegistrationId("first"))).isTrue(); @@ -79,10 +100,14 @@ public class OAuth2WebSecurityConfigurationTests { @Test public void securityConfigurerBacksOffWhenClientRegistrationBeanAbsent() { - this.contextRunner - .withUserConfiguration(TestConfig.class, - OAuth2WebSecurityConfiguration.class) - .run((context) -> assertThat(getAuthCodeFilters(context)).isEmpty()); + this.contextRunner.withUserConfiguration(TestConfig.class, + OAuth2WebSecurityConfiguration.class).run((context) -> { + assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)) + .isEmpty(); + assertThat( + getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)) + .isEmpty(); + }); } @Test @@ -107,7 +132,11 @@ public class OAuth2WebSecurityConfigurationTests { public void securityConfigurerBacksOffWhenOtherWebSecurityAdapterPresent() { this.contextRunner.withUserConfiguration(TestWebSecurityConfigurerConfig.class, OAuth2WebSecurityConfiguration.class).run((context) -> { - assertThat(getAuthCodeFilters(context)).isEmpty(); + assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)) + .isEmpty(); + assertThat( + getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)) + .isEmpty(); assertThat(context).getBean(OAuth2AuthorizedClientService.class) .isNotNull(); }); @@ -137,19 +166,14 @@ public class OAuth2WebSecurityConfigurationTests { } @SuppressWarnings("unchecked") - private List getAuthCodeFilters(AssertableApplicationContext context) { + private List getFilters(AssertableApplicationContext context, + Class filter) { FilterChainProxy filterChain = (FilterChainProxy) context .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); List filterChains = filterChain.getFilterChains(); List filters = (List) ReflectionTestUtils .getField(filterChains.get(0), "filters"); - List oauth2Filters = filters.stream() - .filter((f) -> f instanceof OAuth2LoginAuthenticationFilter - || f instanceof OAuth2AuthorizationRequestRedirectFilter) - .collect(Collectors.toList()); - return oauth2Filters.stream() - .filter((f) -> f instanceof OAuth2LoginAuthenticationFilter) - .collect(Collectors.toList()); + return filters.stream().filter(filter::isInstance).collect(Collectors.toList()); } private boolean isEqual(ClientRegistration reg1, ClientRegistration reg2) { diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 5d945496ddd..75c05c006d3 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -3219,36 +3219,18 @@ https://oauth.net/2/[OAuth2] is a widely used authorization framework that is su Spring. + [[boot-features-security-oauth2-client]] ==== Client If you have `spring-security-oauth2-client` on your classpath, you can take advantage of -some auto-configuration to make it easy to set up an OAuth2 Client. This configuration -makes use of the properties under `OAuth2ClientProperties`. The same properties are applicable -for both servlet and reactive applications. +some auto-configuration to make it easy to set up an OAuth2/Open ID Connect clients. This configuration +makes use of the properties under `OAuth2ClientProperties`. -You can register multiple OAuth2 clients and providers under the -`spring.security.oauth2.client` prefix, as shown in the following example: +You can register multiple OAuth2/OpenID Connect providers under the `spring.security.oauth2.client.provider` +prefix, as shown in the following example: [source,properties,indent=0] ---- - spring.security.oauth2.client.registration.my-client-1.client-id=abcd - spring.security.oauth2.client.registration.my-client-1.client-secret=password - spring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope - spring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider - spring.security.oauth2.client.registration.my-client-1.scope=user - spring.security.oauth2.client.registration.my-client-1.redirect-uri-template=http://my-redirect-uri.com - spring.security.oauth2.client.registration.my-client-1.client-authentication-method=basic - spring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization_code - - spring.security.oauth2.client.registration.my-client-2.client-id=abcd - spring.security.oauth2.client.registration.my-client-2.client-secret=password - spring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope - spring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider - spring.security.oauth2.client.registration.my-client-2.scope=email - spring.security.oauth2.client.registration.my-client-2.redirect-uri-template=http://my-redirect-uri.com - spring.security.oauth2.client.registration.my-client-2.client-authentication-method=basic - spring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code - spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=http://my-auth-server/oauth/authorize spring.security.oauth2.client.provider.my-oauth-provider.token-uri=http://my-auth-server/oauth/token spring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=http://my-auth-server/userinfo @@ -3257,6 +3239,48 @@ You can register multiple OAuth2 clients and providers under the spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name ---- +For OpenID Connect providers that support https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect discovery], +the configuration can be further simplified. The provider needs to be configured with an `issuer-uri` which is the +URI that the it asserts as its Issuer Identifier. For example, if the +`issuer-uri` provided is "https://example.com", then an `OpenID Provider Configuration Request` +will be made to "https://example.com/.well-known/openid-configuration". The result is expected +to be an `OpenID Provider Configuration Response`. The following example shows how an OpenID Connect +Provider can be configured with the `issuer-uri`: + +[source,properties,indent=0] +---- + spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/ +---- + + + +[[boot-features-security-oauth2-login-client-registration]] +===== OpenID Connect Login client registration + +You can register multiple Open ID Connect clients under the +`spring.security.oauth2.client.registration.login` prefix, as shown in the following example: + +[source,properties,indent=0] +---- + spring.security.oauth2.client.registration.login.my-client-1.client-id=abcd + spring.security.oauth2.client.registration.login.my-client-1.client-secret=password + spring.security.oauth2.client.registration.login.my-client-1.client-name=Client for user scope + spring.security.oauth2.client.registration.login.my-client-1.provider=my-oauth-provider + spring.security.oauth2.client.registration.login.my-client-1.scope=user + spring.security.oauth2.client.registration.login.my-client-1.redirect-uri-template=http://localhost:8080/login/oauth2/code/my-client-1 + spring.security.oauth2.client.registration.login.my-client-1.client-authentication-method=basic + spring.security.oauth2.client.registration.login.my-client-1.authorization-grant-type=authorization_code + + spring.security.oauth2.client.registration.login.my-client-2.client-id=abcd + spring.security.oauth2.client.registration.login.my-client-2.client-secret=password + spring.security.oauth2.client.registration.login.my-client-2.client-name=Client for email scope + spring.security.oauth2.client.registration.login.my-client-2.provider=my-oauth-provider + spring.security.oauth2.client.registration.login.my-client-2.scope=email + spring.security.oauth2.client.registration.login.my-client-2.redirect-uri-template=http://localhost:8080/login/oauth2/code/my-client-2 + spring.security.oauth2.client.registration.login.my-client-2.client-authentication-method=basic + spring.security.oauth2.client.registration.login.my-client-2.authorization-grant-type=authorization_code +---- + By default, Spring Security's `OAuth2LoginAuthenticationFilter` only processes URLs matching `/login/oauth2/code/*`. If you want to customize the `redirect-uri-template` to use a different pattern, you need to provide configuration to process that custom pattern. @@ -3280,6 +3304,40 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { } ---- +The same properties are applicable to both servlet and reactive applications. + + +[[boot-features-security-oauth2-authorization-code-client-registration]] +===== OAuth2 Authorization Code client registration + +You can register multiple OAuth2 `authorization_code` clients under the +`spring.security.oauth2.client.registration.authorization-code` prefix, as shown in the following example: + +[source,properties,indent=0] +---- + spring.security.oauth2.client.registration.authorization-code.my-client-1.client-id=abcd + spring.security.oauth2.client.registration.authorization-code.my-client-1.client-secret=password + spring.security.oauth2.client.registration.authorization-code.my-client-1.client-name=Client for user scope + spring.security.oauth2.client.registration.authorization-code.my-client-1.provider=my-oauth-provider + spring.security.oauth2.client.registration.authorization-code.my-client-1.scope=user + spring.security.oauth2.client.registration.authorization-code.my-client-1.redirect-uri=http://my-redirect-uri.com + spring.security.oauth2.client.registration.authorization-code.my-client-1.client-authentication-method=basic + spring.security.oauth2.client.registration.authorization-code.my-client-1.authorization-grant-type=authorization_code + + spring.security.oauth2.client.registration.authorization-code.my-client-2.client-id=abcd + spring.security.oauth2.client.registration.authorization-code.my-client-2.client-secret=password + spring.security.oauth2.client.registration.authorization-code.my-client-2.client-name=Client for email scope + spring.security.oauth2.client.registration.authorization-code.my-client-2.provider=my-oauth-provider + spring.security.oauth2.client.registration.authorization-code.my-client-2.scope=email + spring.security.oauth2.client.registration.authorization-code.my-client-2.redirect-uri=http://my-redirect-uri.com + spring.security.oauth2.client.registration.authorization-code.my-client-2.client-authentication-method=basic + spring.security.oauth2.client.registration.authorization-code.my-client-2.authorization-grant-type=authorization_code +---- + + + +[[boot-features-security-oauth2-common-providers]] +===== OAuth2 client registration for common providers For common OAuth2 and OpenID providers, including Google, Github, Facebook, and Okta, we provide a set of provider defaults (`google`, `github`, `facebook`, and `okta`, respectively). @@ -3292,27 +3350,12 @@ In other words, the two configurations in the following example use the Google p [source,properties,indent=0] ---- - spring.security.oauth2.client.registration.my-client.client-id=abcd - spring.security.oauth2.client.registration.my-client.client-secret=password - spring.security.oauth2.client.registration.my-client.provider=google + spring.security.oauth2.client.registration.login.my-client.client-id=abcd + spring.security.oauth2.client.registration.login.my-client.client-secret=password + spring.security.oauth2.client.registration.login.my-client.provider=google - spring.security.oauth2.client.registration.google.client-id=abcd - spring.security.oauth2.client.registration.google.client-secret=password ----- - -For OpenID Connect providers that support https://openid.net/specs/openid-connect-discovery-1_0.html[OpenID Connect discovery], -the configuration can be further simplified. The provider needs to be configured with an `issuer-uri` which is the -URI that the it asserts as its Issuer Identifier. For example, if the -`issuer-uri` provided is "https://example.com", then an `OpenID Provider Configuration Request` -will be made to "https://example.com/.well-known/openid-configuration". The result is expected -to be an `OpenID Provider Configuration Response`. The following example shows how an OpenID Connect -Provider can be configured with the `issuer-uri`: - -[source,properties,indent=0] ----- - spring.security.oauth2.client.registration.oidc-provider.client-id=abcd - spring.security.oauth2.client.registration.oidc-provider.client-secret=password - spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/ + spring.security.oauth2.client.registration.login.google.client-id=abcd + spring.security.oauth2.client.registration.login.google.client-secret=password ---- diff --git a/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml b/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml index 1a7bd711b3d..f496449922e 100644 --- a/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml +++ b/spring-boot-samples/spring-boot-sample-oauth2-client/src/main/resources/application.yml @@ -3,23 +3,32 @@ spring: oauth2: client: registration: - github-client-1: - client-id: ${APP-CLIENT-ID} - client-secret: ${APP-CLIENT-SECRET} - client-name: Github user - provider: github - scope: user - redirect-uri-template: http://localhost:8080/login/oauth2/code/github - github-client-2: - client-id: ${APP-CLIENT-ID} - client-secret: ${APP-CLIENT-SECRET} - client-name: Github email - provider: github - scope: user:email - redirect-uri-template: http://localhost:8080/login/oauth2/code/github - yahoo-oidc: - client-id: ${YAHOO-CLIENT-ID} - client-secret: ${YAHOO-CLIENT-SECRET} + login: + github-client-1: + client-id: ${APP-CLIENT-ID} + client-secret: ${APP-CLIENT-SECRET} + client-name: Github user + provider: github + scope: user + redirect-uri-template: http://localhost:8080/login/oauth2/code/github + github-client-2: + client-id: ${APP-CLIENT-ID} + client-secret: ${APP-CLIENT-SECRET} + client-name: Github email + provider: github + scope: user:email + redirect-uri-template: http://localhost:8080/login/oauth2/code/github + yahoo-oidc: + client-id: a + client-secret: b + authorization_code: + github-repos: + client-id: ${APP-CLIENT-ID} + client-secret: ${APP-CLIENT-SECRET} + scope: public_repo + redirect-uri: "{baseUrl}/github-repos" + provider: github + client-name: GitHub Repositories provider: yahoo-oidc: issuer-uri: https://api.login.yahoo.com/ \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java b/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java index 254dc314056..590a19dfbb8 100644 --- a/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-oauth2-client/src/test/java/sample/oauth2/client/SampleOAuth2ClientApplicationTests.java @@ -53,7 +53,7 @@ public class SampleOAuth2ClientApplicationTests { } @Test - public void loginShouldHaveBothOAuthClientsToChooseFrom() { + public void loginShouldHaveBothOAuth2LoginClientsToChooseFrom() { ResponseEntity entity = this.restTemplate.getForEntity("/login", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -62,4 +62,12 @@ public class SampleOAuth2ClientApplicationTests { assertThat(entity.getBody()).contains("/oauth2/authorization/github-client-2"); } + @Test + public void authorizationCodeClientIsPresent() { + ResponseEntity entity = this.restTemplate.getForEntity("/login", + String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).contains("/oauth2/authorization/github-repos"); + } + } diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml b/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml index 1a7bd711b3d..abad05db724 100644 --- a/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-client/src/main/resources/application.yml @@ -3,23 +3,24 @@ spring: oauth2: client: registration: - github-client-1: - client-id: ${APP-CLIENT-ID} - client-secret: ${APP-CLIENT-SECRET} - client-name: Github user - provider: github - scope: user - redirect-uri-template: http://localhost:8080/login/oauth2/code/github - github-client-2: - client-id: ${APP-CLIENT-ID} - client-secret: ${APP-CLIENT-SECRET} - client-name: Github email - provider: github - scope: user:email - redirect-uri-template: http://localhost:8080/login/oauth2/code/github - yahoo-oidc: - client-id: ${YAHOO-CLIENT-ID} - client-secret: ${YAHOO-CLIENT-SECRET} + login: + github-client-1: + client-id: ${APP-CLIENT-ID} + client-secret: ${APP-CLIENT-SECRET} + client-name: Github user + provider: github + scope: user + redirect-uri-template: http://localhost:8080/login/oauth2/code/github + github-client-2: + client-id: ${APP-CLIENT-ID} + client-secret: ${APP-CLIENT-SECRET} + client-name: Github email + provider: github + scope: user:email + redirect-uri-template: http://localhost:8080/login/oauth2/code/github + yahoo-oidc: + client-id: ${YAHOO-CLIENT-ID} + client-secret: ${YAHOO-CLIENT-SECRET} provider: yahoo-oidc: issuer-uri: https://api.login.yahoo.com/ \ No newline at end of file