From 08821369a3df0b676a1d02083afac1dae7a7e2d8 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Thu, 5 Sep 2019 04:07:53 -0600 Subject: [PATCH] Add Request-based AuthenticationManagerResolvers Closes gh-6762 --- ...legatingAuthenticationManagerResolver.java | 146 +++++++++++++++++ ...ReactiveAuthenticationManagerResolver.java | 151 ++++++++++++++++++ ...ingAuthenticationManagerResolverTests.java | 68 ++++++++ ...iveAuthenticationManagerResolverTests.java | 69 ++++++++ 4 files changed, 434 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java create mode 100644 web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java create mode 100644 web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java diff --git a/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java b/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java new file mode 100644 index 0000000000..04833fdeae --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2022 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.authentication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationManagerResolver; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; +import org.springframework.util.Assert; + +/** + * An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager} + * instances based upon the type of {@link HttpServletRequest} passed into + * {@link #resolve(HttpServletRequest)}. + * + * @author Josh Cummings + * @since 5.7 + */ +public final class RequestMatcherDelegatingAuthenticationManagerResolver + implements AuthenticationManagerResolver { + + private final List> authenticationManagers; + + private AuthenticationManager defaultAuthenticationManager = (authentication) -> { + throw new AuthenticationServiceException("Cannot authenticate " + authentication); + }; + + /** + * Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on + * the provided parameters + * @param authenticationManagers a {@link Map} of + * {@link RequestMatcher}/{@link AuthenticationManager} pairs + */ + RequestMatcherDelegatingAuthenticationManagerResolver( + RequestMatcherEntry... authenticationManagers) { + Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty"); + this.authenticationManagers = Arrays.asList(authenticationManagers); + } + + /** + * Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on + * the provided parameters + * @param authenticationManagers a {@link Map} of + * {@link RequestMatcher}/{@link AuthenticationManager} pairs + */ + RequestMatcherDelegatingAuthenticationManagerResolver( + List> authenticationManagers) { + Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty"); + this.authenticationManagers = authenticationManagers; + } + + /** + * {@inheritDoc} + */ + @Override + public AuthenticationManager resolve(HttpServletRequest context) { + for (RequestMatcherEntry entry : this.authenticationManagers) { + if (entry.getRequestMatcher().matches(context)) { + return entry.getEntry(); + } + } + + return this.defaultAuthenticationManager; + } + + /** + * Set the default {@link AuthenticationManager} to use when a request does not match + * @param defaultAuthenticationManager the default {@link AuthenticationManager} to + * use + */ + public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) { + Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null"); + this.defaultAuthenticationManager = defaultAuthenticationManager; + } + + /** + * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}. + * @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder} + * instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}. + */ + public static final class Builder { + + private final List> entries = new ArrayList<>(); + + private Builder() { + + } + + /** + * Maps a {@link RequestMatcher} to an {@link AuthorizationManager}. + * @param matcher the {@link RequestMatcher} to use + * @param manager the {@link AuthenticationManager} to use + * @return the {@link Builder} for further + * customizationServerWebExchangeDelegatingReactiveAuthenticationManagerResolvers + */ + public Builder add(RequestMatcher matcher, AuthenticationManager manager) { + Assert.notNull(matcher, "matcher cannot be null"); + Assert.notNull(manager, "manager cannot be null"); + this.entries.add(new RequestMatcherEntry<>(matcher, manager)); + return this; + } + + /** + * Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance. + * @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance + */ + public RequestMatcherDelegatingAuthenticationManagerResolver build() { + return new RequestMatcherDelegatingAuthenticationManagerResolver(this.entries); + } + + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java b/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java new file mode 100644 index 0000000000..a28facf95b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2022 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; +import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; +import org.springframework.security.web.authentication.RequestMatcherDelegatingAuthenticationManagerResolver; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ReactiveAuthenticationManagerResolver} that returns a + * {@link ReactiveAuthenticationManager} instances based upon the type of + * {@link ServerWebExchange} passed into {@link #resolve(ServerWebExchange)}. + * + * @author Josh Cummings + * @since 5.7 + * + */ +public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver + implements ReactiveAuthenticationManagerResolver { + + private final List> authenticationManagers; + + private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono + .error(new AuthenticationServiceException("Cannot authenticate " + authentication)); + + /** + * Construct an + * {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on + * the provided parameters + * @param managers a set of {@link ServerWebExchangeMatcherEntry}s + */ + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver( + ServerWebExchangeMatcherEntry... managers) { + this(Arrays.asList(managers)); + } + + /** + * Construct an + * {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on + * the provided parameters + * @param managers a {@link List} of {@link ServerWebExchangeMatcherEntry}s + */ + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver( + List> managers) { + Assert.notNull(managers, "entries cannot be null"); + this.authenticationManagers = managers; + } + + /** + * {@inheritDoc} + */ + @Override + public Mono resolve(ServerWebExchange exchange) { + return Flux.fromIterable(this.authenticationManagers).filterWhen((entry) -> isMatch(exchange, entry)).next() + .map(ServerWebExchangeMatcherEntry::getEntry).defaultIfEmpty(this.defaultAuthenticationManager); + } + + /** + * Set the default {@link ReactiveAuthenticationManager} to use when a request does + * not match + * @param defaultAuthenticationManager the default + * {@link ReactiveAuthenticationManager} to use + */ + public void setDefaultAuthenticationManager(ReactiveAuthenticationManager defaultAuthenticationManager) { + Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null"); + this.defaultAuthenticationManager = defaultAuthenticationManager; + } + + /** + * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}. + * @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder} + * instance + */ + public static Builder builder() { + return new Builder(); + } + + private Mono isMatch(ServerWebExchange exchange, + ServerWebExchangeMatcherEntry entry) { + ServerWebExchangeMatcher matcher = entry.getMatcher(); + return matcher.matches(exchange).map(ServerWebExchangeMatcher.MatchResult::isMatch); + } + + /** + * A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}. + */ + public static final class Builder { + + private final List> entries = new ArrayList<>(); + + private Builder() { + + } + + /** + * Maps a {@link ServerWebExchangeMatcher} to an + * {@link ReactiveAuthenticationManager}. + * @param matcher the {@link ServerWebExchangeMatcher} to use + * @param manager the {@link ReactiveAuthenticationManager} to use + * @return the + * {@link RequestMatcherDelegatingAuthenticationManagerResolver.Builder} for + * further customizations + */ + public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder add( + ServerWebExchangeMatcher matcher, ReactiveAuthenticationManager manager) { + Assert.notNull(matcher, "matcher cannot be null"); + Assert.notNull(manager, "manager cannot be null"); + this.entries.add(new ServerWebExchangeMatcherEntry<>(matcher, manager)); + return this; + } + + /** + * Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance. + * @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver} + * instance + */ + public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver build() { + return new ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(this.entries); + } + + } + +} diff --git a/web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java b/web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java new file mode 100644 index 0000000000..df0f7258f4 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2022 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.authentication; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RequestMatcherDelegatingAuthenticationManagerResolverTests} + * + * @author Josh Cummings + */ +public class RequestMatcherDelegatingAuthenticationManagerResolverTests { + + private AuthenticationManager one = mock(AuthenticationManager.class); + + private AuthenticationManager two = mock(AuthenticationManager.class); + + @Test + public void resolveWhenMatchesThenReturnsAuthenticationManager() { + RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver + .builder().add(new AntPathRequestMatcher("/one/**"), this.one) + .add(new AntPathRequestMatcher("/two/**"), this.two).build(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/one/location"); + request.setServletPath("/one/location"); + assertThat(resolver.resolve(request)).isEqualTo(this.one); + } + + @Test + public void resolveWhenDoesNotMatchThenReturnsDefaultAuthenticationManager() { + RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver + .builder().add(new AntPathRequestMatcher("/one/**"), this.one) + .add(new AntPathRequestMatcher("/two/**"), this.two).build(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/wrong/location"); + AuthenticationManager authenticationManager = resolver.resolve(request); + + Authentication authentication = new TestingAuthenticationToken("principal", "creds"); + assertThatExceptionOfType(AuthenticationServiceException.class) + .isThrownBy(() -> authenticationManager.authenticate(authentication)); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java new file mode 100644 index 0000000000..f40b794ee2 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2022 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 org.junit.jupiter.api.Test; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} + * + * @author Josh Cummings + */ +public class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests { + + private ReactiveAuthenticationManager one = mock(ReactiveAuthenticationManager.class); + + private ReactiveAuthenticationManager two = mock(ReactiveAuthenticationManager.class); + + @Test + public void resolveWhenMatchesThenReturnsReactiveAuthenticationManager() { + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver resolver = ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver + .builder().add(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one) + .add(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two).build(); + + MockServerHttpRequest request = MockServerHttpRequest.get("/one/location").build(); + assertThat(resolver.resolve(MockServerWebExchange.from(request)).block()).isEqualTo(this.one); + } + + @Test + public void resolveWhenDoesNotMatchThenReturnsDefaultReactiveAuthenticationManager() { + ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver resolver = ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver + .builder().add(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one) + .add(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two).build(); + + MockServerHttpRequest request = MockServerHttpRequest.get("/wrong/location").build(); + ReactiveAuthenticationManager authenticationManager = resolver.resolve(MockServerWebExchange.from(request)) + .block(); + + Authentication authentication = new TestingAuthenticationToken("principal", "creds"); + assertThatExceptionOfType(AuthenticationServiceException.class) + .isThrownBy(() -> authenticationManager.authenticate(authentication).block()); + } + +}