From 5afeaa3ce7946471d231b460e6ff5b2d1f3b92a6 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 | 24 +++++++++++++++++-- .../web/server/ServerHttpSecurityTests.java | 24 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 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 0880cfc343..7c8cc759e2 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 @@ -41,6 +41,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.converter.Converter; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager; @@ -111,6 +112,7 @@ import org.springframework.security.web.server.authentication.AnonymousAuthentic import org.springframework.security.web.server.authentication.AuthenticationConverterServerWebExchangeMatcher; 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; @@ -1910,13 +1912,25 @@ public class ServerHttpSecurity { */ public final 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; 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; } /** @@ -1981,7 +1995,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)); 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 de1f4ec014..1ecdf934ac 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. @@ -32,6 +32,7 @@ import org.mockito.junit.MockitoJUnitRunner; import reactor.core.publisher.Mono; import reactor.test.publisher.TestPublisher; +import org.springframework.http.HttpStatus; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.config.annotation.web.reactive.ServerHttpSecurityConfigurationBuilder; @@ -45,9 +46,11 @@ import org.springframework.security.oauth2.core.endpoint.TestOAuth2Authorization import org.springframework.security.test.web.reactive.server.WebTestClientBuilder; import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; +import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.authentication.ServerX509AuthenticationConverter; import org.springframework.security.web.server.authentication.logout.DelegatingServerLogoutHandler; import org.springframework.security.web.server.authentication.logout.LogoutWebFilter; @@ -184,6 +187,25 @@ public class ServerHttpSecurityTests { // @formatter:on } + @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();