From c30b506cc129d7b29b5b2b8a091b258737fdef7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Kov=C3=A1=C4=8D?= Date: Mon, 20 Jul 2020 20:06:12 +0200 Subject: [PATCH] Introduce AuthenticationConverterServerWebExchangeMatcher AuthenticationConverterServerWebExchangeMatcher is ServerWebExchangeMatcher implementation based on AuthenticationConverter which matches if ServerWebExchange can be converted to Authentication. It can be used as a matcher where SecurityFilterChain should be matched based on used authentication method. BearerTokenServerWebExchangeMatcher was replaced by this matcher. Closes gh-8824 --- .../config/web/server/ServerHttpSecurity.java | 37 ++----- ...tionConverterServerWebExchangeMatcher.java | 51 ++++++++++ ...onverterServerWebExchangeMatcherTests.java | 96 +++++++++++++++++++ 3 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcher.java create mode 100644 web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcherTests.java 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 91866bff42f..8334fce3eb5 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 @@ -108,6 +108,7 @@ import org.springframework.security.web.server.ServerAuthenticationEntryPoint; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter; +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.ReactivePreAuthenticatedAuthenticationManager; @@ -179,8 +180,6 @@ import org.springframework.web.server.WebFilterChain; import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry; -import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match; -import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch; /** * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux. @@ -1629,8 +1628,7 @@ public class OAuth2ResourceServerSpec { private ServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint(); private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler(); private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter(); - private BearerTokenServerWebExchangeMatcher bearerTokenServerWebExchangeMatcher = - new BearerTokenServerWebExchangeMatcher(); + private AuthenticationConverterServerWebExchangeMatcher authenticationConverterServerWebExchangeMatcher; private JwtSpec jwt; private OpaqueTokenSpec opaqueToken; @@ -1748,8 +1746,8 @@ public OAuth2ResourceServerSpec opaqueToken(Customizer opaqueTo } protected void configure(ServerHttpSecurity http) { - this.bearerTokenServerWebExchangeMatcher - .setBearerTokenConverter(this.bearerTokenConverter); + this.authenticationConverterServerWebExchangeMatcher = + new AuthenticationConverterServerWebExchangeMatcher(this.bearerTokenConverter); registerDefaultAccessDeniedHandler(http); registerDefaultAuthenticationEntryPoint(http); @@ -1794,7 +1792,7 @@ private void registerDefaultAccessDeniedHandler(ServerHttpSecurity http) { if ( http.exceptionHandling != null ) { http.defaultAccessDeniedHandlers.add( new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry( - this.bearerTokenServerWebExchangeMatcher, + this.authenticationConverterServerWebExchangeMatcher, OAuth2ResourceServerSpec.this.accessDeniedHandler ) ); @@ -1805,7 +1803,7 @@ private void registerDefaultAuthenticationEntryPoint(ServerHttpSecurity http) { if (http.exceptionHandling != null) { http.defaultEntryPoints.add( new DelegateEntry( - this.bearerTokenServerWebExchangeMatcher, + this.authenticationConverterServerWebExchangeMatcher, OAuth2ResourceServerSpec.this.entryPoint ) ); @@ -1820,27 +1818,7 @@ private void registerDefaultCsrfOverride(ServerHttpSecurity http) { new AndServerWebExchangeMatcher( CsrfWebFilter.DEFAULT_CSRF_MATCHER, new NegatedServerWebExchangeMatcher( - this.bearerTokenServerWebExchangeMatcher))); - } - } - - private class BearerTokenServerWebExchangeMatcher implements ServerWebExchangeMatcher { - ServerAuthenticationConverter bearerTokenConverter; - - @Override - public Mono matches(ServerWebExchange exchange) { - return this.bearerTokenConverter.convert(exchange) - .flatMap(this::nullAuthentication) - .onErrorResume(e -> notMatch()); - } - - public void setBearerTokenConverter(ServerAuthenticationConverter bearerTokenConverter) { - Assert.notNull(bearerTokenConverter, "bearerTokenConverter cannot be null"); - this.bearerTokenConverter = bearerTokenConverter; - } - - private Mono nullAuthentication(Authentication authentication) { - return authentication == null ? notMatch() : match(); + this.authenticationConverterServerWebExchangeMatcher))); } } @@ -4034,4 +4012,5 @@ private String getKey() { private AnonymousSpec() {} } + } diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcher.java new file mode 100644 index 00000000000..05a164d523a --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcher.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.security.web.server.authentication; + +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match; +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +/** + * Matches if the {@link ServerAuthenticationConverter} can convert a {@link ServerWebExchange} to an {@link Authentication}. + * + * @author David Kovac + * @since 5.4 + * @see ServerAuthenticationConverter + */ +public final class AuthenticationConverterServerWebExchangeMatcher implements ServerWebExchangeMatcher { + private final ServerAuthenticationConverter serverAuthenticationConverter; + + public AuthenticationConverterServerWebExchangeMatcher(ServerAuthenticationConverter serverAuthenticationConverter) { + Assert.notNull(serverAuthenticationConverter, "serverAuthenticationConverter cannot be null"); + this.serverAuthenticationConverter = serverAuthenticationConverter; + } + + @Override + public Mono matches(ServerWebExchange exchange) { + return this.serverAuthenticationConverter.convert(exchange) + .flatMap(a -> match()) + .onErrorResume(e -> notMatch()) + .switchIfEmpty(notMatch()); + } +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcherTests.java new file mode 100644 index 00000000000..53646a3251c --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/AuthenticationConverterServerWebExchangeMatcherTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2020 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 + * + * https://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.security.web.server.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.core.Authentication; + +import reactor.core.publisher.Mono; + +/** + * @author David Kovac + * @since 5.4 + */ +@RunWith(MockitoJUnitRunner.class) +public class AuthenticationConverterServerWebExchangeMatcherTests { + private MockServerWebExchange exchange; + private AuthenticationConverterServerWebExchangeMatcher matcher; + @Mock + private ServerAuthenticationConverter converter; + @Mock + private Authentication authentication; + + @Before + public void setup() { + MockServerHttpRequest request = MockServerHttpRequest.get("/path").build(); + exchange = MockServerWebExchange.from(request); + matcher = new AuthenticationConverterServerWebExchangeMatcher(converter); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorConverterWhenConverterNullThenThrowsException() { + new AuthenticationConverterServerWebExchangeMatcher(null); + } + + @Test + public void matchesWhenNotEmptyThenReturnTrue() { + when(converter.convert(any())).thenReturn(Mono.just(authentication)); + + assertThat(matcher.matches(exchange).block().isMatch()).isTrue(); + } + + @Test + public void matchesWhenEmptyThenReturnFalse() { + when(converter.convert(any())).thenReturn(Mono.empty()); + + assertThat(matcher.matches(exchange).block().isMatch()).isFalse(); + } + + @Test + public void matchesWhenErrorThenReturnFalse() { + when(converter.convert(any())).thenReturn(Mono.error(new RuntimeException())); + + assertThat(matcher.matches(exchange).block().isMatch()).isFalse(); + } + + @Test + public void matchesWhenNullThenThrowsException() { + when(this.converter.convert(any())).thenReturn(null); + + assertThatCode(() -> matcher.matches(exchange).block()) + .isInstanceOf(NullPointerException.class); + } + + @Test + public void matchesWhenExceptionThenPropagates() { + when(this.converter.convert(any())).thenThrow(RuntimeException.class); + + assertThatCode(() -> matcher.matches(exchange).block()) + .isInstanceOf(RuntimeException.class); + } +}