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