From d190b7e6a9d6e2127edd724137215421d2103852 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 11 Sep 2017 22:52:46 -0500 Subject: [PATCH] Add DelegatingAuthenticationEntryPoint Fixes gh-4535 --- .../DelegatingAuthenticationEntryPoint.java | 94 +++++++++++++++++++ ...legatingAuthenticationEntryPointTests.java | 92 ++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 webflux/src/main/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPoint.java create mode 100644 webflux/src/test/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPointTests.java diff --git a/webflux/src/main/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPoint.java b/webflux/src/main/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPoint.java new file mode 100644 index 0000000000..7ba910b580 --- /dev/null +++ b/webflux/src/main/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPoint.java @@ -0,0 +1,94 @@ +/* + * + * * Copyright 2002-2017 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.security.web.server; + +import org.springframework.http.HttpStatus; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.web.server.ServerWebExchange; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class DelegatingAuthenticationEntryPoint implements AuthenticationEntryPoint { + private final Flux entryPoints; + + private AuthenticationEntryPoint defaultEntryPoint = (exchange, e) -> { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return exchange.getResponse().setComplete(); + }; + + public DelegatingAuthenticationEntryPoint( + DelegateEntry... entryPoints) { + this(Arrays.asList(entryPoints)); + } + public DelegatingAuthenticationEntryPoint( + List entryPoints) { + this.entryPoints = Flux.fromIterable(entryPoints); + } + + public Mono commence(ServerWebExchange exchange, + AuthenticationException e) { + return this.entryPoints.filterWhen( entry -> isMatch(exchange, entry)) + .next() + .map( entry -> entry.getEntryPoint()) + .defaultIfEmpty(this.defaultEntryPoint) + .flatMap( entryPoint -> entryPoint.commence(exchange, e)); + } + + private Mono isMatch(ServerWebExchange exchange, DelegateEntry entry) { + ServerWebExchangeMatcher matcher = entry.getMatcher(); + return matcher.matches(exchange) + .map( result -> result.isMatch()); + } + + /** + * EntryPoint which is used when no RequestMatcher returned true + */ + public void setDefaultEntryPoint( + AuthenticationEntryPoint defaultEntryPoint) { + this.defaultEntryPoint = defaultEntryPoint; + } + + public static class DelegateEntry { + private final ServerWebExchangeMatcher matcher; + private final AuthenticationEntryPoint entryPoint; + + public DelegateEntry(ServerWebExchangeMatcher matcher, + AuthenticationEntryPoint entryPoint) { + this.matcher = matcher; + this.entryPoint = entryPoint; + } + + public ServerWebExchangeMatcher getMatcher() { + return this.matcher; + } + + public AuthenticationEntryPoint getEntryPoint() { + return this.entryPoint; + } + } +} diff --git a/webflux/src/test/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPointTests.java b/webflux/src/test/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPointTests.java new file mode 100644 index 0000000000..ad9b9a5d31 --- /dev/null +++ b/webflux/src/test/java/org/springframework/security/web/server/DelegatingAuthenticationEntryPointTests.java @@ -0,0 +1,92 @@ +/* + * + * * Copyright 2002-2017 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.security.web.server; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.http.HttpStatus; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.springframework.security.web.server.DelegatingAuthenticationEntryPoint.*; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class DelegatingAuthenticationEntryPointTests { + private ServerWebExchange exchange = MockServerHttpRequest.get("/").toExchange(); + + @Mock + private ServerWebExchangeMatcher matcher1; + @Mock + private ServerWebExchangeMatcher matcher2; + @Mock + private AuthenticationEntryPoint delegate1; + @Mock + private AuthenticationEntryPoint delegate2; + + private AuthenticationException e = new AuthenticationCredentialsNotFoundException("Log In"); + private DelegatingAuthenticationEntryPoint entryPoint; + + @Test + public void commenceWhenNotMatchThenMatchThenOnlySecondDelegateInvoked() { + Mono expectedResult = Mono.empty(); + when(this.matcher1.matches(this.exchange)).thenReturn( + ServerWebExchangeMatcher.MatchResult.notMatch()); + when(this.matcher2.matches(this.exchange)).thenReturn( + ServerWebExchangeMatcher.MatchResult.match()); + when(this.delegate2.commence(this.exchange, this.e)).thenReturn(expectedResult); + this.entryPoint = new DelegatingAuthenticationEntryPoint( + new DelegateEntry(this.matcher1, this.delegate1), + new DelegateEntry(this.matcher2, this.delegate2)); + + Mono actualResult = this.entryPoint.commence(this.exchange, this.e); + actualResult.block(); + + assertThat(actualResult).isEqualTo(actualResult); + verifyZeroInteractions(this.delegate1); + verify(this.delegate2).commence(this.exchange, this.e); + } + + @Test + public void commenceWhenNotMatchThenDefault() { + when(this.matcher1.matches(this.exchange)).thenReturn( + ServerWebExchangeMatcher.MatchResult.notMatch()); + this.entryPoint = new DelegatingAuthenticationEntryPoint( + new DelegateEntry(this.matcher1, this.delegate1)); + + this.entryPoint.commence(this.exchange, this.e).block(); + + assertThat(this.exchange.getResponse().getStatusCode()).isEqualTo( + HttpStatus.UNAUTHORIZED); + verifyZeroInteractions(this.delegate1, this.delegate2); + } +}