diff --git a/config/src/main/java/org/springframework/security/config/Customizer.java b/config/src/main/java/org/springframework/security/config/Customizer.java new file mode 100644 index 00000000000..3b8a02ab2d2 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/Customizer.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.security.config; + +/** + * Callback interface that accepts a single input argument and returns no result, + * with the ability to throw a (checked) exception. + * + * @author Eleftheria Stein + * @param the type of the input to the operation + * @since 5.2 + */ +@FunctionalInterface +public interface Customizer { + + /** + * Performs the customizations on the input argument. + * + * @param t the input argument + * @throws Exception if any error occurs + */ + void customize(T t) throws Exception; + + /** + * Returns a {@link Customizer} that does not alter the input argument. + * + * @return a {@link Customizer} that does not alter the input argument. + */ + static Customizer withDefaults() { + return t -> {}; + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index bbf2edc1fab..e5e588a40a3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -19,6 +19,7 @@ import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.SecurityBuilder; @@ -1094,6 +1095,41 @@ public HttpBasicConfigurer httpBasic() throws Exception { return getOrApply(new HttpBasicConfigurer<>()); } + /** + * Configures HTTP Basic authentication. + * + *

Example Configuration

+ * + * The example below demonstrates how to configure HTTP Basic authentication for an + * application. The default realm is "Realm", but can be + * customized using {@link HttpBasicConfigurer#realmName(String)}. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class HttpBasicSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.httpBasic(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * @param httpBasicCustomizer the {@link Customizer} to provide more options for + * the {@link HttpBasicConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity httpBasic(Customizer> httpBasicCustomizer) throws Exception { + httpBasicCustomizer.customize(getOrApply(new HttpBasicConfigurer<>())); + return HttpSecurity.this; + } + public void setSharedObject(Class sharedType, C object) { super.setSharedObject(sharedType, object); } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java index a42fac317a1..a6445aa35b8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurerTests.java @@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -91,6 +92,37 @@ public O postProcess(O object) { } } + @Test + public void httpBasicWhenUsingDefaultsInLambdaThenResponseIncludesBasicChallenge() throws Exception { + this.spring.register(DefaultsLambdaEntryPointConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()) + .andExpect(header().string("WWW-Authenticate", "Basic realm=\"Realm\"")); + } + + @EnableWebSecurity + static class DefaultsLambdaEntryPointConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic(withDefaults()); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + } + //SEC-2198 @Test public void httpBasicWhenUsingDefaultsThenResponseIncludesBasicChallenge() throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.java index 34e8b57241a..83d05080573 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpBasicTests.java @@ -38,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -102,6 +103,36 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void basicAuthenticationWhenUsingDefaultsInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(HttpBasicLambdaConfig.class, UserConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()); + + this.mvc.perform(get("/") + .with(httpBasic("user", "invalid"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Realm\"")); + + this.mvc.perform(get("/") + .with(httpBasic("user", "password"))) + .andExpect(status().isNotFound()); + } + + @EnableWebSecurity + static class HttpBasicLambdaConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .httpBasic(withDefaults()); + // @formatter:on + } + } + /** * http@realm equivalent */ @@ -127,6 +158,30 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void basicAuthenticationWhenUsingCustomRealmInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(CustomHttpBasicLambdaConfig.class, UserConfig.class).autowire(); + + this.mvc.perform(get("/") + .with(httpBasic("user", "invalid"))) + .andExpect(status().isUnauthorized()) + .andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Custom Realm\"")); + } + + @EnableWebSecurity + static class CustomHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .httpBasic(httpBasicConfig -> httpBasicConfig.realmName("Custom Realm")); + // @formatter:on + } + } + /** * http/http-basic@authentication-details-source-ref equivalent */ @@ -161,6 +216,40 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void basicAuthenticationWhenUsingAuthenticationDetailsSourceRefInLambdaThenMatchesNamespace() + throws Exception { + this.spring.register(AuthenticationDetailsSourceHttpBasicLambdaConfig.class, UserConfig.class).autowire(); + + AuthenticationDetailsSource source = + this.spring.getContext().getBean(AuthenticationDetailsSource.class); + + this.mvc.perform(get("/") + .with(httpBasic("user", "password"))); + + verify(source).buildDetails(any(HttpServletRequest.class)); + } + + @EnableWebSecurity + static class AuthenticationDetailsSourceHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter { + AuthenticationDetailsSource authenticationDetailsSource = + mock(AuthenticationDetailsSource.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(httpBasicConfig -> + httpBasicConfig.authenticationDetailsSource(this.authenticationDetailsSource)); + // @formatter:on + } + + @Bean + AuthenticationDetailsSource authenticationDetailsSource() { + return this.authenticationDetailsSource; + } + } + /** * http/http-basic@entry-point-ref */ @@ -195,4 +284,38 @@ protected void configure(HttpSecurity http) throws Exception { .authenticationEntryPoint(this.authenticationEntryPoint); } } + + @Test + public void basicAuthenticationWhenUsingEntryPointRefInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(EntryPointRefHttpBasicLambdaConfig.class, UserConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().is(999)); + + this.mvc.perform(get("/") + .with(httpBasic("user", "invalid"))) + .andExpect(status().is(999)); + + this.mvc.perform(get("/") + .with(httpBasic("user", "password"))) + .andExpect(status().isNotFound()); + } + + @EnableWebSecurity + static class EntryPointRefHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter { + AuthenticationEntryPoint authenticationEntryPoint = + (request, response, ex) -> response.setStatus(999); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .httpBasic(httpBasicConfig -> + httpBasicConfig.authenticationEntryPoint(this.authenticationEntryPoint)); + // @formatter:on + } + } }