Browse Source

Add ContinueOnError Support For Failed Authentications

Closes gh-14521
ruabtmh 1 year ago
parent
commit
09010f3f51

+ 18 - 7
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 @@ package org.springframework.security.authentication;
 
 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;
 
@@ -28,7 +31,7 @@ import org.springframework.util.Assert;
 /**
  * A {@link ReactiveAuthenticationManager} that delegates to other
  * {@link ReactiveAuthenticationManager} instances using the result from the first non
- * empty result.
+ * empty result. Errors from delegates will be ignored if continueOnError is true.
  *
  * @author Rob Winch
  * @since 5.1
@@ -37,6 +40,10 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti
 
 	private final List<ReactiveAuthenticationManager> delegates;
 
+	private boolean continueOnError = false;
+
+	private final Log logger = LogFactory.getLog(getClass());
+
 	public DelegatingReactiveAuthenticationManager(ReactiveAuthenticationManager... entryPoints) {
 		this(Arrays.asList(entryPoints));
 	}
@@ -48,11 +55,15 @@ public class DelegatingReactiveAuthenticationManager implements ReactiveAuthenti
 
 	@Override
 	public Mono<Authentication> authenticate(Authentication authentication) {
-		// @formatter:off
-		return Flux.fromIterable(this.delegates)
-				.concatMap((m) -> m.authenticate(authentication))
-				.next();
-		// @formatter:on
+		Flux<ReactiveAuthenticationManager> result = Flux.fromIterable(this.delegates);
+		Function<ReactiveAuthenticationManager, Mono<Authentication>> logging = (m) -> m.authenticate(authentication)
+			.doOnError(this.logger::debug);
+
+		return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next();
+	}
+
+	public void setContinueOnError(boolean continueOnError) {
+		this.continueOnError = continueOnError;
 	}
 
 }

+ 40 - 1
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 class DelegatingReactiveAuthenticationManagerTests {
 			.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;
+	}
+
 }