Skip to content

Commit 8e6e975

Browse files
eddumelendezeleftherias
authored andcommitted
Prevent authentication when user is inactive for reactive apps
Currently, reactive applications doesn't perform validation when user is locked, disabled or expired. This commit introduces these validations. Fixes gh-7113
1 parent 4ca9e15 commit 8e6e975

File tree

3 files changed

+235
-82
lines changed

3 files changed

+235
-82
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authentication;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
import reactor.core.publisher.Mono;
22+
import reactor.core.scheduler.Scheduler;
23+
import reactor.core.scheduler.Schedulers;
24+
25+
import org.springframework.context.support.MessageSourceAccessor;
26+
import org.springframework.security.core.Authentication;
27+
import org.springframework.security.core.SpringSecurityMessageSource;
28+
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
29+
import org.springframework.security.core.userdetails.UserDetails;
30+
import org.springframework.security.core.userdetails.UserDetailsChecker;
31+
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
32+
import org.springframework.security.crypto.password.PasswordEncoder;
33+
import org.springframework.util.Assert;
34+
35+
/**
36+
* A base {@link ReactiveAuthenticationManager} that allows subclasses to override and work with
37+
* {@link UserDetails} objects.
38+
*
39+
* <p>
40+
* Upon successful validation, a <code>UsernamePasswordAuthenticationToken</code> will be
41+
* created and returned to the caller. The token will include as its principal either a
42+
* <code>String</code> representation of the username, or the {@link UserDetails} that was
43+
* returned from the authentication repository.
44+
*
45+
* @author Eddú Meléndez
46+
* @since 5.2
47+
*/
48+
public abstract class AbstractUserDetailsReactiveAuthenticationManager implements ReactiveAuthenticationManager {
49+
50+
protected final Log logger = LogFactory.getLog(getClass());
51+
52+
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
53+
54+
private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
55+
56+
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
57+
58+
private Scheduler scheduler = Schedulers.parallel();
59+
60+
private UserDetailsChecker preAuthenticationChecks = user -> {
61+
if (!user.isAccountNonLocked()) {
62+
logger.debug("User account is locked");
63+
64+
throw new LockedException(this.messages.getMessage(
65+
"AbstractUserDetailsAuthenticationProvider.locked",
66+
"User account is locked"));
67+
}
68+
69+
if (!user.isEnabled()) {
70+
logger.debug("User account is disabled");
71+
72+
throw new DisabledException(this.messages.getMessage(
73+
"AbstractUserDetailsAuthenticationProvider.disabled",
74+
"User is disabled"));
75+
}
76+
77+
if (!user.isAccountNonExpired()) {
78+
logger.debug("User account is expired");
79+
80+
throw new AccountExpiredException(this.messages.getMessage(
81+
"AbstractUserDetailsAuthenticationProvider.expired",
82+
"User account has expired"));
83+
}
84+
};
85+
86+
private UserDetailsChecker postAuthenticationChecks = user -> {
87+
if (!user.isCredentialsNonExpired()) {
88+
logger.debug("User account credentials have expired");
89+
90+
throw new CredentialsExpiredException(this.messages.getMessage(
91+
"AbstractUserDetailsAuthenticationProvider.credentialsExpired",
92+
"User credentials have expired"));
93+
}
94+
};
95+
96+
@Override
97+
public Mono<Authentication> authenticate(Authentication authentication) {
98+
final String username = authentication.getName();
99+
final String presentedPassword = (String) authentication.getCredentials();
100+
return retrieveUser(username)
101+
.doOnNext(this.preAuthenticationChecks::check)
102+
.publishOn(this.scheduler)
103+
.filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword()))
104+
.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
105+
.flatMap(u -> {
106+
boolean upgradeEncoding = this.userDetailsPasswordService != null
107+
&& this.passwordEncoder.upgradeEncoding(u.getPassword());
108+
if (upgradeEncoding) {
109+
String newPassword = this.passwordEncoder.encode(presentedPassword);
110+
return this.userDetailsPasswordService.updatePassword(u, newPassword);
111+
}
112+
return Mono.just(u);
113+
})
114+
.doOnNext(this.postAuthenticationChecks::check)
115+
.map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) );
116+
}
117+
118+
/**
119+
* The {@link PasswordEncoder} that is used for validating the password. The default is
120+
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
121+
* @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null
122+
*/
123+
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
124+
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
125+
this.passwordEncoder = passwordEncoder;
126+
}
127+
128+
/**
129+
* Sets the {@link Scheduler} used by the {@link UserDetailsRepositoryReactiveAuthenticationManager}.
130+
* The default is {@code Schedulers.parallel()} because modern password encoding is
131+
* a CPU intensive task that is non blocking. This means validation is bounded by the
132+
* number of CPUs. Some applications may want to customize the {@link Scheduler}. For
133+
* example, if users are stuck using the insecure {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}
134+
* they might want to leverage {@code Schedulers.immediate()}.
135+
*
136+
* @param scheduler the {@link Scheduler} to use. Cannot be null.
137+
* @since 5.0.6
138+
*/
139+
public void setScheduler(Scheduler scheduler) {
140+
Assert.notNull(scheduler, "scheduler cannot be null");
141+
this.scheduler = scheduler;
142+
}
143+
144+
/**
145+
* Sets the service to use for upgrading passwords on successful authentication.
146+
* @param userDetailsPasswordService the service to use
147+
*/
148+
public void setUserDetailsPasswordService(
149+
ReactiveUserDetailsPasswordService userDetailsPasswordService) {
150+
this.userDetailsPasswordService = userDetailsPasswordService;
151+
}
152+
153+
/**
154+
* Sets the strategy which will be used to validate the loaded <tt>UserDetails</tt>
155+
* object after authentication occurs.
156+
*
157+
* @param postAuthenticationChecks The {@link UserDetailsChecker}
158+
* @since 5.2
159+
*/
160+
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
161+
Assert.notNull(this.postAuthenticationChecks, "postAuthenticationChecks cannot be null");
162+
this.postAuthenticationChecks = postAuthenticationChecks;
163+
}
164+
165+
/**
166+
* Allows subclasses to retrieve the <code>UserDetails</code>
167+
* from an implementation-specific location.
168+
*
169+
* @param username The username to retrieve
170+
* @return the user information. If authentication fails, a Mono error is returned.
171+
*/
172+
protected abstract Mono<UserDetails> retrieveUser(String username);
173+
174+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,105 +17,31 @@
1717
package org.springframework.security.authentication;
1818

1919
import reactor.core.publisher.Mono;
20-
import reactor.core.scheduler.Scheduler;
21-
import reactor.core.scheduler.Schedulers;
2220

23-
import org.springframework.security.core.Authentication;
24-
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
2521
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
26-
import org.springframework.security.core.userdetails.UserDetailsChecker;
27-
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
28-
import org.springframework.security.crypto.password.PasswordEncoder;
22+
import org.springframework.security.core.userdetails.UserDetails;
2923
import org.springframework.util.Assert;
3024

3125
/**
3226
* A {@link ReactiveAuthenticationManager} that uses a {@link ReactiveUserDetailsService} to validate the provided
3327
* username and password.
3428
*
3529
* @author Rob Winch
30+
* @author Eddú Meléndez
3631
* @since 5.0
3732
*/
38-
public class UserDetailsRepositoryReactiveAuthenticationManager implements ReactiveAuthenticationManager {
39-
private final ReactiveUserDetailsService userDetailsService;
33+
public class UserDetailsRepositoryReactiveAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager {
4034

41-
private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
42-
43-
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
44-
45-
private Scheduler scheduler = Schedulers.parallel();
46-
47-
private UserDetailsChecker postAuthenticationChecks = userDetails -> {};
35+
private ReactiveUserDetailsService userDetailsService;
4836

4937
public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) {
5038
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
5139
this.userDetailsService = userDetailsService;
5240
}
5341

5442
@Override
55-
public Mono<Authentication> authenticate(Authentication authentication) {
56-
final String username = authentication.getName();
57-
final String presentedPassword = (String) authentication.getCredentials();
58-
return this.userDetailsService.findByUsername(username)
59-
.publishOn(this.scheduler)
60-
.filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword()))
61-
.switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials"))))
62-
.flatMap(u -> {
63-
boolean upgradeEncoding = this.userDetailsPasswordService != null
64-
&& this.passwordEncoder.upgradeEncoding(u.getPassword());
65-
if (upgradeEncoding) {
66-
String newPassword = this.passwordEncoder.encode(presentedPassword);
67-
return this.userDetailsPasswordService.updatePassword(u, newPassword);
68-
}
69-
return Mono.just(u);
70-
})
71-
.doOnNext(this.postAuthenticationChecks::check)
72-
.map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) );
73-
}
74-
75-
/**
76-
* The {@link PasswordEncoder} that is used for validating the password. The default is
77-
* {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
78-
* @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null
79-
*/
80-
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
81-
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
82-
this.passwordEncoder = passwordEncoder;
43+
protected Mono<UserDetails> retrieveUser(String username) {
44+
return this.userDetailsService.findByUsername(username);
8345
}
8446

85-
/**
86-
* Sets the {@link Scheduler} used by the {@link UserDetailsRepositoryReactiveAuthenticationManager}.
87-
* The default is {@code Schedulers.parallel()} because modern password encoding is
88-
* a CPU intensive task that is non blocking. This means validation is bounded by the
89-
* number of CPUs. Some applications may want to customize the {@link Scheduler}. For
90-
* example, if users are stuck using the insecure {@link org.springframework.security.crypto.password.NoOpPasswordEncoder}
91-
* they might want to leverage {@code Schedulers.immediate()}.
92-
*
93-
* @param scheduler the {@link Scheduler} to use. Cannot be null.
94-
* @since 5.0.6
95-
*/
96-
public void setScheduler(Scheduler scheduler) {
97-
Assert.notNull(scheduler, "scheduler cannot be null");
98-
this.scheduler = scheduler;
99-
}
100-
101-
/**
102-
* Sets the service to use for upgrading passwords on successful authentication.
103-
* @param userDetailsPasswordService the service to use
104-
*/
105-
public void setUserDetailsPasswordService(
106-
ReactiveUserDetailsPasswordService userDetailsPasswordService) {
107-
this.userDetailsPasswordService = userDetailsPasswordService;
108-
}
109-
110-
/**
111-
* Sets the strategy which will be used to validate the loaded <tt>UserDetails</tt>
112-
* object after authentication occurs.
113-
*
114-
* @param postAuthenticationChecks The {@link UserDetailsChecker}
115-
* @since 5.2
116-
*/
117-
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
118-
Assert.notNull(this.postAuthenticationChecks, "postAuthenticationChecks cannot be null");
119-
this.postAuthenticationChecks = postAuthenticationChecks;
120-
}
12147
}

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

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@
3939

4040
/**
4141
* @author Rob Winch
42+
* @author Eddú Meléndez
4243
* @since 5.1
4344
*/
4445
@RunWith(MockitoJUnitRunner.class)
@@ -171,4 +172,56 @@ public void authenticateWhenPostAuthenticationChecksNotSet() {
171172

172173
verifyZeroInteractions(this.postAuthenticationChecks);
173174
}
175+
176+
@Test(expected = AccountExpiredException.class)
177+
public void authenticateWhenAccountExpiredThenException() {
178+
this.manager.setPasswordEncoder(this.encoder);
179+
180+
UserDetails expiredUser = User.withUsername("user")
181+
.password("password")
182+
.roles("USER")
183+
.accountExpired(true)
184+
.build();
185+
when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(expiredUser));
186+
187+
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
188+
expiredUser, expiredUser.getPassword());
189+
190+
this.manager.authenticate(token).block();
191+
}
192+
193+
@Test(expected = LockedException.class)
194+
public void authenticateWhenAccountLockedThenException() {
195+
this.manager.setPasswordEncoder(this.encoder);
196+
197+
UserDetails lockedUser = User.withUsername("user")
198+
.password("password")
199+
.roles("USER")
200+
.accountLocked(true)
201+
.build();
202+
when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(lockedUser));
203+
204+
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
205+
lockedUser, lockedUser.getPassword());
206+
207+
this.manager.authenticate(token).block();
208+
}
209+
210+
@Test(expected = DisabledException.class)
211+
public void authenticateWhenAccountDisabledThenException() {
212+
this.manager.setPasswordEncoder(this.encoder);
213+
214+
UserDetails disabledUser = User.withUsername("user")
215+
.password("password")
216+
.roles("USER")
217+
.disabled(true)
218+
.build();
219+
when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(disabledUser));
220+
221+
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
222+
disabledUser, disabledUser.getPassword());
223+
224+
this.manager.authenticate(token).block();
225+
}
226+
174227
}

0 commit comments

Comments
 (0)