From 92314b0956295a42123b5b6d7e0cd6a7124ff6c8 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 19 Jun 2019 12:55:26 -0400 Subject: [PATCH 01/23] Allow configuration of logout through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 47 ++++++++++ .../configurers/LogoutConfigurerTests.java | 90 ++++++++++++++++++- .../configurers/NamespaceHttpLogoutTests.java | 74 +++++++++++++++ 3 files changed, 209 insertions(+), 2 deletions(-) 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 e5e588a40a3..565acac30bd 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 @@ -773,6 +773,53 @@ public LogoutConfigurer logout() throws Exception { return getOrApply(new LogoutConfigurer<>()); } + /** + * Provides logout support. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL + * "/logout" will log the user out by invalidating the HTTP Session, cleaning up any + * {@link #rememberMe()} authentication that was configured, clearing the + * {@link SecurityContextHolder}, and then redirect to "/login?success". + * + *

Example Custom Configuration

+ * + * The following customization to log out when the URL "/custom-logout" is invoked. + * Log out will remove the cookie named "remove", not invalidate the HttpSession, + * clear the SecurityContextHolder, and upon completion redirect to "/logout-success". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class LogoutSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.formLogin()
+	 * 				.and()
+	 * 			// sample logout customization
+	 * 			.logout(logout ->
+	 * 				logout.deleteCookies("remove")
+	 * 					.invalidateHttpSession(false)
+	 * 					.logoutUrl("/custom-logout")
+	 * 					.logoutSuccessUrl("/logout-success")
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param logoutCustomizer the {@link Customizer} to provide more options for + * the {@link LogoutConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity logout(Customizer> logoutCustomizer) throws Exception { + logoutCustomizer.customize(getOrApply(new LogoutConfigurer<>())); + return HttpSecurity.this; + } + /** * Allows configuring how an anonymous user is represented. This is automatically * applied when used in conjunction with {@link WebSecurityConfigurerAdapter}. By diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java index 3ef62bf4a8f..86bf87a6bed 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java @@ -37,10 +37,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -77,6 +82,26 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void configureWhenDefaultLogoutSuccessHandlerForHasNullLogoutHandlerInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(NullLogoutSuccessHandlerInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class NullLogoutSuccessHandlerInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> + logout.defaultLogoutSuccessHandlerFor(null, mock(RequestMatcher.class)) + ); + // @formatter:on + } + } + @Test public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherThenException() { assertThatThrownBy(() -> this.spring.register(NullMatcherConfig.class).autowire()) @@ -96,6 +121,26 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void configureWhenDefaultLogoutSuccessHandlerForHasNullMatcherInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(NullMatcherInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class NullMatcherInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> + logout.defaultLogoutSuccessHandlerFor(mock(LogoutSuccessHandler.class), null) + ); + // @formatter:on + } + } + @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnLogoutFilter() { this.spring.register(ObjectPostProcessorConfig.class).autowire(); @@ -263,6 +308,29 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void logoutWhenCustomLogoutUrlInLambdaThenRedirectsToLogin() throws Exception { + this.spring.register(CsrfDisabledAndCustomLogoutInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/custom/logout")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")); + } + + @EnableWebSecurity + static class CsrfDisabledAndCustomLogoutInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .disable() + .logout(logout -> logout.logoutUrl("/custom/logout")); + // @formatter:on + } + } + // SEC-3170 @Test public void configureWhenLogoutHandlerNullThenException() { @@ -283,6 +351,24 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void configureWhenLogoutHandlerNullInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(NullLogoutHandlerInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class NullLogoutHandlerInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> logout.addLogoutHandler(null)); + // @formatter:on + } + } + // SEC-3170 @Test public void rememberMeWhenRememberMeServicesNotLogoutHandlerThenRedirectsToLogin() throws Exception { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java index cd0bf72decd..a1ad03748d5 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpLogoutTests.java @@ -41,9 +41,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Tests to verify that all the functionality of attributes is present @@ -83,6 +85,23 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + @WithMockUser + public void logoutWhenDisabledInLambdaThenRespondsWithNotFound() throws Exception { + this.spring.register(HttpLogoutDisabledInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/logout").with(csrf()).with(user("user"))) + .andExpect(status().isNotFound()); + } + + @EnableWebSecurity + static class HttpLogoutDisabledInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.logout(AbstractHttpConfigurer::disable); + } + } + /** * http/logout custom */ @@ -112,6 +131,35 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + @WithMockUser + public void logoutWhenUsingVariousCustomizationsInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(CustomHttpLogoutInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/custom-logout").with(csrf())) + .andExpect(authenticated(false)) + .andExpect(redirectedUrl("/logout-success")) + .andExpect(result -> assertThat(result.getResponse().getCookies()).hasSize(1)) + .andExpect(cookie().maxAge("remove", 0)) + .andExpect(session(Objects::nonNull)); + } + + @EnableWebSecurity + static class CustomHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(logout -> + logout.deleteCookies("remove") + .invalidateHttpSession(false) + .logoutUrl("/custom-logout") + .logoutSuccessUrl("/logout-success") + ); + // @formatter:on + } + } + /** * http/logout@success-handler-ref */ @@ -141,6 +189,32 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + @WithMockUser + public void logoutWhenUsingSuccessHandlerRefInLambdaThenMatchesNamespace() throws Exception { + this.spring.register(SuccessHandlerRefHttpLogoutInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/logout").with(csrf())) + .andExpect(authenticated(false)) + .andExpect(redirectedUrl("/SuccessHandlerRefHttpLogoutConfig")) + .andExpect(noCookies()) + .andExpect(session(Objects::isNull)); + } + + @EnableWebSecurity + static class SuccessHandlerRefHttpLogoutInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); + logoutSuccessHandler.setDefaultTargetUrl("/SuccessHandlerRefHttpLogoutConfig"); + + // @formatter:off + http + .logout(logout -> logout.logoutSuccessHandler(logoutSuccessHandler)); + // @formatter:on + } + } + ResultMatcher authenticated(boolean authenticated) { return result -> assertThat( Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) From 1a31376ddaedda8808e1f8fbde96ea5eea392132 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 19 Jun 2019 14:24:20 -0400 Subject: [PATCH 02/23] Allow configuration of exception handling through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 39 +++++++++++++ ...ingConfigurerAccessDeniedHandlerTests.java | 43 +++++++++++++- ...aceHttpServerAccessDeniedHandlerTests.java | 58 +++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) 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 565acac30bd..341515f5c32 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 @@ -681,6 +681,45 @@ public ExceptionHandlingConfigurer exceptionHandling() throws Exce return getOrApply(new ExceptionHandlingConfigurer<>()); } + /** + * Allows configuring exception handling. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. + * + *

Example Custom Configuration

+ * + * The following customization will ensure that users who are denied access are forwarded + * to the page "/errors/access-denied". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class ExceptionHandlingSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			// sample exception handling customization
+	 * 			.exceptionHandling(exceptionHandling ->
+	 * 				exceptionHandling
+	 * 					.accessDeniedPage("/errors/access-denied")
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param exceptionHandlingCustomizer the {@link Customizer} to provide more options for + * the {@link ExceptionHandlingConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity exceptionHandling(Customizer> exceptionHandlingCustomizer) throws Exception { + exceptionHandlingCustomizer.customize(getOrApply(new ExceptionHandlingConfigurer<>())); + return HttpSecurity.this; + } + /** * Sets up management of the {@link SecurityContext} on the * {@link SecurityContextHolder} between {@link HttpServletRequest}'s. This is diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java index aad85a825be..b2f92d42478 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.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. @@ -86,6 +86,47 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + @WithMockUser(roles = "ANYTHING") + public void getWhenAccessDeniedOverriddenInLambdaThenCustomizesResponseByRequest() + throws Exception { + this.spring.register(RequestMatcherBasedAccessDeniedHandlerInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/hello")) + .andExpect(status().isIAmATeapot()); + + this.mvc.perform(get("/goodbye")) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class RequestMatcherBasedAccessDeniedHandlerInLambdaConfig extends WebSecurityConfigurerAdapter { + AccessDeniedHandler teapotDeniedHandler = + (request, response, exception) -> + response.setStatus(HttpStatus.I_AM_A_TEAPOT.value()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling(exceptionHandling -> + exceptionHandling + .defaultAccessDeniedHandlerFor( + this.teapotDeniedHandler, + new AntPathRequestMatcher("/hello/**") + ) + .defaultAccessDeniedHandlerFor( + new AccessDeniedHandlerImpl(), + AnyRequestMatcher.INSTANCE + ) + ); + // @formatter:on + } + } + @Test @WithMockUser(roles = "ANYTHING") public void getWhenAccessDeniedOverriddenByOnlyOneHandlerThenAllRequestsUseThatHandler() diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java index eb4763bd551..b6ddacb2371 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java @@ -83,6 +83,31 @@ private static Authentication user() { return new UsernamePasswordAuthenticationToken("user", null, AuthorityUtils.NO_AUTHORITIES); } + @Test + public void requestWhenCustomAccessDeniedPageInLambdaThenForwardedToCustomPage() throws Exception { + this.spring.register(AccessDeniedPageInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/") + .with(authentication(user()))) + .andExpect(status().isForbidden()) + .andExpect(forwardedUrl("/AccessDeniedPageConfig")); + } + + @EnableWebSecurity + static class AccessDeniedPageInLambdaConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling(exceptionHandling -> + exceptionHandling.accessDeniedPage("/AccessDeniedPageConfig") + ); + // @formatter:on + } + } + @Test public void requestWhenCustomAccessDeniedHandlerThenBehaviorMatchesNamespace() throws Exception { this.spring.register(AccessDeniedHandlerRefConfig.class).autowire(); @@ -109,6 +134,39 @@ AccessDeniedHandler accessDeniedHandler() { } } + @Test + public void requestWhenCustomAccessDeniedHandlerInLambdaThenBehaviorMatchesNamespace() throws Exception { + this.spring.register(AccessDeniedHandlerRefInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/") + .with(authentication(user()))); + + verify(AccessDeniedHandlerRefInLambdaConfig.accessDeniedHandler) + .handle(any(HttpServletRequest.class), any(HttpServletResponse.class), any(AccessDeniedException.class)); + } + + @EnableWebSecurity + static class AccessDeniedHandlerRefInLambdaConfig extends WebSecurityConfigurerAdapter { + static AccessDeniedHandler accessDeniedHandler = mock(AccessDeniedHandler.class); + + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().denyAll() + .and() + .exceptionHandling(exceptionHandling -> + exceptionHandling.accessDeniedHandler(accessDeniedHandler()) + ); + // @formatter:on + } + + @Bean + AccessDeniedHandler accessDeniedHandler() { + return accessDeniedHandler; + } + } + private T verifyBean(Class beanClass) { return verify(this.spring.getContext().getBean(beanClass)); } From 6986cf3ef3bd8c513539564b98b2960bce2875d4 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 19 Jun 2019 16:32:44 -0400 Subject: [PATCH 03/23] Allow configuration of csrf through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 29 +++++++ ...onfigurerIgnoringRequestMatchersTests.java | 66 ++++++++++++++- .../web/configurers/CsrfConfigurerTests.java | 81 +++++++++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) 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 341515f5c32..bad4a7fbb48 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 @@ -771,6 +771,35 @@ public CsrfConfigurer csrf() throws Exception { return getOrApply(new CsrfConfigurer<>(context)); } + /** + * Adds CSRF support. This is activated by default when using + * {@link WebSecurityConfigurerAdapter}'s default constructor. You can disable it + * using: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 *     protected void configure(HttpSecurity http) throws Exception {
+	 *         http
+	 *             .csrf(csrf -> csrf.disable());
+	 *     }
+	 * }
+	 * 
+ * + * @param csrfCustomizer the {@link Customizer} to provide more options for + * the {@link CsrfConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity csrf(Customizer> csrfCustomizer) throws Exception { + ApplicationContext context = getContext(); + csrfCustomizer.customize(getOrApply(new CsrfConfigurer<>(context))); + return HttpSecurity.this; + } + /** * Provides logout support. This is automatically applied when using * {@link WebSecurityConfigurerAdapter}. The default is that accessing the URL diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java index 39af9745cb3..56d99f0610b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerIgnoringRequestMatchersTests.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. @@ -75,6 +75,36 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void requestWhenIgnoringRequestMatchersInLambdaThenAugmentedByConfiguredRequestMatcher() + throws Exception { + this.spring.register(IgnoringRequestInLambdaMatchers.class, BasicController.class).autowire(); + + this.mvc.perform(get("/path")) + .andExpect(status().isForbidden()); + + this.mvc.perform(post("/path")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class IgnoringRequestInLambdaMatchers extends WebSecurityConfigurerAdapter { + RequestMatcher requestMatcher = + request -> HttpMethod.POST.name().equals(request.getMethod()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf(csrf -> + csrf + .requireCsrfProtectionMatcher(new AntPathRequestMatcher("/path")) + .ignoringRequestMatchers(this.requestMatcher) + ); + // @formatter:on + } + } + @Test public void requestWhenIgnoringRequestMatcherThenUnionsWithConfiguredIgnoringAntMatchers() throws Exception { @@ -107,6 +137,40 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void requestWhenIgnoringRequestMatcherInLambdaThenUnionsWithConfiguredIgnoringAntMatchers() + throws Exception { + + this.spring.register(IgnoringPathsAndMatchersInLambdaConfig.class, BasicController.class).autowire(); + + this.mvc.perform(put("/csrf")) + .andExpect(status().isForbidden()); + + this.mvc.perform(post("/csrf")) + .andExpect(status().isOk()); + + this.mvc.perform(put("/no-csrf")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class IgnoringPathsAndMatchersInLambdaConfig extends WebSecurityConfigurerAdapter { + RequestMatcher requestMatcher = + request -> HttpMethod.POST.name().equals(request.getMethod()); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf(csrf -> + csrf + .ignoringAntMatchers("/no-csrf") + .ignoringRequestMatchers(this.requestMatcher) + ); + // @formatter:on + } + } + @RestController public static class BasicController { @RequestMapping("/path") diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java index e66fd1a075d..be2aad08152 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java @@ -210,6 +210,26 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void postWhenCsrfDisabledInLambdaThenRespondsWithOk() throws Exception { + this.spring.register(DisableCsrfInLambdaConfig.class, BasicController.class).autowire(); + + this.mvc.perform(post("/")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class DisableCsrfInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf(AbstractHttpConfigurer::disable); + // @formatter:on + } + } + // SEC-2498 @Test public void loginWhenCsrfDisabledThenRedirectsToPreviousPostRequest() throws Exception { @@ -386,6 +406,40 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void requireCsrfProtectionMatcherInLambdaWhenRequestDoesNotMatchThenRespondsWithOk() throws Exception { + RequireCsrfProtectionMatcherInLambdaConfig.MATCHER = mock(RequestMatcher.class); + this.spring.register(RequireCsrfProtectionMatcherInLambdaConfig.class, BasicController.class).autowire(); + when(RequireCsrfProtectionMatcherInLambdaConfig.MATCHER.matches(any())) + .thenReturn(false); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()); + } + + @Test + public void requireCsrfProtectionMatcherInLambdaWhenRequestMatchesThenRespondsWithForbidden() throws Exception { + RequireCsrfProtectionMatcherInLambdaConfig.MATCHER = mock(RequestMatcher.class); + when(RequireCsrfProtectionMatcherInLambdaConfig.MATCHER.matches(any())).thenReturn(true); + this.spring.register(RequireCsrfProtectionMatcherInLambdaConfig.class, BasicController.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class RequireCsrfProtectionMatcherInLambdaConfig extends WebSecurityConfigurerAdapter { + static RequestMatcher MATCHER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf(csrf -> csrf.requireCsrfProtectionMatcher(MATCHER)); + // @formatter:on + } + } + @Test public void getWhenCustomCsrfTokenRepositoryThenRepositoryIsUsed() throws Exception { CsrfTokenRepositoryConfig.REPO = mock(CsrfTokenRepository.class); @@ -454,6 +508,33 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void getWhenCustomCsrfTokenRepositoryInLambdaThenRepositoryIsUsed() throws Exception { + CsrfTokenRepositoryInLambdaConfig.REPO = mock(CsrfTokenRepository.class); + when(CsrfTokenRepositoryInLambdaConfig.REPO.loadToken(any())) + .thenReturn(new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", "token")); + this.spring.register(CsrfTokenRepositoryInLambdaConfig.class, BasicController.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isOk()); + verify(CsrfTokenRepositoryInLambdaConfig.REPO).loadToken(any(HttpServletRequest.class)); + } + + @EnableWebSecurity + static class CsrfTokenRepositoryInLambdaConfig extends WebSecurityConfigurerAdapter { + static CsrfTokenRepository REPO; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin() + .and() + .csrf(csrf -> csrf.csrfTokenRepository(REPO)); + // @formatter:on + } + } + @Test public void getWhenCustomAccessDeniedHandlerThenHandlerIsUsed() throws Exception { AccessDeniedHandlerConfig.DENIED_HANDLER = mock(AccessDeniedHandler.class); From 758397f1020a107294f1ea08a7700a03e437fb74 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 25 Jun 2019 15:27:34 -0400 Subject: [PATCH 04/23] Allow configuration of headers through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 97 +++++ .../web/configurers/HeadersConfigurer.java | 188 +++++++++- .../configurers/HeadersConfigurerTests.java | 338 ++++++++++++++++++ .../ContentSecurityPolicyHeaderWriter.java | 10 + ...ontentSecurityPolicyHeaderWriterTests.java | 19 + 5 files changed, 651 insertions(+), 1 deletion(-) 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 bad4a7fbb48..75e818caf92 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 @@ -338,6 +338,103 @@ public HeadersConfigurer headers() throws Exception { return getOrApply(new HeadersConfigurer<>()); } + /** + * Adds the Security headers to the response. This is activated by default when using + * {@link WebSecurityConfigurerAdapter}'s default constructor. + * + *

Example Configurations

+ * + * Accepting the default provided by {@link WebSecurityConfigurerAdapter} or only invoking + * {@link #headers()} without invoking additional methods on it, is the equivalent of: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	@Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 *			.headers(headers ->
+	 *				headers
+	 *					.contentTypeOptions(withDefaults())
+	 *					.xssProtection(withDefaults())
+	 *					.cacheControl(withDefaults())
+	 *					.httpStrictTransportSecurity(withDefaults())
+	 *					.frameOptions(withDefaults()
+	 *			);
+	 *	}
+	 * }
+	 * 
+ * + * You can disable the headers using the following: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	@Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.headers(headers -> headers.disable());
+	 *	}
+	 * }
+	 * 
+ * + * You can enable only a few of the headers by first invoking + * {@link HeadersConfigurer#defaultsDisabled()} + * and then invoking the appropriate methods on the {@link #headers()} result. + * For example, the following will enable {@link HeadersConfigurer#cacheControl()} and + * {@link HeadersConfigurer#frameOptions()} only. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 *	@Override
+	 *	protected void configure(HttpSecurity http) throws Exception {
+	 *		http
+	 *			.headers(headers ->
+	 *				headers
+	 *			 		.defaultsDisabled()
+	 *			 		.cacheControl(withDefaults())
+	 *			 		.frameOptions(withDefaults())
+	 *			);
+	 * 	}
+	 * }
+	 * 
+ * + * You can also choose to keep the defaults but explicitly disable a subset of headers. + * For example, the following will enable all the default headers except + * {@link HeadersConfigurer#frameOptions()}. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 *  protected void configure(HttpSecurity http) throws Exception {
+	 *  	http
+	 *  		.headers(headers ->
+	 *  			headers
+	 *  				.frameOptions(frameOptions -> frameOptions.disable())
+	 *  		);
+	 * }
+	 * 
+ * + * @param headersCustomizer the {@link Customizer} to provide more options for + * the {@link HeadersConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity headers(Customizer> headersCustomizer) throws Exception { + headersCustomizer.customize(getOrApply(new HeadersConfigurer<>())); + return HttpSecurity.this; + } + /** * Adds a {@link CorsFilter} to be used. If a bean by the name of corsFilter is * provided, that {@link CorsFilter} is used. Else if corsConfigurationSource is diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index 1f38253d407..77c840d5a04 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.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. @@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -30,6 +31,8 @@ import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.*; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy; +import org.springframework.security.web.header.writers.XContentTypeOptionsHeaderWriter; +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter; import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -121,6 +124,26 @@ public ContentTypeOptionsConfig contentTypeOptions() { return contentTypeOptions.enable(); } + /** + * Configures the {@link XContentTypeOptionsHeaderWriter} which inserts the X-Content-Type-Options: + * + *
+	 * X-Content-Type-Options: nosniff
+	 * 
+ * + * @param contentTypeOptionsCustomizer the {@link Customizer} to provide more options for + * the {@link ContentTypeOptionsConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer contentTypeOptions(Customizer contentTypeOptionsCustomizer) + throws Exception { + contentTypeOptionsCustomizer.customize(contentTypeOptions.enable()); + return HeadersConfigurer.this; + } + public final class ContentTypeOptionsConfig { private XContentTypeOptionsHeaderWriter writer; @@ -174,6 +197,25 @@ public XXssConfig xssProtection() { return xssProtection.enable(); } + /** + * Note this is not comprehensive XSS protection! + * + *

+ * Allows customizing the {@link XXssProtectionHeaderWriter} which adds the X-XSS-Protection header + *

+ * + * @param xssCustomizer the {@link Customizer} to provide more options for + * the {@link XXssConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer xssProtection(Customizer xssCustomizer) throws Exception { + xssCustomizer.customize(xssProtection.enable()); + return HeadersConfigurer.this; + } + public final class XXssConfig { private XXssProtectionHeaderWriter writer; @@ -268,6 +310,26 @@ public CacheControlConfig cacheControl() { return cacheControl.enable(); } + /** + * Allows customizing the {@link CacheControlHeadersWriter}. Specifically it adds the + * following headers: + *
    + *
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • + *
  • Pragma: no-cache
  • + *
  • Expires: 0
  • + *
+ * + * @param cacheControlCustomizer the {@link Customizer} to provide more options for + * the {@link CacheControlConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer cacheControl(Customizer cacheControlCustomizer) throws Exception { + cacheControlCustomizer.customize(cacheControl.enable()); + return HeadersConfigurer.this; + } + + public final class CacheControlConfig { private CacheControlHeadersWriter writer; @@ -319,6 +381,21 @@ public HstsConfig httpStrictTransportSecurity() { return hsts.enable(); } + /** + * Allows customizing the {@link HstsHeaderWriter} which provides support for HTTP Strict Transport Security + * (HSTS). + * + * @param hstsCustomizer the {@link Customizer} to provide more options for + * the {@link HstsConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer httpStrictTransportSecurity(Customizer hstsCustomizer) throws Exception { + hstsCustomizer.customize(hsts.enable()); + return HeadersConfigurer.this; + } + public final class HstsConfig { private HstsHeaderWriter writer; @@ -440,6 +517,19 @@ public FrameOptionsConfig frameOptions() { return frameOptions.enable(); } + /** + * Allows customizing the {@link XFrameOptionsHeaderWriter}. + * + * @param frameOptionsCustomizer the {@link Customizer} to provide more options for + * the {@link FrameOptionsConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer frameOptions(Customizer frameOptionsCustomizer) throws Exception { + frameOptionsCustomizer.customize(frameOptions.enable()); + return HeadersConfigurer.this; + } + public final class FrameOptionsConfig { private XFrameOptionsHeaderWriter writer; @@ -516,6 +606,20 @@ public HpkpConfig httpPublicKeyPinning() { return hpkp.enable(); } + /** + * Allows customizing the {@link HpkpHeaderWriter} which provides support for HTTP Public Key Pinning (HPKP). + * + * @param hpkpCustomizer the {@link Customizer} to provide more options for + * the {@link HpkpConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer httpPublicKeyPinning(Customizer hpkpCustomizer) throws Exception { + hpkpCustomizer.customize(hpkp.enable()); + return HeadersConfigurer.this; + } + public final class HpkpConfig { private HpkpHeaderWriter writer; @@ -713,12 +817,57 @@ public ContentSecurityPolicyConfig contentSecurityPolicy(String policyDirectives return contentSecurityPolicy; } + /** + *

+ * Allows configuration for Content Security Policy (CSP) Level 2. + *

+ * + *

+ * Calling this method automatically enables (includes) the Content-Security-Policy header in the response + * using the supplied security policy directive(s). + *

+ * + *

+ * Configuration is provided to the {@link ContentSecurityPolicyHeaderWriter} which supports the writing + * of the two headers as detailed in the W3C Candidate Recommendation: + *

+ *
    + *
  • Content-Security-Policy
  • + *
  • Content-Security-Policy-Report-Only
  • + *
+ * + * @see ContentSecurityPolicyHeaderWriter + * @param contentSecurityCustomizer the {@link Customizer} to provide more options for + * the {@link ContentSecurityPolicyConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer contentSecurityPolicy(Customizer contentSecurityCustomizer) + throws Exception { + this.contentSecurityPolicy.writer = new ContentSecurityPolicyHeaderWriter(); + contentSecurityCustomizer.customize(this.contentSecurityPolicy); + + return HeadersConfigurer.this; + } + public final class ContentSecurityPolicyConfig { private ContentSecurityPolicyHeaderWriter writer; private ContentSecurityPolicyConfig() { } + /** + * Sets the security policy directive(s) to be used in the response header. + * + * @param policyDirectives the security policy directive(s) + * @return the {@link ContentSecurityPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policyDirectives is null or empty + */ + public ContentSecurityPolicyConfig policyDirectives(String policyDirectives) { + this.writer.setPolicyDirectives(policyDirectives); + return this; + } + /** * Enables (includes) the Content-Security-Policy-Report-Only header in the response. * @@ -860,6 +1009,31 @@ public ReferrerPolicyConfig referrerPolicy(ReferrerPolicy policy) { return this.referrerPolicy; } + /** + *

+ * Allows configuration for Referrer Policy. + *

+ * + *

+ * Configuration is provided to the {@link ReferrerPolicyHeaderWriter} which support the writing + * of the header as detailed in the W3C Technical Report: + *

+ *
    + *
  • Referrer-Policy
  • + *
+ * + * @see ReferrerPolicyHeaderWriter + * @param referrerPolicyCustomizer the {@link Customizer} to provide more options for + * the {@link ReferrerPolicyConfig} + * @return the {@link HeadersConfigurer} for additional customizations + * @throws Exception + */ + public HeadersConfigurer referrerPolicy(Customizer referrerPolicyCustomizer) throws Exception { + this.referrerPolicy.writer = new ReferrerPolicyHeaderWriter(); + referrerPolicyCustomizer.customize(this.referrerPolicy); + return HeadersConfigurer.this; + } + public final class ReferrerPolicyConfig { private ReferrerPolicyHeaderWriter writer; @@ -867,6 +1041,18 @@ public final class ReferrerPolicyConfig { private ReferrerPolicyConfig() { } + /** + * Sets the policy to be used in the response header. + * + * @param policy a referrer policy + * @return the {@link ReferrerPolicyConfig} for additional configuration + * @throws IllegalArgumentException if policy is null + */ + public ReferrerPolicyConfig policy(ReferrerPolicy policy) { + this.writer.setPolicy(policy); + return this; + } + public HeadersConfigurer and() { return HeadersConfigurer.this; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java index f8ed9adcf20..0ca822a6fdc 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.java @@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -87,6 +88,36 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenHeadersConfiguredInLambdaThenDefaultHeadersInResponse() throws Exception { + this.spring.register(HeadersInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_CONTENT_TYPE_OPTIONS, "nosniff")) + .andExpect(header().string(HttpHeaders.X_FRAME_OPTIONS, XFrameOptionsMode.DENY.name())) + .andExpect(header().string(HttpHeaders.STRICT_TRANSPORT_SECURITY, "max-age=31536000 ; includeSubDomains")) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")) + .andExpect(header().string(HttpHeaders.EXPIRES, "0")) + .andExpect(header().string(HttpHeaders.PRAGMA, "no-cache")) + .andExpect(header().string(HttpHeaders.X_XSS_PROTECTION, "1; mode=block")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactlyInAnyOrder( + HttpHeaders.X_CONTENT_TYPE_OPTIONS, HttpHeaders.X_FRAME_OPTIONS, HttpHeaders.STRICT_TRANSPORT_SECURITY, + HttpHeaders.CACHE_CONTROL, HttpHeaders.EXPIRES, HttpHeaders.PRAGMA, HttpHeaders.X_XSS_PROTECTION); + } + + @EnableWebSecurity + static class HeadersInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(withDefaults()); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndContentTypeConfiguredThenOnlyContentTypeHeaderInResponse() throws Exception { @@ -112,6 +143,33 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenOnlyContentTypeConfiguredInLambdaThenOnlyContentTypeHeaderInResponse() + throws Exception { + this.spring.register(ContentTypeOptionsInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/")) + .andExpect(header().string(HttpHeaders.X_CONTENT_TYPE_OPTIONS, "nosniff")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_CONTENT_TYPE_OPTIONS); + } + + @EnableWebSecurity + static class ContentTypeOptionsInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentTypeOptions(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndFrameOptionsConfiguredThenOnlyFrameOptionsHeaderInResponse() throws Exception { @@ -190,6 +248,36 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenOnlyCacheControlConfiguredInLambdaThenCacheControlAndExpiresAndPragmaHeadersInResponse() + throws Exception { + this.spring.register(CacheControlInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0, must-revalidate")) + .andExpect(header().string(HttpHeaders.EXPIRES, "0")) + .andExpect(header().string(HttpHeaders.PRAGMA, "no-cache")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactlyInAnyOrder(HttpHeaders.CACHE_CONTROL, + HttpHeaders.EXPIRES, HttpHeaders.PRAGMA); + } + + @EnableWebSecurity + static class CacheControlInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .cacheControl(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndXssProtectionConfiguredThenOnlyXssProtectionHeaderInResponse() throws Exception { @@ -215,6 +303,33 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenOnlyXssProtectionConfiguredInLambdaThenOnlyXssProtectionHeaderInResponse() + throws Exception { + this.spring.register(XssProtectionInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_XSS_PROTECTION, "1; mode=block")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.X_XSS_PROTECTION); + } + + @EnableWebSecurity + static class XssProtectionInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .xssProtection(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenFrameOptionsSameOriginConfiguredThenFrameOptionsHeaderHasValueSameOrigin() throws Exception { this.spring.register(HeadersCustomSameOriginConfig.class).autowire(); @@ -237,6 +352,31 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenFrameOptionsSameOriginConfiguredInLambdaThenFrameOptionsHeaderHasValueSameOrigin() + throws Exception { + this.spring.register(HeadersCustomSameOriginInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.X_FRAME_OPTIONS, XFrameOptionsMode.SAMEORIGIN.name())) + .andReturn(); + } + + @EnableWebSecurity + static class HeadersCustomSameOriginInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .frameOptions(frameOptionsConfig -> frameOptionsConfig.sameOrigin()) + ); + // @formatter:on + } + } + @Test public void getWhenHeaderDefaultsDisabledAndPublicHpkpWithNoPinThenNoHeadersInResponse() throws Exception { this.spring.register(HpkpConfigNoPins.class).autowire(); @@ -465,6 +605,38 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenHpkpWithReportUriInLambdaThenPublicKeyPinsReportOnlyHeaderWithReportUriInResponse() + throws Exception { + this.spring.register(HpkpWithReportUriInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.PUBLIC_KEY_PINS_REPORT_ONLY, + "max-age=5184000 ; pin-sha256=\"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=\" ; report-uri=\"https://example.net/pkp-report\"")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.PUBLIC_KEY_PINS_REPORT_ONLY); + } + + @EnableWebSecurity + static class HpkpWithReportUriInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .httpPublicKeyPinning(hpkp -> + hpkp + .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=") + .reportUri("https://example.net/pkp-report") + ) + ); + // @formatter:on + } + } + @Test public void getWhenContentSecurityPolicyConfiguredThenContentSecurityPolicyHeaderInResponse() throws Exception { this.spring.register(ContentSecurityPolicyDefaultConfig.class).autowire(); @@ -515,6 +687,38 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenContentSecurityPolicyWithReportOnlyInLambdaThenContentSecurityPolicyReportOnlyHeaderInResponse() + throws Exception { + this.spring.register(ContentSecurityPolicyReportOnlyInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY, + "default-src 'self'; script-src trustedscripts.example.com")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CONTENT_SECURITY_POLICY_REPORT_ONLY); + } + + @EnableWebSecurity + static class ContentSecurityPolicyReportOnlyInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentSecurityPolicy(csp -> + csp + .policyDirectives("default-src 'self'; script-src trustedscripts.example.com") + .reportOnly() + ) + ); + // @formatter:on + } + } + @Test public void configureWhenContentSecurityPolicyEmptyThenException() { assertThatThrownBy(() -> this.spring.register(ContentSecurityPolicyInvalidConfig.class).autowire()) @@ -536,6 +740,58 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void configureWhenContentSecurityPolicyEmptyInLambdaThenException() { + assertThatThrownBy(() -> this.spring.register(ContentSecurityPolicyInvalidInLambdaConfig.class).autowire()) + .isInstanceOf(BeanCreationException.class) + .hasRootCauseInstanceOf(IllegalArgumentException.class); + } + + @EnableWebSecurity + static class ContentSecurityPolicyInvalidInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentSecurityPolicy(csp -> + csp.policyDirectives("") + ) + ); + // @formatter:on + } + } + + @Test + public void configureWhenContentSecurityPolicyNoPolicyDirectivesInLambdaThenDefaultHeaderValue() throws Exception { + this.spring.register(ContentSecurityPolicyNoDirectivesInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.CONTENT_SECURITY_POLICY, + "default-src 'self'")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.CONTENT_SECURITY_POLICY); + } + + @EnableWebSecurity + static class ContentSecurityPolicyNoDirectivesInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .contentSecurityPolicy(withDefaults()) + ); + // @formatter:on + } + } + @Test public void getWhenReferrerPolicyConfiguredThenReferrerPolicyHeaderInResponse() throws Exception { this.spring.register(ReferrerPolicyDefaultConfig.class).autowire(); @@ -560,6 +816,32 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenReferrerPolicyInLambdaThenReferrerPolicyHeaderInResponse() throws Exception { + this.spring.register(ReferrerPolicyDefaultInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string("Referrer-Policy", ReferrerPolicy.NO_REFERRER.getPolicy())) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Referrer-Policy"); + } + + @EnableWebSecurity + static class ReferrerPolicyDefaultInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .referrerPolicy() + ); + // @formatter:on + } + } + @Test public void getWhenReferrerPolicyConfiguredWithCustomValueThenReferrerPolicyHeaderWithCustomValueInResponse() throws Exception { @@ -585,6 +867,34 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenReferrerPolicyConfiguredWithCustomValueInLambdaThenCustomValueInResponse() throws Exception { + this.spring.register(ReferrerPolicyCustomInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string("Referrer-Policy", ReferrerPolicy.SAME_ORIGIN.getPolicy())) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly("Referrer-Policy"); + } + + @EnableWebSecurity + static class ReferrerPolicyCustomInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .referrerPolicy(referrerPolicy -> + referrerPolicy.policy(ReferrerPolicy.SAME_ORIGIN) + ) + ); + // @formatter:on + } + } + @Test public void getWhenFeaturePolicyConfiguredThenFeaturePolicyHeaderInResponse() throws Exception { this.spring.register(FeaturePolicyConfig.class).autowire(); @@ -656,4 +966,32 @@ protected void configure(HttpSecurity http) throws Exception { // @formatter:on } } + + @Test + public void getWhenHstsConfiguredWithPreloadInLambdaThenStrictTransportSecurityHeaderWithPreloadInResponse() + throws Exception { + this.spring.register(HstsWithPreloadInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/").secure(true)) + .andExpect(header().string(HttpHeaders.STRICT_TRANSPORT_SECURITY, + "max-age=31536000 ; includeSubDomains ; preload")) + .andReturn(); + assertThat(mvcResult.getResponse().getHeaderNames()).containsExactly(HttpHeaders.STRICT_TRANSPORT_SECURITY); + } + + @EnableWebSecurity + static class HstsWithPreloadInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .headers(headers -> + headers + .defaultsDisabled() + .httpStrictTransportSecurity(hstsConfig -> hstsConfig.preload(true)) + ); + // @formatter:on + } + } } diff --git a/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java b/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java index 12ad08e9785..4ae86198f36 100644 --- a/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java +++ b/web/src/main/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriter.java @@ -81,10 +81,20 @@ public final class ContentSecurityPolicyHeaderWriter implements HeaderWriter { private static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_HEADER = "Content-Security-Policy-Report-Only"; + private static final String DEFAULT_SRC_SELF_POLICY = "default-src 'self'"; + private String policyDirectives; private boolean reportOnly; + /** + * Creates a new instance. Default value: default-src 'self' + */ + public ContentSecurityPolicyHeaderWriter() { + setPolicyDirectives(DEFAULT_SRC_SELF_POLICY); + this.reportOnly = false; + } + /** * Creates a new instance * diff --git a/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java b/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java index 5ff062f4342..2f3c7914e8d 100644 --- a/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/writers/ContentSecurityPolicyHeaderWriterTests.java @@ -43,6 +43,15 @@ public void setup() { writer = new ContentSecurityPolicyHeaderWriter(DEFAULT_POLICY_DIRECTIVES); } + @Test + public void writeHeadersWhenNoPolicyDirectivesThenUsesDefault() { + ContentSecurityPolicyHeaderWriter noPolicyWriter = new ContentSecurityPolicyHeaderWriter(); + noPolicyWriter.writeHeaders(request, response); + + assertThat(response.getHeaderNames()).hasSize(1); + assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + @Test public void writeHeadersContentSecurityPolicyDefault() { writer.writeHeaders(request, response); @@ -64,6 +73,16 @@ public void writeHeadersContentSecurityPolicyCustom() { assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(policyDirectives); } + @Test + public void writeHeadersWhenNoPolicyDirectivesReportOnlyThenUsesDefault() { + ContentSecurityPolicyHeaderWriter noPolicyWriter = new ContentSecurityPolicyHeaderWriter(); + writer.setReportOnly(true); + noPolicyWriter.writeHeaders(request, response); + + assertThat(response.getHeaderNames()).hasSize(1); + assertThat(response.getHeader("Content-Security-Policy")).isEqualTo(DEFAULT_POLICY_DIRECTIVES); + } + @Test public void writeHeadersContentSecurityPolicyReportOnlyDefault() { writer.setReportOnly(true); From a9a1f8ee53bc43eea6a878fa345ebd44ef2a051d Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Thu, 27 Jun 2019 14:29:11 -0400 Subject: [PATCH 05/23] Allow configuration of form login through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 68 +++++++- .../web/configurers/CsrfConfigurerTests.java | 4 +- .../configurers/FormLoginConfigurerTests.java | 147 ++++++++++++++++++ 3 files changed, 215 insertions(+), 4 deletions(-) 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 75e818caf92..4c0fae24e0f 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 @@ -962,8 +962,7 @@ public LogoutConfigurer logout() throws Exception { * .authorizeRequests() * .antMatchers("/**").hasRole("USER") * .and() - * .formLogin() - * .and() + * .formLogin(withDefaults()) * // sample logout customization * .logout(logout -> * logout.deleteCookies("remove") @@ -1112,6 +1111,71 @@ public FormLoginConfigurer formLogin() throws Exception { return getOrApply(new FormLoginConfigurer<>()); } + /** + * Specifies to support form based authentication. If + * {@link FormLoginConfigurer#loginPage(String)} is not specified a default login page + * will be generated. + * + *

Example Configurations

+ * + * The most basic configuration defaults to automatically generating a login page at + * the URL "/login", redirecting to "/login?error" for authentication failure. The + * details of the login page can be found on + * {@link FormLoginConfigurer#loginPage(String)} + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.formLogin(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below demonstrates customizing the defaults. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.formLogin(formLogin ->
+	 * 				formLogin
+	 * 					.usernameParameter("username")
+	 * 					.passwordParameter("password")
+	 * 					.loginPage("/authentication/login")
+	 * 					.failureUrl("/authentication/login?failed")
+	 * 					.loginProcessingUrl("/authentication/login/process")
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @see FormLoginConfigurer#loginPage(String) + * + * @param formLoginCustomizer the {@link Customizer} to provide more options for + * the {@link FormLoginConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity formLogin(Customizer> formLoginCustomizer) throws Exception { + formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. *
diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java index be2aad08152..22e88597ce1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurerTests.java @@ -55,6 +55,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; @@ -528,8 +529,7 @@ static class CsrfTokenRepositoryInLambdaConfig extends WebSecurityConfigurerAdap protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .formLogin() - .and() + .formLogin(withDefaults()) .csrf(csrf -> csrf.csrfTokenRepository(REPO)); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index 42118060943..ae5a2379e44 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -42,6 +42,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.logout; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; @@ -195,6 +196,81 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void loginWhenFormLoginDefaultsInLambdaThenHasDefaultUsernameAndPasswordParameterNames() throws Exception { + this.spring.register(FormLoginInLambdaConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("username", "user").password("password", "password")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")); + } + + @Test + public void loginWhenFormLoginDefaultsInLambdaThenHasDefaultFailureUrl() throws Exception { + this.spring.register(FormLoginInLambdaConfig.class).autowire(); + + this.mockMvc.perform(formLogin().user("invalid")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + } + + @Test + public void loginWhenFormLoginDefaultsInLambdaThenHasDefaultSuccessUrl() throws Exception { + this.spring.register(FormLoginInLambdaConfig.class).autowire(); + + this.mockMvc.perform(formLogin()) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")); + } + + @Test + public void getLoginPageWhenFormLoginDefaultsInLambdaThenNotSecured() throws Exception { + this.spring.register(FormLoginInLambdaConfig.class).autowire(); + + this.mockMvc.perform(get("/login")) + .andExpect(status().isOk()); + } + + @Test + public void loginWhenFormLoginDefaultsInLambdaThenSecured() throws Exception { + this.spring.register(FormLoginInLambdaConfig.class).autowire(); + + this.mockMvc.perform(post("/login")) + .andExpect(status().isForbidden()); + } + + @Test + public void requestProtectedWhenFormLoginDefaultsInLambdaThenRedirectsToLogin() throws Exception { + this.spring.register(FormLoginInLambdaConfig.class).autowire(); + + this.mockMvc.perform(get("/private")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login")); + } + + @EnableWebSecurity + static class FormLoginInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .formLogin(withDefaults()); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + @Test public void getLoginPageWhenFormLoginPermitAllThenPermittedAndNoRedirect() throws Exception { this.spring.register(FormLoginConfigPermitAll.class).autowire(); @@ -297,6 +373,33 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getLoginPageWhenCustomLoginPageInLambdaThenPermittedAndNoRedirect() throws Exception { + this.spring.register(FormLoginDefaultsInLambdaConfig.class).autowire(); + + this.mockMvc.perform(get("/authenticate")) + .andExpect(redirectedUrl(null)); + } + + @EnableWebSecurity + static class FormLoginDefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .formLogin(formLogin -> + formLogin + .loginPage("/authenticate") + .permitAll() + ) + .logout(LogoutConfigurer::permitAll); + // @formatter:on + } + } + @Test public void loginWhenCustomLoginProcessingUrlThenRedirectsToHome() throws Exception { this.spring.register(FormLoginLoginProcessingUrlConfig.class).autowire(); @@ -340,6 +443,50 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void loginWhenCustomLoginProcessingUrlInLambdaThenRedirectsToHome() throws Exception { + this.spring.register(FormLoginLoginProcessingUrlInLambdaConfig.class).autowire(); + + this.mockMvc.perform(formLogin("/loginCheck")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/")); + } + + @EnableWebSecurity + static class FormLoginLoginProcessingUrlInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin(formLogin -> + formLogin + .loginProcessingUrl("/loginCheck") + .loginPage("/login") + .defaultSuccessUrl("/", true) + .permitAll() + ) + .logout(logout -> + logout + .logoutSuccessUrl("/login") + .logoutUrl("/logout") + .deleteCookies("JSESSIONID") + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + @Test public void requestWhenCustomPortMapperThenPortMapperUsed() throws Exception { FormLoginUsesPortMapperConfig.PORT_MAPPER = mock(PortMapper.class); From 6fd515813c398ec5285dafd468f265be91696fe0 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Thu, 27 Jun 2019 15:07:41 -0400 Subject: [PATCH 06/23] Allow configuration of cors through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 30 ++++ .../web/configurers/CorsConfigurerTests.java | 150 ++++++++++++++++++ 2 files changed, 180 insertions(+) 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 4c0fae24e0f..53af9b6e1ee 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 @@ -448,6 +448,36 @@ public CorsConfigurer cors() throws Exception { return getOrApply(new CorsConfigurer<>()); } + /** + * Adds a {@link CorsFilter} to be used. If a bean by the name of corsFilter is + * provided, that {@link CorsFilter} is used. Else if corsConfigurationSource is + * defined, then that {@link CorsConfiguration} is used. Otherwise, if Spring MVC is + * on the classpath a {@link HandlerMappingIntrospector} is used. + * You can enable CORS using: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class CorsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 *     protected void configure(HttpSecurity http) throws Exception {
+	 *         http
+	 *             .cors(withDefaults());
+	 *     }
+	 * }
+	 * 
+ * + * @param corsCustomizer the {@link Customizer} to provide more options for + * the {@link CorsConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity cors(Customizer> corsCustomizer) throws Exception { + corsCustomizer.customize(getOrApply(new CorsConfigurer<>())); + return HttpSecurity.this; + } + /** * Allows configuring of Session Management. * diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java index 8122817caf7..b6bb33e58fe 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java @@ -42,6 +42,7 @@ import java.util.Collections; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -131,6 +132,55 @@ String hello() { } } + @Test + public void getWhenDefaultsInLambdaAndCrossOriginAnnotationThenRespondsWithCorsHeaders() throws Exception { + this.spring.register(MvcCorsInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/") + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().exists("X-Content-Type-Options")); + } + + @Test + public void optionsWhenDefaultsInLambdaAndCrossOriginAnnotationThenRespondsWithCorsHeaders() throws Exception { + this.spring.register(MvcCorsInLambdaConfig.class).autowire(); + + this.mvc.perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().exists("X-Content-Type-Options")); + } + + @EnableWebMvc + @EnableWebSecurity + static class MvcCorsInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .cors(withDefaults()); + // @formatter:on + } + + @RestController + @CrossOrigin(methods = { + RequestMethod.GET, RequestMethod.POST + }) + static class CorsController { + @RequestMapping("/") + String hello() { + return "Hello"; + } + } + } + @Test public void getWhenCorsConfigurationSourceBeanThenRespondsWithCorsHeaders() throws Exception { this.spring.register(ConfigSourceConfig.class).autowire(); @@ -180,6 +230,57 @@ CorsConfigurationSource corsConfigurationSource() { } } + @Test + public void getWhenMvcCorsInLambdaConfigAndCorsConfigurationSourceBeanThenRespondsWithCorsHeaders() + throws Exception { + this.spring.register(ConfigSourceInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/") + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().exists("X-Content-Type-Options")); + } + + @Test + public void optionsWhenMvcCorsInLambdaConfigAndCorsConfigurationSourceBeanThenRespondsWithCorsHeaders() + throws Exception { + this.spring.register(ConfigSourceInLambdaConfig.class).autowire(); + + this.mvc.perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().exists("X-Content-Type-Options")); + } + + @EnableWebSecurity + static class ConfigSourceInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .cors(withDefaults()); + // @formatter:on + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList( + RequestMethod.GET.name(), + RequestMethod.POST.name())); + source.registerCorsConfiguration("/**", corsConfiguration); + return source; + } + } + @Test public void getWhenCorsFilterBeanThenRespondsWithCorsHeaders() throws Exception { this.spring.register(CorsFilterConfig.class).autowire(); @@ -228,4 +329,53 @@ CorsFilter corsFilter() { return new CorsFilter(source); } } + + @Test + public void getWhenConfigSourceInLambdaConfigAndCorsFilterBeanThenRespondsWithCorsHeaders() throws Exception { + this.spring.register(CorsFilterInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/") + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().exists("X-Content-Type-Options")); + } + + @Test + public void optionsWhenConfigSourceInLambdaConfigAndCorsFilterBeanThenRespondsWithCorsHeaders() throws Exception { + this.spring.register(CorsFilterInLambdaConfig.class).autowire(); + + this.mvc.perform(options("/") + .header(org.springframework.http.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.POST.name()) + .header(HttpHeaders.ORIGIN, "https://example.com")) + .andExpect(status().isOk()) + .andExpect(header().exists("Access-Control-Allow-Origin")) + .andExpect(header().exists("X-Content-Type-Options")); + } + + @EnableWebSecurity + static class CorsFilterInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .cors(withDefaults()); + // @formatter:on + } + + @Bean + CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); + corsConfiguration.setAllowedMethods(Arrays.asList( + RequestMethod.GET.name(), + RequestMethod.POST.name())); + source.registerCorsConfiguration("/**", corsConfiguration); + return new CorsFilter(source); + } + } } From 6fbea88e1e5374b3729a5f2966b3995f6a81a212 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Thu, 27 Jun 2019 15:46:52 -0400 Subject: [PATCH 07/23] Allow configuration of session management through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 60 ++++++++++++++++ .../SessionManagementConfigurerTests.java | 68 +++++++++++++++++++ 2 files changed, 128 insertions(+) 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 53af9b6e1ee..2688793d08a 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 @@ -530,6 +530,66 @@ public SessionManagementConfigurer sessionManagement() throws Exce return getOrApply(new SessionManagementConfigurer<>()); } + /** + * Allows configuring of Session Management. + * + *

Example Configuration

+ * + * The following configuration demonstrates how to enforce that only a single instance + * of a user is authenticated at a time. If a user authenticates with the username + * "user" without logging out and an attempt to authenticate with "user" is made the + * first session will be forcibly terminated and sent to the "/login?expired" URL. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class SessionManagementSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.anyRequest().hasRole("USER")
+	 * 				.and()
+	 * 			.formLogin(formLogin ->
+	 * 				formLogin
+	 * 					.permitAll()
+	 * 			)
+	 * 			.sessionManagement(sessionManagement ->
+	 * 				sessionManagement
+	 * 					.maximumSessions(1)
+	 * 					.expiredUrl("/login?expired")
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * When using {@link SessionManagementConfigurer#maximumSessions(int)}, do not forget + * to configure {@link HttpSessionEventPublisher} for the application to ensure that + * expired sessions are cleaned up. + * + * In a web.xml this can be configured using the following: + * + *
+	 * <listener>
+	 *      <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
+	 * </listener>
+	 * 
+ * + * Alternatively, + * {@link AbstractSecurityWebApplicationInitializer#enableHttpSessionEventPublisher()} + * could return true. + * + * @param sessionManagementCustomizer the {@link Customizer} to provide more options for + * the {@link SessionManagementConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity sessionManagement(Customizer> sessionManagementCustomizer) throws Exception { + sessionManagementCustomizer.customize(getOrApply(new SessionManagementConfigurer<>())); + return HttpSecurity.this; + } + /** * Allows configuring a {@link PortMapper} that is available from * {@link HttpSecurity#getSharedObject(Class)}. Other provided diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index 4278811084c..b7374228117 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -54,6 +54,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -262,6 +263,73 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void loginWhenUserLoggedInAndMaxSessionsOneInLambdaThenLoginPrevented() throws Exception { + this.spring.register(ConcurrencyControlInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password")); + + this.mvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?error")); + } + + @EnableWebSecurity + static class ConcurrencyControlInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + public void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .sessionManagement(sessionManagement -> + sessionManagement + .maximumSessions(1) + .maxSessionsPreventsLogin(true) + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + + @Test + public void requestWhenSessionCreationPolicyStateLessInLambdaThenNoSessionCreated() throws Exception { + this.spring.register(SessionCreationPolicyStateLessInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(get("/")) + .andReturn(); + HttpSession session = mvcResult.getRequest().getSession(false); + + assertThat(session).isNull(); + } + + @EnableWebSecurity + static class SessionCreationPolicyStateLessInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .sessionManagement(sessionManagement -> + sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ); + // @formatter:on + } + } + @Test public void configureWhenRegisteringObjectPostProcessorThenInvokedOnSessionManagementFilter() { ObjectPostProcessorConfig.objectPostProcessor = spy(ReflectingObjectPostProcessor.class); From 86f0f84740ce447e0912316a02cac362b6169ae3 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Fri, 28 Jun 2019 13:43:41 -0400 Subject: [PATCH 08/23] Allow configuration of port mapper through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 46 ++++++++++++++++ .../PortMapperConfigurerTests.java | 55 +++++++++++++++++++ 2 files changed, 101 insertions(+) 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 2688793d08a..7c1acd72003 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 @@ -633,6 +633,52 @@ public PortMapperConfigurer portMapper() throws Exception { return getOrApply(new PortMapperConfigurer<>()); } + /** + * Allows configuring a {@link PortMapper} that is available from + * {@link HttpSecurity#getSharedObject(Class)}. Other provided + * {@link SecurityConfigurer} objects use this configured {@link PortMapper} as a + * default {@link PortMapper} when redirecting from HTTP to HTTPS or from HTTPS to + * HTTP (for example when used in combination with {@link #requiresChannel()}. By + * default Spring Security uses a {@link PortMapperImpl} which maps the HTTP port 8080 + * to the HTTPS port 8443 and the HTTP port of 80 to the HTTPS port of 443. + * + *

Example Configuration

+ * + * The following configuration will ensure that redirects within Spring Security from + * HTTP of a port of 9090 will redirect to HTTPS port of 9443 and the HTTP port of 80 + * to the HTTPS port of 443. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class PortMapperSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.requiresChannel()
+	 * 				.anyRequest().requiresSecure()
+	 * 				.and()
+	 * 			.portMapper(portMapper ->
+	 * 				portMapper
+	 * 					.http(9090).mapsTo(9443)
+	 * 					.http(80).mapsTo(443)
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @see #requiresChannel() + * @param portMapperCustomizer the {@link Customizer} to provide more options for + * the {@link PortMapperConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity portMapper(Customizer> portMapperCustomizer) throws Exception { + portMapperCustomizer.customize(getOrApply(new PortMapperConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures container based pre authentication. In this case, authentication * is managed by the Servlet Container. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java index bc451f4c95f..d9ed42ff22a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java @@ -22,8 +22,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.web.PortMapperImpl; import org.springframework.test.web.servlet.MockMvc; +import java.util.Collections; + import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -61,4 +64,56 @@ protected void configure(HttpSecurity http) throws Exception { .portMapper(); } } + + @Test + public void requestWhenPortMapperHttpMapsToInLambdaThenRedirectsToHttpsPort() throws Exception { + this.spring.register(HttpMapsToInLambdaConfig.class).autowire(); + + this.mockMvc.perform(get("http://localhost:543")) + .andExpect(redirectedUrl("https://localhost:123")); + } + + @EnableWebSecurity + static class HttpMapsToInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .requiresChannel() + .anyRequest().requiresSecure() + .and() + .portMapper(portMapper -> + portMapper + .http(543).mapsTo(123) + ); + // @formatter:on + } + } + + @Test + public void requestWhenCustomPortMapperInLambdaThenRedirectsToHttpsPort() throws Exception { + this.spring.register(CustomPortMapperInLambdaConfig.class).autowire(); + + this.mockMvc.perform(get("http://localhost:543")) + .andExpect(redirectedUrl("https://localhost:123")); + } + + @EnableWebSecurity + static class CustomPortMapperInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + PortMapperImpl customPortMapper = new PortMapperImpl(); + customPortMapper.setPortMappings(Collections.singletonMap("543", "123")); + // @formatter:off + http + .requiresChannel() + .anyRequest().requiresSecure() + .and() + .portMapper(portMapper -> + portMapper + .portMapper(customPortMapper) + ); + // @formatter:on + } + } } From bfc9538da1bc6223be08d9bf02f4a6dbe3558d90 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Fri, 28 Jun 2019 15:56:43 -0400 Subject: [PATCH 09/23] Allow configuration of jee through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 80 +++++++++++++ .../web/configurers/JeeConfigurerTests.java | 111 ++++++++++++++++++ 2 files changed, 191 insertions(+) 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 7c1acd72003..7d8654687d4 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 @@ -751,6 +751,86 @@ public JeeConfigurer jee() throws Exception { return getOrApply(new JeeConfigurer<>()); } + /** + * Configures container based pre authentication. In this case, authentication + * is managed by the Servlet Container. + * + *

Example Configuration

+ * + * The following configuration will use the principal found on the + * {@link HttpServletRequest} and if the user is in the role "ROLE_USER" or + * "ROLE_ADMIN" will add that to the resulting {@link Authentication}. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class JeeSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.jee(jee ->
+	 * 				jee
+	 * 					.mappableRoles("USER", "ADMIN")
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * Developers wishing to use pre authentication with the container will need to ensure + * their web.xml configures the security constraints. For example, the web.xml (there + * is no equivalent Java based configuration supported by the Servlet specification) + * might look like: + * + *
+	 * <login-config>
+	 *     <auth-method>FORM</auth-method>
+	 *     <form-login-config>
+	 *         <form-login-page>/login</form-login-page>
+	 *         <form-error-page>/login?error</form-error-page>
+	 *     </form-login-config>
+	 * </login-config>
+	 *
+	 * <security-role>
+	 *     <role-name>ROLE_USER</role-name>
+	 * </security-role>
+	 * <security-constraint>
+	 *     <web-resource-collection>
+	 *     <web-resource-name>Public</web-resource-name>
+	 *         <description>Matches unconstrained pages</description>
+	 *         <url-pattern>/login</url-pattern>
+	 *         <url-pattern>/logout</url-pattern>
+	 *         <url-pattern>/resources/*</url-pattern>
+	 *     </web-resource-collection>
+	 * </security-constraint>
+	 * <security-constraint>
+	 *     <web-resource-collection>
+	 *         <web-resource-name>Secured Areas</web-resource-name>
+	 *         <url-pattern>/*</url-pattern>
+	 *     </web-resource-collection>
+	 *     <auth-constraint>
+	 *         <role-name>ROLE_USER</role-name>
+	 *     </auth-constraint>
+	 * </security-constraint>
+	 * 
+ * + * Last you will need to configure your container to contain the user with the correct + * roles. This configuration is specific to the Servlet Container, so consult your + * Servlet Container's documentation. + * + * @param jeeCustomizer the {@link Customizer} to provide more options for + * the {@link JeeConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity jee(Customizer> jeeCustomizer) throws Exception { + jeeCustomizer.customize(getOrApply(new JeeConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures X509 based pre authentication. * diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java index fac4baf354b..c55ad59f477 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java @@ -25,6 +25,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource; import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter; import org.springframework.test.web.servlet.MockMvc; @@ -125,4 +128,112 @@ protected void configure(HttpSecurity http) throws Exception { // @formatter:on } } + + @Test + public void requestWhenJeeMappableRolesInLambdaThenAuthenticatedWithMappableRoles() throws Exception { + this.spring.register(JeeMappableRolesConfig.class).autowire(); + Principal user = mock(Principal.class); + when(user.getName()).thenReturn("user"); + + this.mvc.perform(get("/") + .principal(user) + .with(request -> { + request.addUserRole("ROLE_ADMIN"); + request.addUserRole("ROLE_USER"); + return request; + })) + .andExpect(authenticated().withRoles("USER")); + } + + @EnableWebSecurity + public static class JeeMappableRolesConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .jee(jee -> + jee + .mappableRoles("USER") + ); + // @formatter:on + } + } + + @Test + public void requestWhenJeeMappableAuthoritiesInLambdaThenAuthenticatedWithMappableAuthorities() throws Exception { + this.spring.register(JeeMappableAuthoritiesConfig.class).autowire(); + Principal user = mock(Principal.class); + when(user.getName()).thenReturn("user"); + + this.mvc.perform(get("/") + .principal(user) + .with(request -> { + request.addUserRole("ROLE_ADMIN"); + request.addUserRole("ROLE_USER"); + return request; + })) + .andExpect(authenticated().withAuthorities(AuthorityUtils.createAuthorityList("ROLE_USER"))); + } + + @EnableWebSecurity + public static class JeeMappableAuthoritiesConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .jee(jee -> + jee + .mappableAuthorities("ROLE_USER") + ); + // @formatter:on + } + } + + @Test + public void requestWhenCustomAuthenticatedUserDetailsServiceInLambdaThenCustomAuthenticatedUserDetailsServiceUsed() + throws Exception { + this.spring.register(JeeCustomAuthenticatedUserDetailsServiceConfig.class).autowire(); + Principal user = mock(Principal.class); + User userDetails = new User("user", "N/A", true, true, true, true, + AuthorityUtils.createAuthorityList("ROLE_USER")); + when(user.getName()).thenReturn("user"); + when(JeeCustomAuthenticatedUserDetailsServiceConfig.authenticationUserDetailsService.loadUserDetails(any())) + .thenReturn(userDetails); + + this.mvc.perform(get("/") + .principal(user) + .with(request -> { + request.addUserRole("ROLE_ADMIN"); + request.addUserRole("ROLE_USER"); + return request; + })) + .andExpect(authenticated().withRoles("USER")); + } + + @EnableWebSecurity + public static class JeeCustomAuthenticatedUserDetailsServiceConfig extends WebSecurityConfigurerAdapter { + static AuthenticationUserDetailsService authenticationUserDetailsService = + mock(AuthenticationUserDetailsService.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .jee(jee -> + jee + .authenticatedUserDetailsService(authenticationUserDetailsService) + ); + // @formatter:on + } + } } From ae9eb6f56b23962d01df9be4d4e3d11d74f27caa Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 2 Jul 2019 14:27:02 -0400 Subject: [PATCH 10/23] Allow configuration of x509 through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 36 +++++++++++ .../web/configurers/X509ConfigurerTests.java | 64 +++++++++++++++++++ 2 files changed, 100 insertions(+) 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 7d8654687d4..3bd002b5e83 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 @@ -861,6 +861,42 @@ public X509Configurer x509() throws Exception { return getOrApply(new X509Configurer<>()); } + /** + * Configures X509 based pre authentication. + * + *

Example Configuration

+ * + * The following configuration will attempt to extract the username from the X509 + * certificate. Remember that the Servlet Container will need to be configured to + * request client certificates in order for this to work. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class X509SecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**")
+	 * 				.hasRole("USER")
+	 * 				.and()
+	 * 			.x509(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * @param x509Customizer the {@link Customizer} to provide more options for + * the {@link X509Configurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity x509(Customizer> x509Customizer) throws Exception { + x509Customizer.customize(getOrApply(new X509Configurer<>())); + return HttpSecurity.this; + } + /** * Allows configuring of Remember Me authentication. * diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java index 49ca4e431b5..f7c24c00135 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/X509ConfigurerTests.java @@ -38,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.x509; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -122,6 +123,69 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void x509WhenConfiguredInLambdaThenUsesDefaults() throws Exception { + this.spring.register(DefaultsInLambdaConfig.class).autowire(); + X509Certificate certificate = loadCert("rod.cer"); + + this.mvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")); + } + + @EnableWebSecurity + static class DefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509(withDefaults()); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser("rod").password("password").roles("USER", "ADMIN"); + // @formatter:on + } + } + + @Test + public void x509WhenSubjectPrincipalRegexInLambdaThenUsesRegexToExtractPrincipal() throws Exception { + this.spring.register(SubjectPrincipalRegexInLambdaConfig.class).autowire(); + X509Certificate certificate = loadCert("rodatexampledotcom.cer"); + + this.mvc.perform(get("/") + .with(x509(certificate))) + .andExpect(authenticated().withUsername("rod")); + } + + @EnableWebSecurity + static class SubjectPrincipalRegexInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .x509(x509 -> + x509 + .subjectPrincipalRegex("CN=(.*?)@example.com(?:,|$)") + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser("rod").password("password").roles("USER", "ADMIN"); + // @formatter:on + } + } + private T loadCert(String location) { try (InputStream is = new ClassPathResource(location).getInputStream()) { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); From fcb119b94e87050bd1634a28b3730553a5e23cea Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 2 Jul 2019 14:56:08 -0400 Subject: [PATCH 11/23] Allow configuration of remember me through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 37 +++++++++ .../RememberMeConfigurerTests.java | 78 +++++++++++++++++++ 2 files changed, 115 insertions(+) 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 3bd002b5e83..b8d607d254f 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 @@ -934,6 +934,43 @@ public RememberMeConfigurer rememberMe() throws Exception { return getOrApply(new RememberMeConfigurer<>()); } + /** + * Allows configuring of Remember Me authentication. + * + *

Example Configuration

+ * + * The following configuration demonstrates how to allow token based remember me + * authentication. Upon authenticating if the HTTP parameter named "remember-me" + * exists, then the user will be remembered even after their + * {@link javax.servlet.http.HttpSession} expires. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RememberMeSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.formLogin(withDefaults())
+	 * 			.rememberMe(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * @param rememberMeCustomizer the {@link Customizer} to provide more options for + * the {@link RememberMeConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity rememberMe(Customizer> rememberMeCustomizer) throws Exception { + rememberMeCustomizer.customize(getOrApply(new RememberMeConfigurer<>())); + return HttpSecurity.this; + } + /** * Allows restricting access based upon the {@link HttpServletRequest} using * diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index 4c217ee2d94..84c9ec8816d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -51,6 +51,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated; @@ -299,6 +300,44 @@ public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception } } + + @Test + public void loginWhenRememberMeConfiguredInLambdaThenRespondsWithRememberMeCookie() throws Exception { + this.spring.register(RememberMeInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password") + .param("remember-me", "true")) + .andExpect(cookie().exists("remember-me")); + } + + @EnableWebSecurity + static class RememberMeInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .formLogin(withDefaults()) + .rememberMe(withDefaults()); + // @formatter:on + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + @Test public void loginWhenRememberMeTrueAndCookieDomainThenRememberMeCookieHasDomain() throws Exception { this.spring.register(RememberMeCookieDomainConfig.class).autowire(); @@ -337,6 +376,45 @@ public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception } } + @Test + public void loginWhenRememberMeTrueAndCookieDomainInLambdaThenRememberMeCookieHasDomain() throws Exception { + this.spring.register(RememberMeCookieDomainInLambdaConfig.class).autowire(); + + this.mvc.perform(post("/login") + .with(csrf()) + .param("username", "user") + .param("password", "password") + .param("remember-me", "true")) + .andExpect(cookie().exists("remember-me")) + .andExpect(cookie().domain("remember-me", "spring.io")); + } + + @EnableWebSecurity + static class RememberMeCookieDomainInLambdaConfig extends WebSecurityConfigurerAdapter { + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().hasRole("USER") + .and() + .formLogin(withDefaults()) + .rememberMe(rememberMe -> + rememberMe + .rememberMeCookieDomain("spring.io") + ); + // @formatter:on + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + @Test public void configureWhenRememberMeCookieNameAndRememberMeServicesThenException() { assertThatThrownBy(() -> this.spring.register(RememberMeCookieNameAndRememberMeServicesConfig.class).autowire()) From 1445d1b012feab36cc930efc1dde2886a194afb4 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 3 Jul 2019 09:46:26 -0400 Subject: [PATCH 12/23] Allow configuration of request cache through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 40 +++++++++ .../RequestCacheConfigurerTests.java | 88 ++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) 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 b8d607d254f..7b1884b86be 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 @@ -1056,6 +1056,46 @@ public RequestCacheConfigurer requestCache() throws Exception { return getOrApply(new RequestCacheConfigurer<>()); } + /** + * Allows configuring the Request Cache. For example, a protected page (/protected) + * may be requested prior to authentication. The application will redirect the user to + * a login page. After authentication, Spring Security will redirect the user to the + * originally requested protected page (/protected). This is automatically applied + * when using {@link WebSecurityConfigurerAdapter}. + * + *

Example Custom Configuration

+ * + * The following example demonstrates how to disable request caching. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestCacheDisabledSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests()
+	 * 				.antMatchers("/**").hasRole("USER")
+	 * 				.and()
+	 * 			.requestCache(requestCache ->
+	 * 				requestCache.disable()
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param requestCacheCustomizer the {@link Customizer} to provide more options for + * the {@link RequestCacheConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity requestCache(Customizer> requestCacheCustomizer) + throws Exception { + requestCacheCustomizer.customize(getOrApply(new RequestCacheConfigurer<>())); + return HttpSecurity.this; + } + /** * Allows configuring exception handling. This is automatically applied when using * {@link WebSecurityConfigurerAdapter}. diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index 683a4951c2c..c21aca11628 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.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. @@ -33,6 +33,7 @@ import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.RequestCacheAwareFilter; import org.springframework.test.web.servlet.MockMvc; @@ -42,6 +43,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -271,6 +273,90 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void getWhenRequestCacheIsDisabledInLambdaThenExceptionTranslationFilterDoesNotStoreRequest() throws Exception { + this.spring.register(RequestCacheDisabledInLambdaConfig.class, DefaultSecurityConfig.class).autowire(); + + MockHttpSession session = (MockHttpSession) + this.mvc.perform(get("/bob")) + .andReturn().getRequest().getSession(); + + this.mvc.perform(formLogin(session)) + .andExpect(redirectedUrl("/")); + } + + @EnableWebSecurity + static class RequestCacheDisabledInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin(withDefaults()) + .requestCache(RequestCacheConfigurer::disable); + // @formatter:on + } + } + + @Test + public void getWhenRequestCacheInLambdaThenRedirectedToCachedPage() throws Exception { + this.spring.register(RequestCacheInLambdaConfig.class, DefaultSecurityConfig.class).autowire(); + + MockHttpSession session = (MockHttpSession) + this.mvc.perform(get("/bob")) + .andReturn().getRequest().getSession(); + + this.mvc.perform(formLogin(session)) + .andExpect(redirectedUrl("http://localhost/bob")); + } + + @EnableWebSecurity + static class RequestCacheInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin(withDefaults()) + .requestCache(withDefaults()); + // @formatter:on + } + } + + @Test + public void getWhenCustomRequestCacheInLambdaThenCustomRequestCacheUsed() throws Exception { + this.spring.register(CustomRequestCacheInLambdaConfig.class, DefaultSecurityConfig.class).autowire(); + + MockHttpSession session = (MockHttpSession) + this.mvc.perform(get("/bob")) + .andReturn().getRequest().getSession(); + + this.mvc.perform(formLogin(session)) + .andExpect(redirectedUrl("/")); + } + + @EnableWebSecurity + static class CustomRequestCacheInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .formLogin(withDefaults()) + .requestCache(requestCache -> + requestCache + .requestCache(new NullRequestCache()) + ); + // @formatter:on + } + } + @EnableWebSecurity static class DefaultSecurityConfig { From 81d3cf1e7b814c2abe304fd5f8461768d8a8f044 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 3 Jul 2019 12:20:32 -0400 Subject: [PATCH 13/23] Allow configuration of authorize requests through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 156 +++++++++--- .../configurers/AuthorizeRequestsTests.java | 227 +++++++++++++++++- .../web/configurers/CorsConfigurerTests.java | 21 +- ...ingConfigurerAccessDeniedHandlerTests.java | 7 +- .../configurers/FormLoginConfigurerTests.java | 21 +- .../configurers/HttpBasicConfigurerTests.java | 7 +- .../web/configurers/JeeConfigurerTests.java | 21 +- .../configurers/NamespaceHttpBasicTests.java | 21 +- ...aceHttpServerAccessDeniedHandlerTests.java | 14 +- .../RememberMeConfigurerTests.java | 14 +- .../RequestCacheConfigurerTests.java | 21 +- 11 files changed, 435 insertions(+), 95 deletions(-) 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 7b1884b86be..e7397d9c04e 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 @@ -548,9 +548,10 @@ public SessionManagementConfigurer sessionManagement() throws Exce * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .anyRequest().hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .anyRequest().hasRole("USER") + * ) * .formLogin(formLogin -> * formLogin * .permitAll() @@ -769,9 +770,10 @@ public JeeConfigurer jee() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .jee(jee -> * jee * .mappableRoles("USER", "ADMIN") @@ -878,10 +880,10 @@ public X509Configurer x509() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**") - * .hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .x509(withDefaults()); * } * } @@ -952,9 +954,10 @@ public RememberMeConfigurer rememberMe() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .formLogin(withDefaults()) * .rememberMe(withDefaults()); * } @@ -1042,6 +1045,91 @@ public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrl .getRegistry(); } + /** + * Allows restricting access based upon the {@link HttpServletRequest} using + * {@link RequestMatcher} implementations (i.e. via URL patterns). + * + *

Example Configurations

+ * + * The most basic example is to configure all URLs to require the role "ROLE_USER". + * The configuration below requires authentication to every URL and will grant access + * to both the user "admin" and "user". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * We can also configure multiple URLs. The configuration below requires + * authentication to every URL and will grant access to URLs starting with /admin/ to + * only the "admin" user. All other URLs either user can access. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/admin/**").hasRole("ADMIN")
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * Note that the matchers are considered in order. Therefore, the following is invalid + * because the first matcher matches every request and will never get to the second + * mapping: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		 http
+	 * 		 	.authorizeRequests(authorizeRequests ->
+	 * 		 		authorizeRequests
+	 * 			 		.antMatchers("/**").hasRole("USER")
+	 * 			 		.antMatchers("/admin/**").hasRole("ADMIN")
+	 * 		 	);
+	 * 	}
+	 * }
+	 * 
+ * + * @see #requestMatcher(RequestMatcher) + * + * @param authorizeRequestsCustomizer the {@link Customizer} to provide more options for + * the {@link ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity authorizeRequests(Customizer.ExpressionInterceptUrlRegistry> authorizeRequestsCustomizer) + throws Exception { + ApplicationContext context = getContext(); + authorizeRequestsCustomizer.customize(getOrApply(new ExpressionUrlAuthorizationConfigurer<>(context)) + .getRegistry()); + return HttpSecurity.this; + } + /** * Allows configuring the Request Cache. For example, a protected page (/protected) * may be requested prior to authentication. The application will redirect the user to @@ -1075,9 +1163,10 @@ public RequestCacheConfigurer requestCache() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .requestCache(requestCache -> * requestCache.disable() * ); @@ -1124,9 +1213,10 @@ public ExceptionHandlingConfigurer exceptionHandling() throws Exce * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * // sample exception handling customization * .exceptionHandling(exceptionHandling -> * exceptionHandling @@ -1288,9 +1378,10 @@ public LogoutConfigurer logout() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .formLogin(withDefaults()) * // sample logout customization * .logout(logout -> @@ -1460,9 +1551,10 @@ public FormLoginConfigurer formLogin() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .formLogin(withDefaults()); * } * } @@ -1478,9 +1570,10 @@ public FormLoginConfigurer formLogin() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .formLogin(formLogin -> * formLogin * .usernameParameter("username") @@ -1717,9 +1810,10 @@ public HttpBasicConfigurer httpBasic() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .authorizeRequests() - * .antMatchers("/**").hasRole("USER") - * .and() + * .authorizeRequests(authorizeRequests -> + * authorizeRequests + * .antMatchers("/**").hasRole("USER") + * ) * .httpBasic(withDefaults()); * } * } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index 526f3b86579..b9fb86db21b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 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. @@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.spy; +import static org.springframework.security.config.Customizer.withDefaults; /** * @author Rob Winch @@ -113,6 +114,39 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void postWhenPostDenyAllInLambdaThenRespondsWithForbidden() throws Exception { + loadConfig(AntMatchersNoPatternsInLambdaConfig.class); + this.request.setMethod("POST"); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + } + + @EnableWebSecurity + @Configuration + static class AntMatchersNoPatternsInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .antMatchers(HttpMethod.POST).denyAll() + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + } + // SEC-2256 @Test public void antMatchersPathVariables() throws Exception { @@ -314,6 +348,66 @@ public String path() { } } + @Test + public void requestWhenMvcMatcherDenyAllThenRespondsWithUnauthorized() throws Exception { + loadConfig(MvcMatcherInLambdaConfig.class); + + this.request.setRequestURI("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setRequestURI("/path.html"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/path/"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MvcMatcherInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(withDefaults()) + .authorizeRequests(authorizeRequests -> + authorizeRequests + .mvcMatchers("/path").denyAll() + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + @Test public void mvcMatcherServletPath() throws Exception { loadConfig(MvcMatcherServletPathConfig.class); @@ -391,6 +485,85 @@ public String path() { } } + @Test + public void requestWhenMvcMatcherServletPathDenyAllThenMatchesOnServletPath() throws Exception { + loadConfig(MvcMatcherServletPathInLambdaConfig.class); + + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path.html"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path/"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/foo"); + this.request.setRequestURI("/foo/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + + setup(); + + this.request.setServletPath("/"); + this.request.setRequestURI("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MvcMatcherServletPathInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(withDefaults()) + .authorizeRequests(authorizeRequests -> + authorizeRequests + .mvcMatchers("/path").servletPath("/spring").denyAll() + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + @Test public void mvcMatcherPathVariables() throws Exception { loadConfig(MvcMatcherPathVariablesConfig.class); @@ -441,6 +614,58 @@ public String path() { } } + @Test + public void requestWhenMvcMatcherPathVariablesThenMatchesOnPathVariables() throws Exception { + loadConfig(MvcMatcherPathVariablesInLambdaConfig.class); + + this.request.setRequestURI("/user/user"); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + + this.setup(); + this.request.setRequestURI("/user/deny"); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MvcMatcherPathVariablesInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(withDefaults()) + .authorizeRequests(authorizeRequests -> + authorizeRequests + .mvcMatchers("/user/{userName}").access("#userName == 'user'") + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + @EnableWebSecurity @Configuration @EnableWebMvc diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java index b6bb33e58fe..b880b3ef5b1 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.java @@ -162,9 +162,10 @@ static class MvcCorsInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .cors(withDefaults()); // @formatter:on } @@ -261,9 +262,10 @@ static class ConfigSourceInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .cors(withDefaults()); // @formatter:on } @@ -359,9 +361,10 @@ static class CorsFilterInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .cors(withDefaults()); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java index b2f92d42478..ef1084cdbda 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerAccessDeniedHandlerTests.java @@ -109,9 +109,10 @@ static class RequestMatcherBasedAccessDeniedHandlerInLambdaConfig extends WebSec protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().denyAll() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().denyAll() + ) .exceptionHandling(exceptionHandling -> exceptionHandling .defaultAccessDeniedHandlerFor( diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java index ae5a2379e44..f7b9c1635e0 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.java @@ -254,9 +254,10 @@ static class FormLoginInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .formLogin(withDefaults()); // @formatter:on } @@ -387,9 +388,10 @@ static class FormLoginDefaultsInLambdaConfig extends WebSecurityConfigurerAdapte protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .formLogin(formLogin -> formLogin .loginPage("/authenticate") @@ -458,9 +460,10 @@ static class FormLoginLoginProcessingUrlInLambdaConfig extends WebSecurityConfig protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .formLogin(formLogin -> formLogin .loginProcessingUrl("/loginCheck") 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 a6445aa35b8..43d956b3e51 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 @@ -107,9 +107,10 @@ static class DefaultsLambdaEntryPointConfig extends WebSecurityConfigurerAdapter protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .httpBasic(withDefaults()); // @formatter:on } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java index c55ad59f477..57de13f0877 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/JeeConfigurerTests.java @@ -151,9 +151,10 @@ public static class JeeMappableRolesConfig extends WebSecurityConfigurerAdapter protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .jee(jee -> jee .mappableRoles("USER") @@ -185,9 +186,10 @@ public static class JeeMappableAuthoritiesConfig extends WebSecurityConfigurerAd protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .jee(jee -> jee .mappableAuthorities("ROLE_USER") @@ -226,9 +228,10 @@ public static class JeeCustomAuthenticatedUserDetailsServiceConfig extends WebSe protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .jee(jee -> jee .authenticatedUserDetailsService(authenticationUserDetailsService) 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 83d05080573..9a66e356fd7 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 @@ -125,9 +125,10 @@ static class HttpBasicLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .httpBasic(withDefaults()); // @formatter:on } @@ -174,9 +175,10 @@ static class CustomHttpBasicLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .httpBasic(httpBasicConfig -> httpBasicConfig.realmName("Custom Realm")); // @formatter:on } @@ -310,9 +312,10 @@ static class EntryPointRefHttpBasicLambdaConfig extends WebSecurityConfigurerAda protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .httpBasic(httpBasicConfig -> httpBasicConfig.authenticationEntryPoint(this.authenticationEntryPoint)); // @formatter:on diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java index b6ddacb2371..0aea788b3d8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceHttpServerAccessDeniedHandlerTests.java @@ -98,9 +98,10 @@ static class AccessDeniedPageInLambdaConfig extends WebSecurityConfigurerAdapter protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().denyAll() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().denyAll() + ) .exceptionHandling(exceptionHandling -> exceptionHandling.accessDeniedPage("/AccessDeniedPageConfig") ); @@ -152,9 +153,10 @@ static class AccessDeniedHandlerRefInLambdaConfig extends WebSecurityConfigurerA protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().denyAll() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().denyAll() + ) .exceptionHandling(exceptionHandling -> exceptionHandling.accessDeniedHandler(accessDeniedHandler()) ); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java index 84c9ec8816d..1e3ee6018db 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RememberMeConfigurerTests.java @@ -320,9 +320,10 @@ static class RememberMeInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .formLogin(withDefaults()) .rememberMe(withDefaults()); // @formatter:on @@ -394,9 +395,10 @@ static class RememberMeCookieDomainInLambdaConfig extends WebSecurityConfigurerA protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().hasRole("USER") - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().hasRole("USER") + ) .formLogin(withDefaults()) .rememberMe(rememberMe -> rememberMe diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index c21aca11628..7b161231657 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -291,9 +291,10 @@ static class RequestCacheDisabledInLambdaConfig extends WebSecurityConfigurerAda protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .formLogin(withDefaults()) .requestCache(RequestCacheConfigurer::disable); // @formatter:on @@ -318,9 +319,10 @@ static class RequestCacheInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .formLogin(withDefaults()) .requestCache(withDefaults()); // @formatter:on @@ -345,9 +347,10 @@ static class CustomRequestCacheInLambdaConfig extends WebSecurityConfigurerAdapt protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .authorizeRequests() - .anyRequest().authenticated() - .and() + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) .formLogin(withDefaults()) .requestCache(requestCache -> requestCache From 04e0dcfe61dca06187b12d7b4f99b29ad06bb8b8 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Wed, 3 Jul 2019 14:41:50 -0400 Subject: [PATCH 14/23] Allow configuration of security context through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 34 ++++++ .../SecurityContextConfigurerTests.java | 101 ++++++++++++++++++ 2 files changed, 135 insertions(+) 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 e7397d9c04e..21bc5d22a39 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 @@ -62,6 +62,7 @@ import org.springframework.security.web.PortMapperImpl; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -1248,6 +1249,39 @@ public SecurityContextConfigurer securityContext() throws Exceptio return getOrApply(new SecurityContextConfigurer<>()); } + /** + * Sets up management of the {@link SecurityContext} on the + * {@link SecurityContextHolder} between {@link HttpServletRequest}'s. This is + * automatically applied when using {@link WebSecurityConfigurerAdapter}. + * + * The following customization specifies the shared {@link SecurityContextRepository} + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class SecurityContextSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.securityContext(securityContext ->
+	 * 				securityContext
+	 * 					.securityContextRepository(SCR)
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param securityContextCustomizer the {@link Customizer} to provide more options for + * the {@link SecurityContextConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity securityContext(Customizer> securityContextCustomizer) throws Exception { + securityContextCustomizer.customize(getOrApply(new SecurityContextConfigurer<>())); + return HttpSecurity.this; + } + /** * Integrates the {@link HttpServletRequest} methods with the values found on the * {@link SecurityContext}. This is automatically applied when using diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java index b9b09393447..5d0419d9595 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SecurityContextConfigurerTests.java @@ -28,14 +28,22 @@ import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.NullSecurityContextRepository; import org.springframework.security.web.context.SecurityContextPersistenceFilter; import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import javax.servlet.http.HttpSession; + +import static org.assertj.core.api.Assertions.assertThat; 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.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; /** @@ -151,4 +159,97 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { // @formatter:on } } + + @Test + public void requestWhenSecurityContextWithDefaultsInLambdaThenSessionIsCreated() throws Exception { + this.spring.register(SecurityContextWithDefaultsInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(formLogin()).andReturn(); + HttpSession session = mvcResult.getRequest().getSession(false); + assertThat(session).isNotNull(); + } + + @EnableWebSecurity + static class SecurityContextWithDefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .securityContext(withDefaults()); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + + @Test + public void requestWhenSecurityContextDisabledInLambdaThenContextNotSavedInSession() throws Exception { + this.spring.register(SecurityContextDisabledInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(formLogin()).andReturn(); + HttpSession session = mvcResult.getRequest().getSession(false); + assertThat(session).isNull(); + } + + @EnableWebSecurity + static class SecurityContextDisabledInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .securityContext(AbstractHttpConfigurer::disable); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + + @Test + public void requestWhenNullSecurityContextRepositoryInLambdaThenContextNotSavedInSession() throws Exception { + this.spring.register(NullSecurityContextRepositoryInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mvc.perform(formLogin()).andReturn(); + HttpSession session = mvcResult.getRequest().getSession(false); + assertThat(session).isNull(); + } + + @EnableWebSecurity + static class NullSecurityContextRepositoryInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .formLogin(withDefaults()) + .securityContext(securityContext -> + securityContext + .securityContextRepository(new NullSecurityContextRepository()) + ); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } } From a5943fbafb7e1e90c2bae90b99d26c37faf04d57 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Thu, 4 Jul 2019 13:54:24 -0400 Subject: [PATCH 15/23] Allow configuration of servlet api through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 30 ++++++++++++ .../ServletApiConfigurerTests.java | 48 +++++++++++++++++++ 2 files changed, 78 insertions(+) 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 21bc5d22a39..cf31cfeadc2 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 @@ -1294,6 +1294,36 @@ public ServletApiConfigurer servletApi() throws Exception { return getOrApply(new ServletApiConfigurer<>()); } + /** + * Integrates the {@link HttpServletRequest} methods with the values found on the + * {@link SecurityContext}. This is automatically applied when using + * {@link WebSecurityConfigurerAdapter}. You can disable it using: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class ServletApiSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.servletApi(servletApi ->
+	 * 				servletApi.disable()
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param servletApiCustomizer the {@link Customizer} to provide more options for + * the {@link ServletApiConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity servletApi(Customizer> servletApiCustomizer) throws Exception { + servletApiCustomizer.customize(getOrApply(new ServletApiConfigurer<>())); + return HttpSecurity.this; + } + /** * Adds CSRF support. This is activated by default when using * {@link WebSecurityConfigurerAdapter}'s default constructor. You can disable it diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java index dcf96fa2d85..b390d004933 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurerTests.java @@ -47,6 +47,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -230,6 +231,53 @@ protected void configure(HttpSecurity http) throws Exception { } } + @Test + public void requestWhenServletApiWithDefaultsInLambdaThenUsesDefaultRolePrefix() throws Exception { + this.spring.register(ServletApiWithDefaultsInLambdaConfig.class, AdminController.class).autowire(); + + this.mvc.perform(get("/admin") + .with(user("user").authorities(AuthorityUtils.createAuthorityList("ROLE_ADMIN")))) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class ServletApiWithDefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .servletApi(withDefaults()); + // @formatter:on + } + } + + @Test + public void requestWhenRolePrefixInLambdaThenUsesCustomRolePrefix() throws Exception { + this.spring.register(RolePrefixInLambdaConfig.class, AdminController.class).autowire(); + + this.mvc.perform(get("/admin") + .with(user("user").authorities(AuthorityUtils.createAuthorityList("PERMISSION_ADMIN")))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/admin") + .with(user("user").authorities(AuthorityUtils.createAuthorityList("ROLE_ADMIN")))) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class RolePrefixInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .servletApi(servletApi -> + servletApi + .rolePrefix("PERMISSION_") + ); + // @formatter:on + } + } + @RestController static class AdminController { @GetMapping("/admin") From ae8e12f049876c7d8f8713adb0f12bf742a103bd Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Thu, 4 Jul 2019 15:08:00 -0400 Subject: [PATCH 16/23] Allow configuration of anonymous through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 70 ++++++++++++ .../configurers/AnonymousConfigurerTests.java | 106 ++++++++++++++++-- 2 files changed, 168 insertions(+), 8 deletions(-) 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 cf31cfeadc2..db0f24bede6 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 @@ -1531,6 +1531,76 @@ public AnonymousConfigurer anonymous() throws Exception { return getOrApply(new AnonymousConfigurer<>()); } + /** + * Allows configuring how an anonymous user is represented. This is automatically + * applied when used in conjunction with {@link WebSecurityConfigurerAdapter}. By + * default anonymous users will be represented with an + * {@link org.springframework.security.authentication.AnonymousAuthenticationToken} + * and contain the role "ROLE_ANONYMOUS". + * + *

Example Configuration

+ * + * The following configuration demonstrates how to specify that anonymous users should + * contain the role "ROLE_ANON" instead. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AnononymousSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults())
+	 * 			// sample anonymous customization
+	 * 			.anonymous(anonymous ->
+	 * 				anonymous
+	 * 					.authorities("ROLE_ANON")
+	 * 			)
+	 * 	}
+	 * }
+	 * 
+ * + * The following demonstrates how to represent anonymous users as null. Note that this + * can cause {@link NullPointerException} in code that assumes anonymous + * authentication is enabled. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class AnonymousSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults())
+	 * 			// sample anonymous customization
+	 * 			.anonymous(anonymous ->
+	 * 				anonymous.disabled()
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param anonymousCustomizer the {@link Customizer} to provide more options for + * the {@link AnonymousConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity anonymous(Customizer> anonymousCustomizer) throws Exception { + anonymousCustomizer.customize(getOrApply(new AnonymousConfigurer<>())); + return HttpSecurity.this; + } + + /** * Specifies to support form based authentication. If * {@link FormLoginConfigurer#loginPage(String)} is not specified a default login page diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurerTests.java index b10dcdbe59f..11d80e02d82 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AnonymousConfigurerTests.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. @@ -18,18 +18,22 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestRule; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * @author Rob Winch @@ -44,7 +48,7 @@ public class AnonymousConfigurerTests { @Test public void requestWhenAnonymousTwiceInvokedThenDoesNotOverride() throws Exception { - this.spring.register(InvokeTwiceDoesNotOverride.class).autowire(); + this.spring.register(InvokeTwiceDoesNotOverride.class, PrincipalController.class).autowire(); this.mockMvc.perform(get("/")) .andExpect(content().string("principal")); @@ -63,13 +67,99 @@ protected void configure(HttpSecurity http) throws Exception { .and() .anonymous(); } + } + + @Test + public void requestWhenAnonymousPrincipalInLambdaThenPrincipalUsed() throws Exception { + this.spring.register(AnonymousPrincipalInLambdaConfig.class, PrincipalController.class).autowire(); + + this.mockMvc.perform(get("/")) + .andExpect(content().string("principal")); + } + + @EnableWebSecurity + @EnableWebMvc + static class AnonymousPrincipalInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .anonymous(anonymous -> + anonymous + .principal("principal") + ); + // @formatter:on + } + } + + @Test + public void requestWhenAnonymousDisabledInLambdaThenRespondsWithForbidden() throws Exception { + this.spring.register(AnonymousDisabledInLambdaConfig.class, PrincipalController.class).autowire(); + + this.mockMvc.perform(get("/")) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class AnonymousDisabledInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() + ) + .anonymous(AbstractHttpConfigurer::disable); + // @formatter:on + } + + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + + @Test + public void requestWhenAnonymousWithDefaultsInLambdaThenRespondsWithOk() throws Exception { + this.spring.register(AnonymousWithDefaultsInLambdaConfig.class, PrincipalController.class).autowire(); + + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()); + } + + @EnableWebSecurity + static class AnonymousWithDefaultsInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() + ) + .anonymous(withDefaults()); + // @formatter:on + } + + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } - @RestController - static class PrincipalController { - @GetMapping("/") - String principal(@AuthenticationPrincipal String principal) { - return principal; - } + @RestController + static class PrincipalController { + @GetMapping("/") + String principal(@AuthenticationPrincipal String principal) { + return principal; } } } From 1ad9f15e193b9967ea6a83033514b64be59a7967 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Thu, 4 Jul 2019 16:55:38 -0400 Subject: [PATCH 17/23] Allow configuration of requires channel through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 53 +++++++++++++++++-- .../ChannelSecurityConfigurerTests.java | 23 ++++++++ .../PortMapperConfigurerTests.java | 14 ++--- 3 files changed, 81 insertions(+), 9 deletions(-) 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 db0f24bede6..c609fa60561 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 @@ -658,9 +658,10 @@ public PortMapperConfigurer portMapper() throws Exception { * @Override * protected void configure(HttpSecurity http) throws Exception { * http - * .requiresChannel() - * .anyRequest().requiresSecure() - * .and() + * .requiresChannel(requiresChannel -> + * requiresChannel + * .anyRequest().requiresSecure() + * ) * .portMapper(portMapper -> * portMapper * .http(9090).mapsTo(9443) @@ -1894,6 +1895,52 @@ public ChannelSecurityConfigurer.ChannelRequestMatcherRegistry req .getRegistry(); } + /** + * Configures channel security. In order for this configuration to be useful at least + * one mapping to a required channel must be provided. + * + *

Example Configuration

+ * + * The example below demonstrates how to require HTTPs for every request. Only + * requiring HTTPS for some requests is supported, but not recommended since an + * application that allows for HTTP introduces many security vulnerabilities. For one + * such example, read about Firesheep. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class ChannelSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.formLogin(withDefaults())
+	 * 			.requiresChannel(requiresChannel ->
+	 * 				requiresChannel
+	 * 					.anyRequest().requiresSecure()
+	 * 			);
+	 * 	}
+	 * }
+	 * 
+ * + * @param requiresChannelCustomizer the {@link Customizer} to provide more options for + * the {@link ChannelSecurityConfigurer.ChannelRequestMatcherRegistry} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity requiresChannel(Customizer.ChannelRequestMatcherRegistry> requiresChannelCustomizer) + throws Exception { + ApplicationContext context = getContext(); + requiresChannelCustomizer.customize(getOrApply(new ChannelSecurityConfigurer<>(context)) + .getRegistry()); + return HttpSecurity.this; + } + /** * Configures HTTP Basic authentication. * diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java index c755c5f3a37..c33682e7169 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurerTests.java @@ -135,4 +135,27 @@ protected void configure(HttpSecurity http) throws Exception { // @formatter:on } } + + @Test + public void requestWhenRequiresChannelConfiguredInLambdaThenRedirectsToHttps() throws Exception { + this.spring.register(RequiresChannelInLambdaConfig.class).autowire(); + + mvc.perform(get("/")) + .andExpect(redirectedUrl("https://localhost/")); + } + + @EnableWebSecurity + static class RequiresChannelInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .requiresChannel(requiresChannel -> + requiresChannel + .anyRequest().requiresSecure() + ); + // @formatter:on + } + } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java index d9ed42ff22a..bd68d2ecec9 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/PortMapperConfigurerTests.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. @@ -79,9 +79,10 @@ static class HttpMapsToInLambdaConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { // @formatter:off http - .requiresChannel() + .requiresChannel(requiresChannel -> + requiresChannel .anyRequest().requiresSecure() - .and() + ) .portMapper(portMapper -> portMapper .http(543).mapsTo(123) @@ -106,9 +107,10 @@ protected void configure(HttpSecurity http) throws Exception { customPortMapper.setPortMappings(Collections.singletonMap("543", "123")); // @formatter:off http - .requiresChannel() - .anyRequest().requiresSecure() - .and() + .requiresChannel(requiresChannel -> + requiresChannel + .anyRequest().requiresSecure() + ) .portMapper(portMapper -> portMapper .portMapper(customPortMapper) From c3dad06ea6097042fc6eaa689a045adefb80c295 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Fri, 5 Jul 2019 09:51:41 -0400 Subject: [PATCH 18/23] Allow configuration of request matchers through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 101 +++++++++++++++ .../HttpSecurityRequestMatchersTests.java | 119 +++++++++++++++++- .../RequestMatcherConfigurerTests.java | 33 +++++ 3 files changed, 252 insertions(+), 1 deletion(-) 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 c609fa60561..175cf5b11fe 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 @@ -2234,6 +2234,107 @@ public RequestMatcherConfigurer requestMatchers() { return requestMatcherConfigurer; } + /** + * Allows specifying which {@link HttpServletRequest} instances this + * {@link HttpSecurity} will be invoked on. This method allows for easily invoking the + * {@link HttpSecurity} for multiple different {@link RequestMatcher} instances. If + * only a single {@link RequestMatcher} is necessary consider using {@link #mvcMatcher(String)}, + * {@link #antMatcher(String)}, {@link #regexMatcher(String)}, or + * {@link #requestMatcher(RequestMatcher)}. + * + *

+ * Invoking {@link #requestMatchers()} will not override previous invocations of {@link #mvcMatcher(String)}}, + * {@link #requestMatchers()}, {@link #antMatcher(String)}, + * {@link #regexMatcher(String)}, and {@link #requestMatcher(RequestMatcher)}. + *

+ * + *

Example Configurations

+ * + * The following configuration enables the {@link HttpSecurity} for URLs that begin + * with "/api/" or "/oauth/". + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.requestMatchers(requestMatchers ->
+	 * 				requestMatchers
+	 * 					.antMatchers("/api/**", "/oauth/**")
+	 * 			)
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below is the same as the previous configuration. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.requestMatchers(requestMatchers ->
+	 * 				requestMatchers
+	 * 					.antMatchers("/api/**")
+	 * 					.antMatchers("/oauth/**")
+	 * 			)
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * The configuration below is also the same as the above configuration. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.requestMatchers(requestMatchers ->
+	 * 				requestMatchers
+	 * 					.antMatchers("/api/**")
+	 * 			)
+	 *			.requestMatchers(requestMatchers ->
+	 *			requestMatchers
+	 * 				.antMatchers("/oauth/**")
+	 * 			)
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.httpBasic(withDefaults());
+	 * 	}
+	 * }
+	 * 
+ * + * @param requestMatcherCustomizer the {@link Customizer} to provide more options for + * the {@link RequestMatcherConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity requestMatchers(Customizer requestMatcherCustomizer) throws Exception { + requestMatcherCustomizer.customize(requestMatcherConfigurer); + return HttpSecurity.this; + } + /** * Allows configuring the {@link HttpSecurity} to only be invoked when matching the * provided {@link RequestMatcher}. If more advanced configuration is necessary, diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java index c05f8064a63..ef4d4a2e425 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecurityRequestMatchersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -38,6 +38,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.config.Customizer.withDefaults; /** * @author Rob Winch @@ -195,6 +196,62 @@ public String path() { } } + @Test + public void requestMatchersWhenMvcMatcherInLambdaThenPathIsSecured() throws Exception { + loadConfig(RequestMatchersMvcMatcherInLambdaConfig.class); + + this.request.setServletPath("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/path.html"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/path/"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class RequestMatchersMvcMatcherInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers(requestMatchers -> + requestMatchers + .mvcMatchers("/path") + ) + .httpBasic(withDefaults()) + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().denyAll() + ); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + @Test public void requestMatchersMvcMatcherServletPath() throws Exception { loadConfig(RequestMatchersMvcMatcherServeltPathConfig.class); @@ -260,6 +317,66 @@ public String path() { } } + @Test + public void requestMatcherWhensMvcMatcherServletPathInLambdaThenPathIsSecured() throws Exception { + loadConfig(RequestMatchersMvcMatcherServletPathInLambdaConfig.class); + + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath(""); + this.request.setRequestURI("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + + setup(); + + this.request.setServletPath("/other"); + this.request.setRequestURI("/other/path"); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class RequestMatchersMvcMatcherServletPathInLambdaConfig + extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers(requestMatchers -> + requestMatchers + .mvcMatchers("/path").servletPath("/spring") + .mvcMatchers("/never-match") + ) + .httpBasic(withDefaults()) + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().denyAll() + ); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherConfigurerTests.java index c47b8f970fe..4bc131761bf 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestMatcherConfigurerTests.java @@ -71,4 +71,37 @@ protected void configure(HttpSecurity http) throws Exception { // @formatter:on } } + + @Test + public void authorizeRequestsWhenInvokedMultipleTimesInLambdaThenChainsPaths() throws Exception { + this.spring.register(AuthorizeRequestInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/oauth/abc")) + .andExpect(status().isForbidden()); + this.mvc.perform(get("/api/abc")) + .andExpect(status().isForbidden()); + } + + @EnableWebSecurity + static class AuthorizeRequestInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .requestMatchers(requestMatchers -> + requestMatchers + .antMatchers("/api/**") + ) + .requestMatchers(requestMatchers -> + requestMatchers + .antMatchers("/oauth/**") + ) + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().denyAll() + ); + // @formatter:on + } + } } From bf1bbd14e989282d477286156ec54126e07a18bb Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 10:03:29 -0400 Subject: [PATCH 19/23] Allow configuration of openid login through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 122 ++++++++++++ .../openid/OpenIDLoginConfigurer.java | 72 ++++++- .../openid/OpenIDLoginConfigurerTests.java | 178 ++++++++++++++++++ 3 files changed, 370 insertions(+), 2 deletions(-) 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 175cf5b11fe..d1ab52e23c0 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 @@ -239,6 +239,128 @@ public OpenIDLoginConfigurer openidLogin() throws Exception { return getOrApply(new OpenIDLoginConfigurer<>()); } + /** + * Allows configuring OpenID based authentication. + * + *

Example Configurations

+ * + * A basic example accepting the defaults and not using attribute exchange: + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.openidLogin(openidLogin ->
+	 * 				openidLogin
+	 * 					.permitAll()
+	 * 			);
+	 * 	}
+	 *
+	 * 	@Override
+	 * 	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+	 * 		auth.inMemoryAuthentication()
+	 * 				// the username must match the OpenID of the user you are
+	 * 				// logging in with
+	 * 				.withUser(
+	 * 						"https://www.google.com/accounts/o8/id?id=lmkCn9xzPdsxVwG7pjYMuDgNNdASFmobNkcRPaWU")
+	 * 				.password("password").roles("USER");
+	 * 	}
+	 * }
+	 * 
+ * + * A more advanced example demonstrating using attribute exchange and providing a + * custom AuthenticationUserDetailsService that will make any user that authenticates + * a valid user. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class OpenIDLoginConfig extends WebSecurityConfigurerAdapter {
+	 *
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.antMatchers("/**").hasRole("USER")
+	 * 			)
+	 * 			.openidLogin(openidLogin ->
+	 * 				openidLogin
+	 * 					.loginPage("/login")
+	 * 					.permitAll()
+	 * 					.authenticationUserDetailsService(
+	 * 						new AutoProvisioningUserDetailsService())
+	 * 					.attributeExchange(googleExchange ->
+	 * 						googleExchange
+	 * 							.identifierPattern("https://www.google.com/.*")
+	 * 							.attribute(emailAttribute ->
+	 * 								emailAttribute
+	 * 									.name("email")
+	 * 									.type("https://axschema.org/contact/email")
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(firstnameAttribute ->
+	 * 								firstnameAttribute
+	 * 									.name("firstname")
+	 * 									.type("https://axschema.org/namePerson/first")
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(lastnameAttribute ->
+	 * 								lastnameAttribute
+	 * 									.name("lastname")
+	 * 									.type("https://axschema.org/namePerson/last")
+	 * 									.required(true)
+	 * 							)
+	 * 					)
+	 * 					.attributeExchange(yahooExchange ->
+	 * 						yahooExchange
+	 * 							.identifierPattern(".*yahoo.com.*")
+	 * 							.attribute(emailAttribute ->
+	 * 								emailAttribute
+	 * 									.name("email")
+	 * 									.type("https://schema.openid.net/contact/email")
+	 * 									.required(true)
+	 * 							)
+	 * 							.attribute(fullnameAttribute ->
+	 * 								fullnameAttribute
+	 * 									.name("fullname")
+	 * 									.type("https://axschema.org/namePerson")
+	 * 									.required(true)
+	 * 							)
+	 * 					)
+	 * 			);
+	 * 	}
+	 * }
+	 *
+	 * public class AutoProvisioningUserDetailsService implements
+	 * 		AuthenticationUserDetailsService<OpenIDAuthenticationToken> {
+	 * 	public UserDetails loadUserDetails(OpenIDAuthenticationToken token)
+	 * 			throws UsernameNotFoundException {
+	 * 		return new User(token.getName(), "NOTUSED",
+	 * 				AuthorityUtils.createAuthorityList("ROLE_USER"));
+	 * 	}
+	 * }
+	 * 
+ * + * @see OpenIDLoginConfigurer + * + * @param openidLoginCustomizer the {@link Customizer} to provide more options for + * the {@link OpenIDLoginConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity openidLogin(Customizer> openidLoginCustomizer) throws Exception { + openidLoginCustomizer.customize(getOrApply(new OpenIDLoginConfigurer<>())); + return HttpSecurity.this; + } + /** * Adds the Security headers to the response. This is activated by default when using * {@link WebSecurityConfigurerAdapter}'s default constructor. Accepting the diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java index 71d7c2e2151..01e1b3198a9 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -27,6 +27,7 @@ import org.springframework.security.authentication.AuthenticationDetailsSource; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -148,6 +149,24 @@ public AttributeExchangeConfigurer attributeExchange(String identifierPattern) { return attributeExchangeConfigurer; } + /** + * Sets up OpenID attribute exchange for OpenIDs matching the specified pattern. + * The default pattern is ".*", it can be specified using + * {@link AttributeExchangeConfigurer#identifierPattern(String)} + * + * @param attributeExchangeCustomizer the {@link Customizer} to provide more options for + * the {@link AttributeExchangeConfigurer} + * @return a {@link OpenIDLoginConfigurer} for further customizations + * @throws Exception + */ + public OpenIDLoginConfigurer attributeExchange(Customizer attributeExchangeCustomizer) + throws Exception { + AttributeExchangeConfigurer attributeExchangeConfigurer = new AttributeExchangeConfigurer(".*"); + attributeExchangeCustomizer.customize(attributeExchangeConfigurer); + this.attributeExchangeConfigurers.add(attributeExchangeConfigurer); + return this; + } + /** * Allows specifying the {@link OpenIDConsumer} to be used. The default is using an * {@link OpenID4JavaConsumer}. @@ -373,7 +392,7 @@ private void initDefaultLoginFilter(H http) { * @author Rob Winch */ public final class AttributeExchangeConfigurer { - private final String identifier; + private String identifier; private List attributes = new ArrayList<>(); private List attributeConfigurers = new ArrayList<>(); @@ -395,6 +414,19 @@ public OpenIDLoginConfigurer and() { return OpenIDLoginConfigurer.this; } + /** + * Sets the regular expression for matching on OpenID's (i.e. + * "https://www.google.com/.*", ".*yahoo.com.*", etc) + * + * @param identifierPattern the regular expression for matching on OpenID's + * @return the {@link AttributeExchangeConfigurer} for further customization of + * attribute exchange + */ + public AttributeExchangeConfigurer identifierPattern(String identifierPattern) { + this.identifier = identifierPattern; + return this; + } + /** * Adds an {@link OpenIDAttribute} to be obtained for the configured OpenID * pattern. @@ -419,6 +451,22 @@ public AttributeConfigurer attribute(String name) { return attributeConfigurer; } + /** + * Adds an {@link OpenIDAttribute} named "default-attribute". + * The name can by updated using {@link AttributeConfigurer#name(String)}. + * + * @param attributeCustomizer the {@link Customizer} to provide more options for + * the {@link AttributeConfigurer} + * @return a {@link AttributeExchangeConfigurer} for further customizations + * @throws Exception + */ + public AttributeExchangeConfigurer attribute(Customizer attributeCustomizer) throws Exception { + AttributeConfigurer attributeConfigurer = new AttributeConfigurer(); + attributeCustomizer.customize(attributeConfigurer); + this.attributeConfigurers.add(attributeConfigurer); + return this; + } + /** * Gets the {@link OpenIDAttribute}'s for the configured OpenID pattern * @return @@ -443,6 +491,16 @@ public final class AttributeConfigurer { private boolean required = false; private String type; + /** + * Creates a new instance named "default-attribute". + * The name can by updated using {@link #name(String)}. + * + * @see AttributeExchangeConfigurer#attribute(String) + */ + private AttributeConfigurer() { + this.name = "default-attribute"; + } + /** * Creates a new instance * @param name the name of the attribute @@ -486,6 +544,16 @@ public AttributeConfigurer type(String type) { return this; } + /** + * The OpenID attribute name. + * @param name + * @return the {@link AttributeConfigurer} for further customizations + */ + public AttributeConfigurer name(String name) { + this.name = name; + return this; + } + /** * Gets the {@link AttributeExchangeConfigurer} for further customization of * the attributes diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java index d18efe0b374..159c54967ea 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/openid/OpenIDLoginConfigurerTests.java @@ -16,8 +16,13 @@ package org.springframework.security.config.annotation.web.configurers.openid; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; import org.junit.Rule; import org.junit.Test; +import org.openid4java.consumer.ConsumerManager; +import org.openid4java.discovery.DiscoveryInformation; +import org.openid4java.message.AuthRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -26,13 +31,23 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.openid.OpenIDAttribute; import org.springframework.security.openid.OpenIDAuthenticationFilter; import org.springframework.security.openid.OpenIDAuthenticationProvider; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.openid4java.discovery.yadis.YadisResolver.YADIS_XRDS_LOCATION; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -128,4 +143,167 @@ protected void configure(HttpSecurity http) throws Exception { // @formatter:on } } + + @Test + public void requestWhenOpenIdLoginPageInLambdaThenRedirectsToLoginPAge() throws Exception { + this.spring.register(OpenIdLoginPageInLambdaConfig.class).autowire(); + + this.mvc.perform(get("/")) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("http://localhost/login/custom")); + } + + @EnableWebSecurity + static class OpenIdLoginPageInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .openidLogin(openIdLogin -> + openIdLogin + .loginPage("/login/custom") + ); + // @formatter:on + } + } + + @Test + public void requestWhenAttributeExchangeConfiguredThenFetchAttributesMatchAttributeList() throws Exception { + OpenIdAttributesInLambdaConfig.CONSUMER_MANAGER = mock(ConsumerManager.class); + AuthRequest mockAuthRequest = mock(AuthRequest.class); + DiscoveryInformation mockDiscoveryInformation = mock(DiscoveryInformation.class); + when(mockAuthRequest.getDestinationUrl(anyBoolean())).thenReturn("mockUrl"); + when(OpenIdAttributesInLambdaConfig.CONSUMER_MANAGER.associate(any())) + .thenReturn(mockDiscoveryInformation); + when(OpenIdAttributesInLambdaConfig.CONSUMER_MANAGER.authenticate(any(DiscoveryInformation.class), any(), any())) + .thenReturn(mockAuthRequest); + this.spring.register(OpenIdAttributesInLambdaConfig.class).autowire(); + + try ( MockWebServer server = new MockWebServer() ) { + String endpoint = server.url("/").toString(); + + server.enqueue(new MockResponse() + .addHeader(YADIS_XRDS_LOCATION, endpoint)); + server.enqueue(new MockResponse() + .setBody(String.format("%s", endpoint))); + + MvcResult mvcResult = this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, endpoint)) + .andExpect(status().isFound()) + .andReturn(); + + Object attributeObject = mvcResult.getRequest().getSession().getAttribute("SPRING_SECURITY_OPEN_ID_ATTRIBUTES_FETCH_LIST"); + assertThat(attributeObject).isInstanceOf(List.class); + List attributeList = (List) attributeObject; + assertThat(attributeList.stream().anyMatch(attribute -> + "nickname".equals(attribute.getName()) + && "https://schema.openid.net/namePerson/friendly".equals(attribute.getType()))) + .isTrue(); + assertThat(attributeList.stream().anyMatch(attribute -> + "email".equals(attribute.getName()) + && "https://schema.openid.net/contact/email".equals(attribute.getType()) + && attribute.isRequired() + && attribute.getCount() == 2)) + .isTrue(); + } + } + + @EnableWebSecurity + static class OpenIdAttributesInLambdaConfig extends WebSecurityConfigurerAdapter { + static ConsumerManager CONSUMER_MANAGER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() + ) + .openidLogin(openIdLogin -> + openIdLogin + .consumerManager(CONSUMER_MANAGER) + .attributeExchange(attributeExchange -> + attributeExchange + .identifierPattern(".*") + .attribute(nicknameAttribute -> + nicknameAttribute + .name("nickname") + .type("https://schema.openid.net/namePerson/friendly") + ) + .attribute(emailAttribute -> + emailAttribute + .name("email") + .type("https://schema.openid.net/contact/email") + .required(true) + .count(2) + ) + ) + ); + // @formatter:on + } + } + + @Test + public void requestWhenAttributeNameNotSpecifiedThenAttributeNameDefaulted() + throws Exception { + OpenIdAttributesNullNameConfig.CONSUMER_MANAGER = mock(ConsumerManager.class); + AuthRequest mockAuthRequest = mock(AuthRequest.class); + DiscoveryInformation mockDiscoveryInformation = mock(DiscoveryInformation.class); + when(mockAuthRequest.getDestinationUrl(anyBoolean())).thenReturn("mockUrl"); + when(OpenIdAttributesNullNameConfig.CONSUMER_MANAGER.associate(any())) + .thenReturn(mockDiscoveryInformation); + when(OpenIdAttributesNullNameConfig.CONSUMER_MANAGER.authenticate(any(DiscoveryInformation.class), any(), any())) + .thenReturn(mockAuthRequest); + this.spring.register(OpenIdAttributesNullNameConfig.class).autowire(); + + try ( MockWebServer server = new MockWebServer() ) { + String endpoint = server.url("/").toString(); + + server.enqueue(new MockResponse() + .addHeader(YADIS_XRDS_LOCATION, endpoint)); + server.enqueue(new MockResponse() + .setBody(String.format("%s", endpoint))); + + MvcResult mvcResult = this.mvc.perform(get("/login/openid") + .param(OpenIDAuthenticationFilter.DEFAULT_CLAIMED_IDENTITY_FIELD, endpoint)) + .andExpect(status().isFound()) + .andReturn(); + + Object attributeObject = mvcResult.getRequest().getSession().getAttribute("SPRING_SECURITY_OPEN_ID_ATTRIBUTES_FETCH_LIST"); + assertThat(attributeObject).isInstanceOf(List.class); + List attributeList = (List) attributeObject; + assertThat(attributeList).hasSize(1); + assertThat(attributeList.get(0).getName()).isEqualTo("default-attribute"); + } + } + + @EnableWebSecurity + static class OpenIdAttributesNullNameConfig extends WebSecurityConfigurerAdapter { + static ConsumerManager CONSUMER_MANAGER; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().permitAll() + ) + .openidLogin(openIdLogin -> + openIdLogin + .consumerManager(CONSUMER_MANAGER) + .attributeExchange(attributeExchange -> + attributeExchange + .identifierPattern(".*") + .attribute(withDefaults()) + ) + ); + // @formatter:on + } + } } From e47389e60b660e33a472e7671a3576a36479e619 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 13:00:27 -0400 Subject: [PATCH 20/23] Allow configuration of oauth2 login through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 97 ++++++++++ .../oauth2/client/OAuth2LoginConfigurer.java | 57 ++++++ .../client/OAuth2LoginConfigurerTests.java | 183 ++++++++++++++++++ 3 files changed, 337 insertions(+) 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 d1ab52e23c0..6e5b4c82813 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 @@ -1948,6 +1948,103 @@ public OAuth2LoginConfigurer oauth2Login() throws Exception { return getOrApply(new OAuth2LoginConfigurer<>()); } + /** + * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + *
+ *
+ * + * The "authentication flow" is implemented using the Authorization Code Grant, as specified in the + * OAuth 2.0 Authorization Framework + * and OpenID Connect Core 1.0 + * specification. + *
+ *
+ * + * As a prerequisite to using this feature, you must register a client with a provider. + * The client registration information may than be used for configuring + * a {@link org.springframework.security.oauth2.client.registration.ClientRegistration} using a + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration.Builder}. + *
+ *
+ * + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration}(s) are composed within a + * {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository}, + * which is required and must be registered with the {@link ApplicationContext} or + * configured via oauth2Login().clientRegistrationRepository(..). + *
+ *
+ * + * The default configuration provides an auto-generated login page at "/login" and + * redirects to "/login?error" when an authentication error occurs. + * The login page will display each of the clients with a link + * that is capable of initiating the "authentication flow". + *
+ *
+ * + *

+ *

Example Configuration

+ * + * The following example shows the minimal configuration required, using Google as the Authentication Provider. + * + *
+	 * @Configuration
+	 * public class OAuth2LoginConfig {
+	 *
+	 * 	@EnableWebSecurity
+	 * 	public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * 		@Override
+	 * 		protected void configure(HttpSecurity http) throws Exception {
+	 * 			http
+	 * 				.authorizeRequests(authorizeRequests ->
+	 * 					authorizeRequests
+	 * 						.anyRequest().authenticated()
+	 * 				)
+	 * 				.oauth2Login(withDefaults());
+	 *		}
+	 *	}
+	 *
+	 *	@Bean
+	 *	public ClientRegistrationRepository clientRegistrationRepository() {
+	 *		return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
+	 *	}
+	 *
+	 * 	private ClientRegistration googleClientRegistration() {
+	 * 		return ClientRegistration.withRegistrationId("google")
+	 * 			.clientId("google-client-id")
+	 * 			.clientSecret("google-client-secret")
+	 * 			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+	 * 			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+	 * 			.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
+	 * 			.scope("openid", "profile", "email", "address", "phone")
+	 * 			.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
+	 * 			.tokenUri("https://www.googleapis.com/oauth2/v4/token")
+	 * 			.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
+	 * 			.userNameAttributeName(IdTokenClaimNames.SUB)
+	 * 			.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
+	 * 			.clientName("Google")
+	 * 			.build();
+	 *	}
+	 * }
+	 * 
+ * + *

+ * For more advanced configuration, see {@link OAuth2LoginConfigurer} for available options to customize the defaults. + * + * @see Section 4.1 Authorization Code Grant + * @see Section 3.1 Authorization Code Flow + * @see org.springframework.security.oauth2.client.registration.ClientRegistration + * @see org.springframework.security.oauth2.client.registration.ClientRegistrationRepository + * + * @param oauth2LoginCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2LoginConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity oauth2Login(Customizer> oauth2LoginCustomizer) throws Exception { + oauth2LoginCustomizer.customize(getOrApply(new OAuth2LoginConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures OAuth 2.0 Client support. * diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 5e0bb87a2f8..b54b5225912 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -20,6 +20,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; @@ -201,6 +202,20 @@ public AuthorizationEndpointConfig authorizationEndpoint() { return this.authorizationEndpointConfig; } + /** + * Configures the Authorization Server's Authorization Endpoint. + * + * @param authorizationEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link AuthorizationEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer authorizationEndpoint(Customizer authorizationEndpointCustomizer) + throws Exception { + authorizationEndpointCustomizer.customize(this.authorizationEndpointConfig); + return this; + } + /** * Configuration options for the Authorization Server's Authorization Endpoint. */ @@ -268,6 +283,20 @@ public TokenEndpointConfig tokenEndpoint() { return this.tokenEndpointConfig; } + /** + * Configures the Authorization Server's Token Endpoint. + * + * @param tokenEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link TokenEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer tokenEndpoint(Customizer tokenEndpointCustomizer) + throws Exception { + tokenEndpointCustomizer.customize(this.tokenEndpointConfig); + return this; + } + /** * Configuration options for the Authorization Server's Token Endpoint. */ @@ -310,6 +339,20 @@ public RedirectionEndpointConfig redirectionEndpoint() { return this.redirectionEndpointConfig; } + /** + * Configures the Client's Redirection Endpoint. + * + * @param redirectionEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link RedirectionEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer redirectionEndpoint(Customizer redirectionEndpointCustomizer) + throws Exception { + redirectionEndpointCustomizer.customize(this.redirectionEndpointConfig); + return this; + } + /** * Configuration options for the Client's Redirection Endpoint. */ @@ -350,6 +393,20 @@ public UserInfoEndpointConfig userInfoEndpoint() { return this.userInfoEndpointConfig; } + /** + * Configures the Authorization Server's UserInfo Endpoint. + * + * @param userInfoEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link UserInfoEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer userInfoEndpoint(Customizer userInfoEndpointCustomizer) + throws Exception { + userInfoEndpointCustomizer.customize(this.userInfoEndpointConfig); + return this; + } + /** * Configuration options for the Authorization Server's UserInfo Endpoint. */ diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index f33c9d02016..a61a99f8726 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -176,6 +176,25 @@ public void oauth2Login() throws Exception { .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); } + @Test + public void requestWhenOauth2LoginInLambdaThenAuthenticationContainsOauth2UserAuthority() throws Exception { + loadConfig(OAuth2LoginInLambdaConfig.class); + OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest(); + this.authorizationRequestRepository.saveAuthorizationRequest( + authorizationRequest, this.request, this.response); + this.request.setParameter("code", "code123"); + this.request.setParameter("state", authorizationRequest.getState()); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); + } + // gh-6009 @Test public void oauth2LoginWhenSuccessThenAuthenticationSuccessEventPublished() throws Exception { @@ -303,6 +322,29 @@ public void oauth2LoginWithCustomAuthorizationRequestParameters() throws Excepti assertThat(this.response.getRedirectedUrl()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); } + @Test + public void requestWhenOauth2LoginWithCustomAuthorizationRequestParametersThenParametersInRedirectedUrl() + throws Exception { + loadConfig(OAuth2LoginConfigCustomAuthorizationRequestResolverInLambda.class); + OAuth2AuthorizationRequestResolver resolver = this.context.getBean( + OAuth2LoginConfigCustomAuthorizationRequestResolverInLambda.class).resolver; + OAuth2AuthorizationRequest result = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri("https://accounts.google.com/authorize") + .clientId("client-id") + .state("adsfa") + .authorizationRequestUri("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1") + .build(); + when(resolver.resolve(any())).thenReturn(result); + + String requestUri = "/oauth2/authorization/google"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); + } + // gh-5347 @Test public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception { @@ -374,6 +416,19 @@ public void oauth2LoginWithCustomLoginPageThenRedirectCustomLoginPage() throws E assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); } + @Test + public void requestWhenOauth2LoginWithCustomLoginPageInLambdaThenRedirectCustomLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigCustomLoginPageInLambda.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); + } + @Test public void oidcLogin() throws Exception { // setup application context @@ -400,6 +455,32 @@ public void oidcLogin() throws Exception { .isInstanceOf(OidcUserAuthority.class).hasToString("ROLE_USER"); } + @Test + public void requestWhenOauth2LoginInLambdaAndOidcThenAuthenticationContainsOidcUserAuthority() throws Exception { + // setup application context + loadConfig(OAuth2LoginInLambdaConfig.class, JwtDecoderFactoryConfig.class); + + // setup authorization request + OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid"); + this.authorizationRequestRepository.saveAuthorizationRequest( + authorizationRequest, this.request, this.response); + + // setup authentication parameters + this.request.setParameter("code", "code123"); + this.request.setParameter("state", authorizationRequest.getState()); + + // perform test + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + // assertions + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(OidcUserAuthority.class).hasToString("ROLE_USER"); + } + @Test public void oidcLoginCustomWithConfigurer() throws Exception { // setup application context @@ -521,6 +602,30 @@ public void onApplicationEvent(AuthenticationSuccessEvent event) { } } + @EnableWebSecurity + static class OAuth2LoginInLambdaConfig extends CommonLambdaWebSecurityConfigurerAdapter + implements ApplicationListener { + static List EVENTS = new ArrayList<>(); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login(oauth2Login -> + oauth2Login + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + ); + // @formatter:on + super.configure(http); + } + + @Override + public void onApplicationEvent(AuthenticationSuccessEvent event) { + EVENTS.add(event); + } + } + @EnableWebSecurity static class OAuth2LoginConfigCustomWithConfigurer extends CommonWebSecurityConfigurerAdapter { @Override @@ -586,6 +691,28 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class OAuth2LoginConfigCustomAuthorizationRequestResolverInLambda extends CommonLambdaWebSecurityConfigurerAdapter { + private ClientRegistrationRepository clientRegistrationRepository = + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); + + OAuth2AuthorizationRequestResolver resolver = mock(OAuth2AuthorizationRequestResolver.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login(oauth2Login -> + oauth2Login + .clientRegistrationRepository(this.clientRegistrationRepository) + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint + .authorizationRequestResolver(this.resolver) + ) + ); + super.configure(http); + } + } + @EnableWebSecurity static class OAuth2LoginConfigMultipleClients extends CommonWebSecurityConfigurerAdapter { @Override @@ -612,6 +739,23 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class OAuth2LoginConfigCustomLoginPageInLambda extends CommonLambdaWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login(oauth2Login -> + oauth2Login + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + .loginPage("/custom-login") + ); + // @formatter:on + super.configure(http); + } + } + @EnableWebSecurity static class OAuth2LoginConfigWithOidcLogoutSuccessHandler extends CommonWebSecurityConfigurerAdapter { @Override @@ -667,6 +811,45 @@ HttpSessionOAuth2AuthorizationRequestRepository oauth2AuthorizationRequestReposi } } + private static abstract class CommonLambdaWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .securityContext(securityContext -> + securityContext + .securityContextRepository(securityContextRepository()) + ) + .oauth2Login(oauth2Login -> + oauth2Login + .tokenEndpoint(tokenEndpoint -> + tokenEndpoint + .accessTokenResponseClient(createOauth2AccessTokenResponseClient()) + ) + .userInfoEndpoint(userInfoEndpoint -> + userInfoEndpoint + .userService(createOauth2UserService()) + .oidcUserService(createOidcUserService()) + ) + ); + // @formatter:on + } + + @Bean + SecurityContextRepository securityContextRepository() { + return new HttpSessionSecurityContextRepository(); + } + + @Bean + HttpSessionOAuth2AuthorizationRequestRepository oauth2AuthorizationRequestRepository() { + return new HttpSessionOAuth2AuthorizationRequestRepository(); + } + } + @Configuration static class JwtDecoderFactoryConfig { From 415760838f7d4c9df69323a6b9f093f79fd291b6 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 13:31:08 -0400 Subject: [PATCH 21/23] Allow configuration of oauth2 client through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 35 ++++++++++++++++ .../oauth2/client/OAuth2ClientConfigurer.java | 17 +++++++- .../client/OAuth2ClientConfigurerTests.java | 40 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) 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 6e5b4c82813..cffdad7824a 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 @@ -2059,6 +2059,41 @@ public OAuth2ClientConfigurer oauth2Client() throws Exception { return configurer; } + /** + * Configures OAuth 2.0 Client support. + * + *

Example Configuration

+ * + * The following example demonstrates how to enable OAuth 2.0 Client support for all endpoints. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.anyRequest().authenticated()
+	 * 			)
+	 * 			.oauth2Client(withDefaults());
+	 *	}
+	 * }
+	 * 
+ * + * @see OAuth 2.0 Authorization Framework + * + * @param oauth2ClientCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2ClientConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity oauth2Client(Customizer> oauth2ClientCustomizer) throws Exception { + oauth2ClientCustomizer.customize(getOrApply(new OAuth2ClientConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures OAuth 2.0 Resource Server support. * diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java index f4a5c2c366b..066ca0c692e 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.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. @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.client; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; @@ -135,6 +136,20 @@ public AuthorizationCodeGrantConfigurer authorizationCodeGrant() { return this.authorizationCodeGrantConfigurer; } + /** + * Configures the OAuth 2.0 Authorization Code Grant. + * + * @param authorizationCodeGrantCustomizer the {@link Customizer} to provide more options for + * the {@link AuthorizationCodeGrantConfigurer} + * @return the {@link OAuth2ClientConfigurer} for further customizations + * @throws Exception + */ + public OAuth2ClientConfigurer authorizationCodeGrant(Customizer authorizationCodeGrantCustomizer) + throws Exception { + authorizationCodeGrantCustomizer.customize(this.authorizationCodeGrantConfigurer); + return this; + } + /** * Configuration options for the OAuth 2.0 Authorization Code Grant. */ diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java index 269641783f4..5a231dc28ab 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java @@ -65,6 +65,7 @@ import static org.assertj.core.api.Assertions.assertThat; 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.authentication; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -141,6 +142,19 @@ public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization() "redirect_uri=http://localhost/client-1"); } + @Test + public void configureWhenOauth2ClientInLambdaThenRedirectForAuthorization() throws Exception { + this.spring.register(OAuth2ClientInLambdaConfig.class).autowire(); + + MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?" + + "response_type=code&client_id=client-1&" + + "scope=user&state=.{15,}&" + + "redirect_uri=http://localhost/client-1"); + } + @Test public void configureWhenAuthorizationCodeResponseSuccessThenAuthorizedClientSaved() throws Exception { this.spring.register(OAuth2ClientConfig.class).autowire(); @@ -248,4 +262,30 @@ public String resource1(@RegisteredOAuth2AuthorizedClient("registration-1") OAut } } } + + @EnableWebSecurity + @EnableWebMvc + static class OAuth2ClientInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .oauth2Client(withDefaults()); + // @formatter:on + } + + @Bean + public ClientRegistrationRepository clientRegistrationRepository() { + return clientRegistrationRepository; + } + + @Bean + public OAuth2AuthorizedClientRepository authorizedClientRepository() { + return authorizedClientRepository; + } + } } From 4b2539df10dd346d3f4e77d8c6703cb40066ed09 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 14:22:16 -0400 Subject: [PATCH 22/23] Allow configuration of oauth2 resource server through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 49 +++++ .../OAuth2ResourceServerConfigurer.java | 45 ++++- .../OAuth2ResourceServerConfigurerTests.java | 183 ++++++++++++++++++ 3 files changed, 272 insertions(+), 5 deletions(-) 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 cffdad7824a..fac8ba9b6e4 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 @@ -2108,6 +2108,55 @@ public OAuth2ResourceServerConfigurer oauth2ResourceServer() throw return configurer; } + /** + * Configures OAuth 2.0 Resource Server support. + * + *

Example Configuration

+ * + * The following example demonstrates how to configure a custom JWT authentication converter. + * + *
+	 * @Configuration
+	 * @EnableWebSecurity
+	 * public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * 	@Override
+	 * 	protected void configure(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeRequests(authorizeRequests ->
+	 * 				authorizeRequests
+	 * 					.anyRequest().authenticated()
+	 * 			)
+	 * 			.oauth2ResourceServer(oauth2ResourceServer ->
+	 * 				oauth2ResourceServer
+	 * 					.jwt(jwt ->
+	 * 						jwt
+	 * 							.jwtAuthenticationConverter(jwtDecoder())
+	 * 					)
+	 * 			);
+	 *	}
+	 *
+	 * 	@Bean
+	 * 	public JwtDecoder jwtDecoder() {
+	 * 		return JwtDecoders.fromOidcIssuerLocation(issuerUri);
+	 * 	}
+	 * }
+	 * 
+ * + * @see OAuth 2.0 Authorization Framework + * + * @param oauth2ResourceServerCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2ResourceServerConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity oauth2ResourceServer(Customizer> oauth2ResourceServerCustomizer) + throws Exception { + OAuth2ResourceServerConfigurer configurer = getOrApply(new OAuth2ResourceServerConfigurer<>(getContext())); + this.postProcess(configurer); + oauth2ResourceServerCustomizer.customize(configurer); + return HttpSecurity.this; + } + /** * Configures channel security. In order for this configuration to be useful at least * one mapping to a required channel must be provided. diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java index 2d4ffb0cbd6..589dd391e56 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; @@ -65,11 +66,12 @@ *
  • {@link #accessDeniedHandler(AccessDeniedHandler)}
  • - customizes how access denied errors are handled *
  • {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
  • - customizes how authentication failures are handled *
  • {@link #bearerTokenResolver(BearerTokenResolver)} - customizes how to resolve a bearer token from the request
  • - *
  • {@link #jwt()} - enables Jwt-encoded bearer token support
  • + *
  • {@link #jwt(Customizer)} - enables Jwt-encoded bearer token support
  • + *
  • {@link #opaqueToken(Customizer)} - enables opaque bearer token support
  • * * *

    - * When using {@link #jwt()}, either + * When using {@link #jwt(Customizer)}, either * *

      *
    • @@ -83,7 +85,7 @@ *
    • *
    * - * Also with {@link #jwt()} consider + * Also with {@link #jwt(Customizer)} consider * *
      *
    • @@ -93,12 +95,12 @@ *
    * *

    - * When using {@link #opaque()}, supply an introspection endpoint and its authentication configuration + * When using {@link #opaqueToken(Customizer)}, supply an introspection endpoint and its authentication configuration *

    * *

    Security Filters

    * - * The following {@code Filter}s are populated when {@link #jwt()} is configured: + * The following {@code Filter}s are populated when {@link #jwt(Customizer)} is configured: * *
      *
    • {@link BearerTokenAuthenticationFilter}
    • @@ -180,6 +182,22 @@ public JwtConfigurer jwt() { return this.jwtConfigurer; } + /** + * Enables Jwt-encoded bearer token support. + * + * @param jwtCustomizer the {@link Customizer} to provide more options for + * the {@link JwtConfigurer} + * @return the {@link OAuth2ResourceServerConfigurer} for further customizations + * @throws Exception + */ + public OAuth2ResourceServerConfigurer jwt(Customizer jwtCustomizer) throws Exception { + if ( this.jwtConfigurer == null ) { + this.jwtConfigurer = new JwtConfigurer(this.context); + } + jwtCustomizer.customize(this.jwtConfigurer); + return this; + } + public OpaqueTokenConfigurer opaqueToken() { if (this.opaqueTokenConfigurer == null) { this.opaqueTokenConfigurer = new OpaqueTokenConfigurer(this.context); @@ -188,6 +206,23 @@ public OpaqueTokenConfigurer opaqueToken() { return this.opaqueTokenConfigurer; } + /** + * Enables opaque bearer token support. + * + * @param opaqueTokenCustomizer the {@link Customizer} to provide more options for + * the {@link OpaqueTokenConfigurer} + * @return the {@link OAuth2ResourceServerConfigurer} for further customizations + * @throws Exception + */ + public OAuth2ResourceServerConfigurer opaqueToken(Customizer opaqueTokenCustomizer) + throws Exception { + if (this.opaqueTokenConfigurer == null) { + this.opaqueTokenConfigurer = new OpaqueTokenConfigurer(this.context); + } + opaqueTokenCustomizer.customize(this.opaqueTokenConfigurer); + return this; + } + @Override public void init(H http) throws Exception { registerDefaultAccessDeniedHandler(http); diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java index 995e7d98a8e..494ff7d74b8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/resource/OAuth2ResourceServerConfigurerTests.java @@ -127,6 +127,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes; import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri; import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey; @@ -184,6 +185,19 @@ public void getWhenUsingDefaultsWithValidBearerTokenThenAcceptsRequest() .andExpect(content().string("ok")); } + @Test + public void getWhenUsingDefaultsInLambdaWithValidBearerTokenThenAcceptsRequest() + throws Exception { + + this.spring.register(RestOperationsConfig.class, DefaultInLambdaConfig.class, BasicController.class).autowire(); + mockRestOperations(jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("ok")); + } + @Test public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception { this.spring.register(WebServerConfig.class, JwkSetUriConfig.class, BasicController.class).autowire(); @@ -195,6 +209,16 @@ public void getWhenUsingJwkSetUriThenAcceptsRequest() throws Exception { .andExpect(content().string("ok")); } + @Test + public void getWhenUsingJwkSetUriInLambdaThenAcceptsRequest() throws Exception { + this.spring.register(WebServerConfig.class, JwkSetUriInLambdaConfig.class, BasicController.class).autowire(); + mockWebServer(jwks("Default")); + String token = this.token("ValidNoScopes"); + + this.mvc.perform(get("/").with(bearerToken(token))) + .andExpect(status().isOk()) + .andExpect(content().string("ok")); + } @Test public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken() @@ -756,6 +780,23 @@ public void requestWhenCustomJwtDecoderWiredOnDslThenUsed() .andExpect(content().string(JWT_SUBJECT)); } + @Test + public void requestWhenCustomJwtDecoderInLambdaOnDslThenUsed() + throws Exception { + + this.spring.register(CustomJwtDecoderInLambdaOnDsl.class, BasicController.class).autowire(); + + CustomJwtDecoderInLambdaOnDsl config = this.spring.getContext().getBean(CustomJwtDecoderInLambdaOnDsl.class); + JwtDecoder decoder = config.decoder(); + + when(decoder.decode(anyString())).thenReturn(JWT); + + this.mvc.perform(get("/authenticated") + .with(bearerToken(JWT_TOKEN))) + .andExpect(status().isOk()) + .andExpect(content().string(JWT_SUBJECT)); + } + @Test public void requestWhenCustomJwtDecoderExposedAsBeanThenUsed() throws Exception { @@ -1067,6 +1108,17 @@ public void getWhenIntrospectingThenOk() throws Exception { .andExpect(content().string("test-subject")); } + @Test + public void getWhenOpaqueTokenInLambdaAndIntrospectingThenOk() throws Exception { + this.spring.register(RestOperationsConfig.class, OpaqueTokenInLambdaConfig.class, BasicController.class).autowire(); + mockRestOperations(json("Active")); + + this.mvc.perform(get("/authenticated") + .with(bearerToken("token"))) + .andExpect(status().isOk()) + .andExpect(content().string("test-subject")); + } + @Test public void getWhenIntrospectionFailsThenUnauthorized() throws Exception { this.spring.register(RestOperationsConfig.class, OpaqueTokenConfig.class).autowire(); @@ -1104,6 +1156,20 @@ public void getWhenCustomIntrospectionAuthenticationManagerThenUsed() throws Exc verifyBean(AuthenticationProvider.class).authenticate(any(Authentication.class)); } + @Test + public void getWhenCustomIntrospectionAuthenticationManagerInLambdaThenUsed() throws Exception { + this.spring.register(OpaqueTokenAuthenticationManagerInLambdaConfig.class, BasicController.class).autowire(); + + when(bean(AuthenticationProvider.class).authenticate(any(Authentication.class))) + .thenReturn(INTROSPECTION_AUTHENTICATION_TOKEN); + this.mvc.perform(get("/authenticated") + .with(bearerToken("token"))) + .andExpect(status().isOk()) + .andExpect(content().string("mock-test-subject")); + + verifyBean(AuthenticationProvider.class).authenticate(any(Authentication.class)); + } + @Test public void configureWhenOnlyIntrospectionUrlThenException() throws Exception { assertThatCode(() -> this.spring.register(OpaqueTokenHalfConfiguredConfig.class).autowire()) @@ -1311,6 +1377,26 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class DefaultInLambdaConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(withDefaults()) + ); + // @formatter:on + } + } + @EnableWebSecurity static class JwkSetUriConfig extends WebSecurityConfigurerAdapter { @Value("${mockwebserver.url:https://example.org}") @@ -1331,6 +1417,31 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class JwkSetUriInLambdaConfig extends WebSecurityConfigurerAdapter { + @Value("${mockwebserver.url:https://example.org}") + String jwkSetUri; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .antMatchers("/requires-read-scope").access("hasAuthority('SCOPE_message:read')") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .jwkSetUri(this.jwkSetUri) + ) + ); + // @formatter:on + } + } + @EnableWebSecurity static class CsrfDisabledConfig extends WebSecurityConfigurerAdapter { @Value("${mockwebserver.url:https://example.org}") @@ -1677,6 +1788,33 @@ JwtDecoder decoder() { } } + @EnableWebSecurity + static class CustomJwtDecoderInLambdaOnDsl extends WebSecurityConfigurerAdapter { + JwtDecoder decoder = mock(JwtDecoder.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .jwt(jwt -> + jwt + .decoder(decoder()) + ) + ); + // @formatter:on + } + + JwtDecoder decoder() { + return this.decoder; + } + } + @EnableWebSecurity static class CustomJwtDecoderAsBean extends WebSecurityConfigurerAdapter { @Override @@ -1831,6 +1969,25 @@ protected void configure(HttpSecurity http) throws Exception { } } + @EnableWebSecurity + static class OpaqueTokenInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .antMatchers("/requires-read-scope").hasAuthority("SCOPE_message:read") + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .opaqueToken(withDefaults()) + ); + // @formatter:on + } + } + @EnableWebSecurity static class OpaqueTokenAuthenticationManagerConfig extends WebSecurityConfigurerAdapter { @Override @@ -1852,6 +2009,32 @@ public AuthenticationProvider authenticationProvider() { } } + @EnableWebSecurity + static class OpaqueTokenAuthenticationManagerInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer + .opaqueToken(opaqueToken -> + opaqueToken + .authenticationManager(authenticationProvider()::authenticate) + ) + ); + // @formatter:on + } + + @Bean + public AuthenticationProvider authenticationProvider() { + return mock(AuthenticationProvider.class); + } + } + @EnableWebSecurity static class OpaqueAndJwtConfig extends WebSecurityConfigurerAdapter { @Override From 82f38862fbdf4f44115592247ebe59d16e88f032 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Fri, 12 Jul 2019 09:26:50 -0400 Subject: [PATCH 23/23] Allow configuration of session fixation and concurrency through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 7 ++- .../SessionManagementConfigurer.java | 40 ++++++++++++++ .../SessionManagementConfigurerTests.java | 52 ++++++++++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) 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 fac8ba9b6e4..69bc3ab70ce 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 @@ -681,8 +681,11 @@ public SessionManagementConfigurer sessionManagement() throws Exce * ) * .sessionManagement(sessionManagement -> * sessionManagement - * .maximumSessions(1) - * .expiredUrl("/login?expired") + * .sessionConcurrency(sessionConcurrency -> + * sessionConcurrency + * .maximumSessions(1) + * .expiredUrl("/login?expired") + * ) * ); * } * } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java index cb7c2205589..ad8f4e033ac 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.context.event.GenericApplicationListenerAdapter; import org.springframework.context.event.SmartApplicationListener; import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -249,6 +250,19 @@ public SessionFixationConfigurer sessionFixation() { return new SessionFixationConfigurer(); } + /** + * Allows configuring session fixation protection. + * + * @param sessionFixationCustomizer the {@link Customizer} to provide more options for + * the {@link SessionFixationConfigurer} + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public SessionManagementConfigurer sessionFixation(Customizer sessionFixationCustomizer) + throws Exception { + sessionFixationCustomizer.customize(new SessionFixationConfigurer()); + return this; + } + /** * Controls the maximum number of sessions for a user. The default is to allow any * number of users. @@ -260,6 +274,20 @@ public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { return new ConcurrencyControlConfigurer(); } + /** + * Controls the maximum number of sessions for a user. The default is to allow any + * number of users. + * + * @param sessionConcurrencyCustomizer the {@link Customizer} to provide more options for + * the {@link ConcurrencyControlConfigurer} + * @return the {@link SessionManagementConfigurer} for further customizations + */ + public SessionManagementConfigurer sessionConcurrency(Customizer sessionConcurrencyCustomizer) + throws Exception { + sessionConcurrencyCustomizer.customize(new ConcurrencyControlConfigurer()); + return this; + } + /** * Invokes {@link #postProcess(Object)} and sets the * {@link SessionAuthenticationStrategy} for session fixation. @@ -338,6 +366,18 @@ public SessionManagementConfigurer none() { */ public final class ConcurrencyControlConfigurer { + /** + * Controls the maximum number of sessions for a user. The default is to allow any + * number of users. + * + * @param maximumSessions the maximum number of sessions for a user + * @return the {@link ConcurrencyControlConfigurer} for further customizations + */ + public ConcurrencyControlConfigurer maximumSessions(int maximumSessions) { + SessionManagementConfigurer.this.maximumSessions = maximumSessions; + return this; + } + /** * The URL to redirect to if a user tries to access a resource and their session * has been expired due to too many sessions for the current user. The default is diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java index b7374228117..5eaa4d78a72 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTests.java @@ -202,6 +202,51 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } + @Test + public void authenticateWhenNewSessionFixationProtectionInLambdaThenCreatesNewSession() throws Exception { + this.spring.register(SFPNewSessionInLambdaConfig.class).autowire(); + + MockHttpSession givenSession = new MockHttpSession(); + String givenSessionId = givenSession.getId(); + givenSession.setAttribute("name", "value"); + + MockHttpSession resultingSession = (MockHttpSession) + this.mvc.perform(get("/auth") + .session(givenSession) + .with(httpBasic("user", "password"))) + .andExpect(status().isNotFound()) + .andReturn().getRequest().getSession(false); + + assertThat(givenSessionId).isNotEqualTo(resultingSession.getId()); + assertThat(resultingSession.getAttribute("name")).isNull(); + } + + @EnableWebSecurity + static class SFPNewSessionInLambdaConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .sessionManagement(sessionManagement -> + sessionManagement + .sessionFixation(sessionFixation -> + sessionFixation.newSession() + ) + ) + .httpBasic(withDefaults()); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication() + .withUser(PasswordEncodedUser.user()); + // @formatter:on + } + } + @Test public void loginWhenUserLoggedInAndMaxSessionsIsOneThenLoginPrevented() throws Exception { this.spring.register(ConcurrencyControlConfig.class).autowire(); @@ -289,8 +334,11 @@ public void configure(HttpSecurity http) throws Exception { .formLogin(withDefaults()) .sessionManagement(sessionManagement -> sessionManagement - .maximumSessions(1) - .maxSessionsPreventsLogin(true) + .sessionConcurrency(sessionConcurrency -> + sessionConcurrency + .maximumSessions(1) + .maxSessionsPreventsLogin(true) + ) ); // @formatter:on }