Skip to content

Commit 2524a07

Browse files
committed
Propagate Previous Factor to Next One
This commit allows looking up the current authentication and applying it to the latest authentication. This is specifically handy when collecting authorities gained from each authentication factor.
1 parent d765639 commit 2524a07

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

core/src/main/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManager.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.springframework.security.core.Authentication;
2929
import org.springframework.security.core.AuthenticationException;
30+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3031
import org.springframework.util.Assert;
3132

3233
/**
@@ -57,11 +58,24 @@ public DelegatingReactiveAuthenticationManager(List<ReactiveAuthenticationManage
5758

5859
@Override
5960
public Mono<Authentication> authenticate(Authentication authentication) {
61+
return ReactiveSecurityContextHolder.getContext().flatMap((context) -> {
62+
Mono<Authentication> result = doAuthenticate(authentication);
63+
Authentication current = context.getAuthentication();
64+
if (current == null) {
65+
return result;
66+
}
67+
if (!current.isAuthenticated()) {
68+
return result;
69+
}
70+
return doAuthenticate(current).map((r) -> r.toBuilder().apply(current).build());
71+
}).switchIfEmpty(doAuthenticate(authentication));
72+
}
73+
74+
private Mono<Authentication> doAuthenticate(Authentication authentication) {
6075
Flux<ReactiveAuthenticationManager> result = Flux.fromIterable(this.delegates);
6176
Function<ReactiveAuthenticationManager, Mono<Authentication>> logging = (m) -> m.authenticate(authentication)
6277
.doOnError(AuthenticationException.class, (ex) -> ex.setAuthenticationRequest(authentication))
6378
.doOnError(this.logger::debug);
64-
6579
return ((this.continueOnError) ? result.concatMapDelayError(logging) : result.concatMap(logging)).next();
6680
}
6781

core/src/main/java/org/springframework/security/authentication/ProviderManager.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.springframework.security.core.AuthenticationException;
3434
import org.springframework.security.core.CredentialsContainer;
3535
import org.springframework.security.core.SpringSecurityMessageSource;
36+
import org.springframework.security.core.context.SecurityContextHolder;
37+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
3638
import org.springframework.util.Assert;
3739
import org.springframework.util.CollectionUtils;
3840

@@ -92,6 +94,9 @@ public class ProviderManager implements AuthenticationManager, MessageSourceAwar
9294

9395
private static final Log logger = LogFactory.getLog(ProviderManager.class);
9496

97+
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
98+
.getContextHolderStrategy();
99+
95100
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
96101

97102
private List<AuthenticationProvider> providers = Collections.emptyList();
@@ -209,6 +214,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
209214
lastException = ex;
210215
}
211216
}
217+
result = applyPreviousAuthentication(result);
212218
if (result == null && this.parent != null) {
213219
// Allow the parent to try.
214220
try {
@@ -265,6 +271,20 @@ public Authentication authenticate(Authentication authentication) throws Authent
265271
throw lastException;
266272
}
267273

274+
private @Nullable Authentication applyPreviousAuthentication(@Nullable Authentication result) {
275+
if (result == null) {
276+
return null;
277+
}
278+
Authentication current = this.securityContextHolderStrategy.getContext().getAuthentication();
279+
if (current == null) {
280+
return result;
281+
}
282+
if (!current.isAuthenticated()) {
283+
return result;
284+
}
285+
return result.toBuilder().apply(current).build();
286+
}
287+
268288
@SuppressWarnings("deprecation")
269289
private void prepareException(AuthenticationException ex, Authentication auth) {
270290
ex.setAuthenticationRequest(auth);
@@ -287,6 +307,11 @@ public List<AuthenticationProvider> getProviders() {
287307
return this.providers;
288308
}
289309

310+
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
311+
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
312+
this.securityContextHolderStrategy = securityContextHolderStrategy;
313+
}
314+
290315
@Override
291316
public void setMessageSource(MessageSource messageSource) {
292317
this.messages = new MessageSourceAccessor(messageSource);

core/src/test/java/org/springframework/security/authentication/DelegatingReactiveAuthenticationManagerTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@
2727

2828
import org.springframework.security.core.Authentication;
2929
import org.springframework.security.core.AuthenticationException;
30+
import org.springframework.security.core.GrantedAuthority;
31+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
3032

3133
import static org.assertj.core.api.Assertions.assertThat;
3234
import static org.mockito.ArgumentMatchers.any;
3335
import static org.mockito.BDDMockito.given;
36+
import static org.mockito.Mockito.mock;
3437

3538
/**
3639
* @author Rob Winch
@@ -118,6 +121,24 @@ void whenAccountStatusExceptionThenAuthenticationRequestIsIncluded() {
118121
assertThat(expected.getAuthenticationRequest()).isEqualTo(this.authentication);
119122
}
120123

124+
@Test
125+
void authenticateWhenPreviousAuthenticationThenApplies() {
126+
Authentication factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
127+
Authentication factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
128+
ReactiveAuthenticationManager provider = mock(ReactiveAuthenticationManager.class);
129+
given(provider.authenticate(any())).willReturn(Mono.just(factorTwo));
130+
ReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(provider);
131+
Authentication request = new TestingAuthenticationToken("user", "password");
132+
StepVerifier
133+
.create(manager.authenticate(request)
134+
.flatMapIterable(Authentication::getAuthorities)
135+
.map(GrantedAuthority::getAuthority)
136+
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(factorOne)))
137+
.expectNext("FACTOR_TWO")
138+
.expectNext("FACTOR_ONE")
139+
.verifyComplete();
140+
}
141+
121142
private DelegatingReactiveAuthenticationManager managerWithContinueOnError() {
122143
DelegatingReactiveAuthenticationManager manager = new DelegatingReactiveAuthenticationManager(this.delegate1,
123144
this.delegate2);

core/src/test/java/org/springframework/security/authentication/ProviderManagerTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@
1919
import java.util.ArrayList;
2020
import java.util.Arrays;
2121
import java.util.List;
22+
import java.util.Set;
2223

2324
import org.junit.jupiter.api.Test;
2425

2526
import org.springframework.context.MessageSource;
2627
import org.springframework.security.core.Authentication;
2728
import org.springframework.security.core.AuthenticationException;
29+
import org.springframework.security.core.authority.AuthorityUtils;
30+
import org.springframework.security.core.context.SecurityContextHolderStrategy;
31+
import org.springframework.security.core.context.SecurityContextImpl;
2832

2933
import static org.assertj.core.api.Assertions.assertThat;
3034
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -310,6 +314,22 @@ void authenticateWhenFailsInParentAndPublishesThenChildDoesNotPublish() {
310314
verifyNoMoreInteractions(publisher); // Child should not publish (duplicate event)
311315
}
312316

317+
@Test
318+
void authenticateWhenPreviousAuthenticationThenApplies() {
319+
Authentication factorOne = new TestingAuthenticationToken("user", "pass", "FACTOR_ONE");
320+
Authentication factorTwo = new TestingAuthenticationToken("user", "pass", "FACTOR_TWO");
321+
SecurityContextHolderStrategy securityContextHolderStrategy = mock(SecurityContextHolderStrategy.class);
322+
given(securityContextHolderStrategy.getContext()).willReturn(new SecurityContextImpl(factorOne));
323+
AuthenticationProvider provider = mock(AuthenticationProvider.class);
324+
given(provider.authenticate(any())).willReturn(factorTwo);
325+
given(provider.supports(any())).willReturn(true);
326+
ProviderManager manager = new ProviderManager(provider);
327+
manager.setSecurityContextHolderStrategy(securityContextHolderStrategy);
328+
Authentication request = new TestingAuthenticationToken("user", "password");
329+
Set<String> authorities = AuthorityUtils.authorityListToSet(manager.authenticate(request).getAuthorities());
330+
assertThat(authorities).containsExactlyInAnyOrder("FACTOR_ONE", "FACTOR_TWO");
331+
}
332+
313333
private AuthenticationProvider createProviderWhichThrows(final AuthenticationException ex) {
314334
AuthenticationProvider provider = mock(AuthenticationProvider.class);
315335
given(provider.supports(any(Class.class))).willReturn(true);

0 commit comments

Comments
 (0)