From 99db0ca2c5f39de515077c857ac0507c3dc23c46 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Wed, 14 Apr 2021 13:39:41 -0400 Subject: [PATCH] WebFlux httpBasic() matches on XHR requests Closes gh-9660 --- .../config/web/server/ServerHttpSecurity.java | 28 +++++++++++++++++-- .../web/server/ServerHttpSecurityTests.java | 24 +++++++++++++++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 3780bba9be..dfc8b3c24e 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -33,6 +33,7 @@ import java.util.function.Supplier; import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; import org.springframework.security.oauth2.client.web.server.WebSessionOAuth2ServerAuthorizationRequestRepository; +import org.springframework.http.HttpStatus; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2AuthorizationException; import reactor.core.publisher.Mono; @@ -113,6 +114,7 @@ import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter; import org.springframework.security.web.server.authentication.AuthenticationWebFilter; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.authentication.ReactivePreAuthenticatedAuthenticationManager; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint; import org.springframework.security.web.server.authentication.RedirectServerAuthenticationFailureHandler; @@ -2965,11 +2967,17 @@ public class ServerHttpSecurity { * @see #httpBasic() */ public class HttpBasicSpec { + + private final ServerWebExchangeMatcher xhrMatcher = (exchange) -> Mono.just(exchange.getRequest().getHeaders()) + .filter((h) -> h.getOrEmpty("X-Requested-With").contains("XMLHttpRequest")) + .flatMap((h) -> ServerWebExchangeMatcher.MatchResult.match()) + .switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); + private ReactiveAuthenticationManager authenticationManager; private ServerSecurityContextRepository securityContextRepository; - private ServerAuthenticationEntryPoint entryPoint = new HttpBasicServerAuthenticationEntryPoint(); + private ServerAuthenticationEntryPoint entryPoint; /** * The {@link ReactiveAuthenticationManager} used to authenticate. Defaults to @@ -3034,7 +3042,13 @@ public class ServerHttpSecurity { MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML); restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); - ServerHttpSecurity.this.defaultEntryPoints.add(new DelegateEntry(restMatcher, this.entryPoint)); + ServerWebExchangeMatcher notHtmlMatcher = new NegatedServerWebExchangeMatcher( + new MediaTypeServerWebExchangeMatcher(MediaType.TEXT_HTML)); + ServerWebExchangeMatcher restNotHtmlMatcher = new AndServerWebExchangeMatcher( + Arrays.asList(notHtmlMatcher, restMatcher)); + ServerWebExchangeMatcher preferredMatcher = new OrServerWebExchangeMatcher( + Arrays.asList(this.xhrMatcher, restNotHtmlMatcher)); + ServerHttpSecurity.this.defaultEntryPoints.add(new DelegateEntry(preferredMatcher, this.entryPoint)); AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter( this.authenticationManager); authenticationFilter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(this.entryPoint)); @@ -3043,7 +3057,15 @@ public class ServerHttpSecurity { http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.HTTP_BASIC); } - private HttpBasicSpec() {} + private HttpBasicSpec() { + List entryPoints = new ArrayList<>(); + entryPoints + .add(new DelegateEntry(this.xhrMatcher, new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))); + DelegatingServerAuthenticationEntryPoint defaultEntryPoint = new DelegatingServerAuthenticationEntryPoint( + entryPoints); + defaultEntryPoint.setDefaultEntryPoint(new HttpBasicServerAuthenticationEntryPoint()); + this.entryPoint = defaultEntryPoint; + } } /** diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java index 052b6629a4..cb7efee35b 100644 --- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java +++ b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -41,6 +41,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.security.oauth2.client.web.server.ServerAuthorizationRequestRepository; @@ -48,6 +49,8 @@ import org.springframework.security.oauth2.client.web.server.authentication.OAut import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter; import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; @@ -184,6 +187,25 @@ public class ServerHttpSecurityTests { .expectBody().isEmpty(); } + @Test + public void basicWhenXHRRequestThenUnauthorized() { + ServerAuthenticationEntryPoint authenticationEntryPoint = spy( + new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)); + this.http.httpBasic().authenticationEntryPoint(authenticationEntryPoint); + this.http.authorizeExchange().anyExchange().authenticated(); + WebTestClient client = buildClient(); + // @formatter:off + client.get().uri("/") + .header("X-Requested-With", "XMLHttpRequest") + .exchange() + .expectStatus().isUnauthorized() + .expectHeader().doesNotExist("WWW-Authenticate") + .expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+") + .expectBody().isEmpty(); + // @formatter:on + verify(authenticationEntryPoint).commence(any(), any()); + } + @Test public void buildWhenServerWebExchangeFromContextThenFound() { SecurityWebFilterChain filter = this.http.build();