diff --git a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java index 7a06a0695b5..2fdc2d48c42 100644 --- a/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -18,7 +18,10 @@ import java.util.Arrays; import java.util.List; +import java.util.function.Function; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,8 +30,9 @@ /** * A {@link ReactiveAuthenticationManager} that delegates to other - * {@link ReactiveAuthenticationManager} instances using the result from the first non - * empty result. + * {@link ReactiveAuthenticationManager} instances. When {@code continueOnError} is + * {@code true}, will continue until the first non-empty, non-error result; otherwise, + * will continue only until the first non-empty result. * * @author Rob Winch * @since 5.1 @@ -37,6 +41,10 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti private final List delegates; + private boolean continueOnError = false; + + private final Log logger = LogFactory.getLog(getClass()); + public DelegatingReactiveAuthenticationManager(ReactiveAuthenticationManager... entryPoints) { this(Arrays.asList(entryPoints)); } @@ -48,11 +56,20 @@ public DelegatingReactiveAuthenticationManager(List authenticate(Authentication authentication) { - // @formatter:off - return Flux.fromIterable(this.delegates) - .concatMap((m) -> m.authenticate(authentication)) - .next(); - // @formatter:on + Flux result = Flux.fromIterable(this.delegates); + Function> logging = (m) -> m.authenticate(authentication) + .doOnError(this.logger::debug); + + return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next(); + } + + /** + * Continue iterating when a delegate errors, defaults to {@code false} + * @param continueOnError whether to continue when a delegate errors + * @since 6.3 + */ + public void setContinueOnError(boolean continueOnError) { + this.continueOnError = continueOnError; } } diff --git a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java index dd89bd7c89e..2d4b2c7a158 100644 --- a/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -77,4 +77,43 @@ public void authenticateWhenBadCredentialsThenDelegate2NotInvokedAndError() { .verify(); } + @Test + public void authenticateWhenContinueOnErrorAndFirstBadCredentialsThenTriesSecond() { + given(this.delegate1.authenticate(any())).willReturn(Mono.error(new BadCredentialsException("Test"))); + given(this.delegate2.authenticate(any())).willReturn(Mono.just(this.authentication)); + + DelegatingReactiveAuthenticationManager manager = managerWithContinueOnError(); + + assertThat(manager.authenticate(this.authentication).block()).isEqualTo(this.authentication); + } + + @Test + public void authenticateWhenContinueOnErrorAndBothDelegatesBadCredentialsThenError() { + given(this.delegate1.authenticate(any())).willReturn(Mono.error(new BadCredentialsException("Test"))); + given(this.delegate2.authenticate(any())).willReturn(Mono.error(new BadCredentialsException("Test"))); + + DelegatingReactiveAuthenticationManager manager = managerWithContinueOnError(); + + StepVerifier.create(manager.authenticate(this.authentication)) + .expectError(BadCredentialsException.class) + .verify(); + } + + @Test + public void authenticateWhenContinueOnErrorAndDelegate1NotEmptyThenReturnsNotEmpty() { + given(this.delegate1.authenticate(any())).willReturn(Mono.just(this.authentication)); + + DelegatingReactiveAuthenticationManager manager = managerWithContinueOnError(); + + assertThat(manager.authenticate(this.authentication).block()).isEqualTo(this.authentication); + } + + private DelegatingReactiveAuthenticationManager managerWithContinueOnError() { + DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1, + this.delegate2); + manager.setContinueOnError(true); + + return manager; + } + }