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 d56ca5bb4c..c27a779ea3 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 @@ -16,12 +16,15 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.server.resource; +import java.util.Arrays; +import java.util.Collections; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; import org.springframework.context.ApplicationContext; import org.springframework.core.convert.converter.Converter; +import org.springframework.http.MediaType; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; @@ -48,8 +51,15 @@ import org.springframework.security.oauth2.server.resource.web.DefaultBearerToke import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.NegatedRequestMatcher; +import org.springframework.security.web.util.matcher.OrRequestMatcher; +import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; /** * @@ -130,6 +140,9 @@ import org.springframework.util.Assert; public final class OAuth2ResourceServerConfigurer> extends AbstractHttpConfigurer, H> { + private static final RequestHeaderRequestMatcher X_REQUESTED_WITH = new RequestHeaderRequestMatcher( + "X-Requested-With", "XMLHttpRequest"); + private final ApplicationContext context; private AuthenticationManagerResolver authenticationManagerResolver; @@ -273,7 +286,25 @@ public final class OAuth2ResourceServerConfigurer exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling != null) { - exceptionHandling.defaultAuthenticationEntryPointFor(this.authenticationEntryPoint, this.requestMatcher); + ContentNegotiationStrategy contentNegotiationStrategy = http + .getSharedObject(ContentNegotiationStrategy.class); + if (contentNegotiationStrategy == null) { + contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + } + MediaTypeRequestMatcher restMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, + MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, + MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, + MediaType.TEXT_XML); + restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + MediaTypeRequestMatcher allMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.ALL); + allMatcher.setUseEquals(true); + RequestMatcher notHtmlMatcher = new NegatedRequestMatcher( + new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.TEXT_HTML)); + RequestMatcher restNotHtmlMatcher = new AndRequestMatcher( + Arrays.asList(notHtmlMatcher, restMatcher)); + RequestMatcher preferredMatcher = new OrRequestMatcher( + Arrays.asList(this.requestMatcher, X_REQUESTED_WITH, restNotHtmlMatcher, allMatcher)); + exceptionHandling.defaultAuthenticationEntryPointFor(this.authenticationEntryPoint, preferredMatcher); } } 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 05fad9f470..c36a2712b6 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 @@ -89,6 +89,10 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrincipal; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -1108,7 +1112,8 @@ public class OAuth2ResourceServerConfigurerTests { JwtDecoder decoder = this.spring.getContext().getBean(JwtDecoder.class); given(decoder.decode(anyString())).willThrow(JwtException.class); // @formatter:off - MvcResult result = this.mvc.perform(get("/authenticated")) + MvcResult result = this.mvc.perform(get("/authenticated") + .header("Accept", "text/html")) .andExpect(status().isFound()) .andExpect(redirectedUrl("http://localhost/login")) .andReturn(); @@ -1122,6 +1127,15 @@ public class OAuth2ResourceServerConfigurerTests { assertThat(result.getRequest().getSession(false)).isNull(); } + @Test + public void unauthenticatedRequestWhenFormOAuth2LoginAndResourceServerThenNegotiates() throws Exception { + this.spring.register(OAuth2LoginAndResourceServerConfig.class, JwtDecoderConfig.class).autowire(); + this.mvc.perform(get("/any").header("X-Requested-With", "XMLHttpRequest")).andExpect(status().isUnauthorized()); + this.mvc.perform(get("/any").header("Accept", "application/json")).andExpect(status().isUnauthorized()); + this.mvc.perform(get("/any").header("Accept", "text/html")).andExpect(status().is3xxRedirection()); + this.mvc.perform(get("/any").header("Accept", "image/jpg")).andExpect(status().is3xxRedirection()); + } + @Test public void requestWhenDefaultAndResourceServerAccessDeniedHandlersThenMatchedByRequest() throws Exception { this.spring @@ -1721,6 +1735,31 @@ public class OAuth2ResourceServerConfigurerTests { } + @EnableWebSecurity + static class OAuth2LoginAndResourceServerConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests((authz) -> authz + .anyRequest().authenticated() + ) + .oauth2Login(withDefaults()) + .oauth2ResourceServer((oauth2) -> oauth2 + .jwt() + ); + // @formatter:on + } + + @Bean + ClientRegistrationRepository clients() { + ClientRegistration registration = TestClientRegistrations.clientRegistration().build(); + return new InMemoryClientRegistrationRepository(registration); + } + + } + @EnableWebSecurity static class JwtHalfConfiguredConfig extends WebSecurityConfigurerAdapter {