diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SsoSecurityConfigurer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SsoSecurityConfigurer.java index 3681b4aab2..90319c88e0 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SsoSecurityConfigurer.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/SsoSecurityConfigurer.java @@ -16,16 +16,26 @@ package org.springframework.boot.autoconfigure.security.oauth2.client; +import java.util.Collections; + import org.springframework.beans.factory.BeanFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExceptionHandlingConfigurer; import org.springframework.security.oauth2.client.OAuth2RestOperations; import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter; import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; class SsoSecurityConfigurer { @@ -40,8 +50,28 @@ class SsoSecurityConfigurer { // Delay the processing of the filter until we know the // SessionAuthenticationStrategy is available: http.apply(new OAuth2ClientAuthenticationConfigurer(oauth2SsoFilter(sso))); - http.exceptionHandling().authenticationEntryPoint( - new LoginUrlAuthenticationEntryPoint(sso.getLoginPath())); + addAuthenticationEntryPoint(http, sso); + } + + private void addAuthenticationEntryPoint(HttpSecurity http, OAuth2SsoProperties sso) + throws Exception { + ExceptionHandlingConfigurer exceptions = http.exceptionHandling(); + ContentNegotiationStrategy contentNegotiationStrategy = http + .getSharedObject(ContentNegotiationStrategy.class); + if (contentNegotiationStrategy == null) { + contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + } + MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher( + contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, + new MediaType("image", "*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN); + preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + exceptions.defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint(sso.getLoginPath()), + preferredMatcher); + // When multiple entry points are provided the default is the first one + exceptions.defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); } private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter( diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/BasicOAuth2SsoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/BasicOAuth2SsoConfigurationTests.java index 7efdecbc17..151804fbcf 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/BasicOAuth2SsoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/BasicOAuth2SsoConfigurationTests.java @@ -77,6 +77,12 @@ public class BasicOAuth2SsoConfigurationTests { .andExpect(header().string("location", "http://localhost/login")); } + @Test + public void homePageSends401ToXhr() throws Exception { + this.mvc.perform(get("/").header("X-Requested-With", "XMLHttpRequest")) + .andExpect(status().isUnauthorized()); + } + @Configuration @Import(OAuth2AutoConfiguration.class) @EnableOAuth2Sso diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java index 8b71e4a485..899b6b457a 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoConfigurationTests.java @@ -89,6 +89,12 @@ public class CustomOAuth2SsoConfigurationTests { .andExpect(header().string("location", "http://localhost/login")); } + @Test + public void uiPageSends401ToXhr() throws Exception { + this.mvc.perform(get("/ui/").header("X-Requested-With", "XMLHttpRequest")) + .andExpect(status().isUnauthorized()); + } + @Test public void uiTestPageIsAccessible() throws Exception { this.mvc.perform(get("/ui/test")).andExpect(status().isOk()) diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java new file mode 100644 index 0000000000..541b01db2f --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/sso/CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.sso; + +import javax.servlet.Filter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; +import org.springframework.boot.autoconfigure.security.oauth2.sso.CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests.TestConfiguration; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +import static org.hamcrest.Matchers.startsWith; +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.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link OAuth2AutoConfiguration} with custom configuration. + * + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(TestConfiguration.class) +@WebAppConfiguration +@TestPropertySource(properties = { "security.oauth2.client.clientId=client", + "security.oauth2.client.clientSecret=secret", + "security.oauth2.client.authorizationUri=http://example.com/oauth/authorize", + "security.oauth2.client.tokenUri=http://example.com/oauth/token", + "security.oauth2.resource.jwt.keyValue=SSSSHHH" }) +public class CustomOAuth2SsoWithAuthenticationEntryPointConfigurationTests { + + @Autowired + private WebApplicationContext context; + + @Autowired + @Qualifier("springSecurityFilterChain") + private Filter filter; + + private MockMvc mvc; + + @Before + public void init() { + this.mvc = MockMvcBuilders.webAppContextSetup(this.context) + .addFilters(this.filter).build(); + } + + @Test + public void homePageIsBasicAuth() throws Exception { + this.mvc.perform(get("/")).andExpect(status().isUnauthorized()) + .andExpect(header().string("WWW-Authenticate", startsWith("Basic"))); + } + + @Test + public void uiPageIsSecure() throws Exception { + this.mvc.perform(get("/ui/")).andExpect(status().isUnauthorized()); + } + + @Test + public void uiTestPageIsAccessible() throws Exception { + this.mvc.perform(get("/ui/test")).andExpect(status().isOk()) + .andExpect(content().string("test")); + } + + @Configuration + @EnableOAuth2Sso + @Import(OAuth2AutoConfiguration.class) + @MinimalSecureWebConfiguration + protected static class TestConfiguration extends WebSecurityConfigurerAdapter { + + @Override + public void configure(HttpSecurity http) throws Exception { + http.antMatcher("/ui/**").authorizeRequests().antMatchers("/ui/test") + .permitAll().anyRequest().authenticated().and().exceptionHandling() + .authenticationEntryPoint( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); + } + + @RestController + public static class TestController { + + @RequestMapping("/ui/test") + public String test() { + return "test"; + } + + } + + } + +}