Revision 002f394d
Added by Andreas Kohlbecker over 2 years ago
cdmlib-services/src/main/java/eu/etaxonomy/cdm/api/service/security/PasswordResetService.java | ||
---|---|---|
8 | 8 |
*/ |
9 | 9 |
package eu.etaxonomy.cdm.api.service.security; |
10 | 10 |
|
11 |
import java.time.Duration; |
|
12 | 11 |
import java.util.HashMap; |
13 | 12 |
import java.util.Map; |
14 | 13 |
import java.util.Optional; |
... | ... | |
16 | 15 |
import javax.mail.internet.AddressException; |
17 | 16 |
import javax.mail.internet.InternetAddress; |
18 | 17 |
|
19 |
import org.apache.commons.collections4.map.HashedMap; |
|
20 |
import org.apache.commons.text.StringSubstitutor; |
|
21 |
import org.apache.log4j.Logger; |
|
22 | 18 |
import org.springframework.beans.factory.annotation.Autowired; |
23 |
import org.springframework.core.env.Environment;
|
|
19 |
import org.springframework.beans.factory.annotation.Qualifier;
|
|
24 | 20 |
import org.springframework.dao.DataAccessException; |
25 | 21 |
import org.springframework.mail.MailException; |
26 |
import org.springframework.mail.SimpleMailMessage; |
|
27 |
import org.springframework.mail.javamail.JavaMailSender; |
|
28 | 22 |
import org.springframework.scheduling.annotation.Async; |
29 | 23 |
import org.springframework.scheduling.annotation.AsyncResult; |
30 | 24 |
import org.springframework.security.core.userdetails.UserDetails; |
... | ... | |
34 | 28 |
import org.springframework.util.Assert; |
35 | 29 |
import org.springframework.util.concurrent.ListenableFuture; |
36 | 30 |
|
37 |
import com.google.common.util.concurrent.RateLimiter; |
|
38 |
|
|
39 |
import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys; |
|
40 |
import eu.etaxonomy.cdm.api.config.SendEmailConfigurer; |
|
41 | 31 |
import eu.etaxonomy.cdm.api.security.AbstractRequestToken; |
42 |
import eu.etaxonomy.cdm.api.security.IPasswordResetTokenStore;
|
|
32 |
import eu.etaxonomy.cdm.api.security.IAbstractRequestTokenStore;
|
|
43 | 33 |
import eu.etaxonomy.cdm.api.security.PasswordResetRequest; |
44 |
import eu.etaxonomy.cdm.api.service.IUserService; |
|
45 | 34 |
import eu.etaxonomy.cdm.model.permission.User; |
46 |
import eu.etaxonomy.cdm.persistence.dao.permission.IUserDao; |
|
47 | 35 |
|
48 | 36 |
/** |
49 | 37 |
* @author a.kohlbecker |
... | ... | |
51 | 39 |
*/ |
52 | 40 |
@Service |
53 | 41 |
@Transactional(readOnly = false) |
54 |
public class PasswordResetService implements IPasswordResetService { |
|
55 |
|
|
56 |
private static Logger logger = Logger.getLogger(PasswordResetRequest.class); |
|
57 |
|
|
58 |
@Autowired |
|
59 |
private IUserDao userDao; |
|
60 |
|
|
61 |
@Autowired |
|
62 |
private IUserService userService; |
|
42 |
public class PasswordResetService extends AccountSelfManagementService implements IPasswordResetService { |
|
63 | 43 |
|
64 | 44 |
@Autowired |
65 |
private IPasswordResetTokenStore passwordResetTokenStore; |
|
66 |
|
|
67 |
@Autowired |
|
68 |
private JavaMailSender emailSender; |
|
69 |
|
|
70 |
@Autowired |
|
71 |
private Environment env; |
|
72 |
|
|
73 |
private Duration rateLimiterTimeout = null; |
|
74 |
private RateLimiter emailResetToken_rateLimiter = RateLimiter.create(PERMITS_PER_SECOND); |
|
75 |
private RateLimiter resetPassword_rateLimiter = RateLimiter.create(PERMITS_PER_SECOND); |
|
45 |
@Qualifier("passwordResetTokenStore") |
|
46 |
IAbstractRequestTokenStore<PasswordResetRequest> passwordResetTokenStore; |
|
76 | 47 |
|
77 | 48 |
/** |
78 | 49 |
* Create a request token and send it to the user via email. |
... | ... | |
120 | 91 |
Map<String, String> additionalValues = new HashMap<>(); |
121 | 92 |
additionalValues.put("linkUrl", passwordRequestFormUrl); |
122 | 93 |
sendEmail(user.getEmailAddress(), user.getUsername(), |
123 |
PasswordResetTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE,
|
|
124 |
PasswordResetTemplates.RESET_REQUEST_EMAIL_BODY_TEMPLATE, additionalValues);
|
|
94 |
UserAccountEmailTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE,
|
|
95 |
UserAccountEmailTemplates.REGISTRATION_REQUEST_EMAIL_BODY_TEMPLATE, additionalValues);
|
|
125 | 96 |
logger.info("A password reset request for " + user.getUsername() + " has been send to " |
126 | 97 |
+ user.getEmailAddress()); |
127 | 98 |
} catch (UsernameNotFoundException e) { |
... | ... | |
137 | 108 |
} |
138 | 109 |
|
139 | 110 |
/** |
140 |
* Uses the {@link StringSubstitutor} as simple template engine. |
|
141 |
* Below named values are automatically resolved, more can be added via the |
|
142 |
* <code>valuesMap</code> parameter. |
|
143 |
* |
|
144 |
* @param userEmail |
|
145 |
* The TO-address |
|
146 |
* @param userName |
|
147 |
* Used to set the value for <code>${userName}</code> |
|
148 |
* @param subjectTemplate |
|
149 |
* A {@link StringSubstitutor} template for the email subject |
|
150 |
* @param bodyTemplate |
|
151 |
* A {@link StringSubstitutor} template for the email body |
|
152 |
* @param additionalValuesMap |
|
153 |
* Additional named values for to be replaced in the template strings. |
|
154 |
*/ |
|
155 |
public void sendEmail(String userEmail, String userName, String subjectTemplate, String bodyTemplate, Map<String, String> additionalValuesMap) throws MailException { |
|
156 |
|
|
157 |
String from = env.getProperty(SendEmailConfigurer.FROM_ADDRESS); |
|
158 |
String dataSourceBeanId = env.getProperty(CdmConfigurationKeys.CDM_DATA_SOURCE_ID); |
|
159 |
String supportEmailAddress = env.getProperty(CdmConfigurationKeys.MAIL_ADDRESS_SUPPORT); |
|
160 |
if(additionalValuesMap == null) { |
|
161 |
additionalValuesMap = new HashedMap<>(); |
|
162 |
} |
|
163 |
if(supportEmailAddress != null) { |
|
164 |
additionalValuesMap.put("supportEmailAddress", supportEmailAddress); |
|
165 |
} |
|
166 |
additionalValuesMap.put("userName", userName); |
|
167 |
additionalValuesMap.put("dataBase", dataSourceBeanId); |
|
168 |
StringSubstitutor substitutor = new StringSubstitutor(additionalValuesMap); |
|
169 |
|
|
170 |
// TODO use MimeMessages for better email layout? |
|
171 |
// TODO user Thymeleaf instead for HTML support? |
|
172 |
SimpleMailMessage message = new SimpleMailMessage(); |
|
173 |
|
|
174 |
message.setFrom(from); |
|
175 |
message.setTo(userEmail); |
|
176 |
|
|
177 |
message.setSubject(substitutor.replace(subjectTemplate)); |
|
178 |
message.setText(substitutor.replace(bodyTemplate)); |
|
179 |
|
|
180 |
emailSender.send(message); |
|
181 |
} |
|
111 |
* |
|
112 |
* @param token |
|
113 |
* the token string |
|
114 |
* @param newPassword |
|
115 |
* The new password to set |
|
116 |
* @return A <code>Future</code> for a <code>Boolean</code> flag. The |
|
117 |
* boolean value will be <code>false</code> in case the max access |
|
118 |
* rate for this method has been exceeded and a time out has |
|
119 |
* occurred. |
|
120 |
* @throws AccountSelfManagementException |
|
121 |
* in case an invalid token has been used |
|
122 |
* @throws MailException |
|
123 |
* in case sending the email has failed |
|
124 |
*/ |
|
125 |
@Override |
|
126 |
@Async |
|
127 |
public ListenableFuture<Boolean> resetPassword(String token, String newPassword) throws AccountSelfManagementException, MailException { |
|
128 |
|
|
129 |
if (resetPassword_rateLimiter.tryAcquire(getRateLimiterTimeout())) { |
|
130 |
|
|
131 |
Optional<PasswordResetRequest> resetRequest = passwordResetTokenStore.findResetRequest(token); |
|
132 |
if (resetRequest.isPresent()) { |
|
133 |
try { |
|
134 |
UserDetails user = userService.loadUserByUsername(resetRequest.get().getUserName()); |
|
135 |
Assert.isAssignable(user.getClass(), User.class); |
|
136 |
userService.encodeUserPassword((User)user, newPassword); |
|
137 |
userDao.saveOrUpdate((User)user); |
|
138 |
passwordResetTokenStore.remove(token); |
|
139 |
sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(), |
|
140 |
UserAccountEmailTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE, |
|
141 |
UserAccountEmailTemplates.RESET_SUCCESS_EMAIL_BODY_TEMPLATE, null); |
|
142 |
return new AsyncResult<Boolean>(true); |
|
143 |
} catch (DataAccessException | IllegalArgumentException | UsernameNotFoundException e) { |
|
144 |
logger.error("Failed to change password of User " + resetRequest.get().getUserName(), e); |
|
145 |
sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(), |
|
146 |
UserAccountEmailTemplates.RESET_FAILED_EMAIL_SUBJECT_TEMPLATE, |
|
147 |
UserAccountEmailTemplates.RESET_FAILED_EMAIL_BODY_TEMPLATE, null); |
|
148 |
} |
|
149 |
} else { |
|
150 |
throw new AccountSelfManagementException("Invalid password reset token"); |
|
151 |
} |
|
152 |
} |
|
153 |
return new AsyncResult<Boolean>(false); |
|
154 |
} |
|
182 | 155 |
|
183 | 156 |
/** |
184 | 157 |
* |
... | ... | |
204 | 177 |
} |
205 | 178 |
return user; |
206 | 179 |
} |
207 |
|
|
208 |
/** |
|
209 |
* |
|
210 |
* @param token |
|
211 |
* the token string |
|
212 |
* @param newPassword |
|
213 |
* The new password to set |
|
214 |
* @return A <code>Future</code> for a <code>Boolean</code> flag. The |
|
215 |
* boolean value will be <code>false</code> in case the max access |
|
216 |
* rate for this method has been exceeded and a time out has |
|
217 |
* occurred. |
|
218 |
* @throws PasswordResetException |
|
219 |
* in case an invalid token has been used |
|
220 |
* @throws MailException |
|
221 |
* in case sending the email has failed |
|
222 |
*/ |
|
223 |
@Override |
|
224 |
@Async |
|
225 |
public ListenableFuture<Boolean> resetPassword(String token, String newPassword) throws PasswordResetException, MailException { |
|
226 |
|
|
227 |
if (resetPassword_rateLimiter.tryAcquire(getRateLimiterTimeout())) { |
|
228 |
|
|
229 |
Optional<PasswordResetRequest> resetRequest = passwordResetTokenStore.findResetRequest(token); |
|
230 |
if (resetRequest.isPresent()) { |
|
231 |
try { |
|
232 |
UserDetails user = userService.loadUserByUsername(resetRequest.get().getUserName()); |
|
233 |
Assert.isAssignable(user.getClass(), User.class); |
|
234 |
userService.encodeUserPassword((User)user, newPassword); |
|
235 |
userDao.saveOrUpdate((User)user); |
|
236 |
passwordResetTokenStore.remove(token); |
|
237 |
sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(), |
|
238 |
PasswordResetTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE, |
|
239 |
PasswordResetTemplates.RESET_SUCCESS_EMAIL_BODY_TEMPLATE, null); |
|
240 |
return new AsyncResult<Boolean>(true); |
|
241 |
} catch (DataAccessException | IllegalArgumentException | UsernameNotFoundException e) { |
|
242 |
logger.error("Failed to change password of User " + resetRequest.get().getUserName(), e); |
|
243 |
sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(), |
|
244 |
PasswordResetTemplates.RESET_FAILED_EMAIL_SUBJECT_TEMPLATE, |
|
245 |
PasswordResetTemplates.RESET_FAILED_EMAIL_BODY_TEMPLATE, null); |
|
246 |
} |
|
247 |
} else { |
|
248 |
throw new PasswordResetException("Invalid password reset token"); |
|
249 |
} |
|
250 |
} |
|
251 |
return new AsyncResult<Boolean>(false); |
|
252 |
} |
|
253 |
|
|
254 |
@Override |
|
255 |
public Duration getRateLimiterTimeout() { |
|
256 |
if(rateLimiterTimeout == null) { |
|
257 |
rateLimiterTimeout = Duration.ofSeconds(RATE_LIMTER_TIMEOUT_SECONDS); |
|
258 |
} |
|
259 |
return rateLimiterTimeout; |
|
260 |
} |
|
261 |
|
|
262 |
@Override |
|
263 |
public void setRateLimiterTimeout(Duration timeout) { |
|
264 |
this.rateLimiterTimeout = timeout; |
|
265 |
} |
|
266 |
|
|
267 |
@Override |
|
268 |
public void setRate(double rate) { |
|
269 |
resetPassword_rateLimiter.setRate(rate); |
|
270 |
emailResetToken_rateLimiter.setRate(rate); |
|
271 |
} |
|
272 | 180 |
} |
Also available in: Unified diff
fix #9497 User self registration service implemented