spring-security-registration-i-forgot-my-password
Spring Security – Reset Your Password
1. Overview
2. The Password Reset Token
@Entity
public class PasswordResetToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
private Date expiryDate;
}
When a password reset is triggered – a token will be created and a special link containing this token will be emailed to the user.
The token and the link will only be valid for a set period of time (24 hours in this example).
3. forgotPassword.html
The first page in the process is the “I forgot my password” page – where the user is prompted for their email address in order for the actual reset process to start.
So – let’s craft a simple forgotPassword.html asking the user for an email address:
<html>
<body>
<h1 th:text="#{message.resetPassword}">reset</h1>
<label th:text="#{label.user.email}">email</label>
<input id="email" name="email" type="email" value="" />
<button type="submit" onclick="resetPass()"
th:text="#{message.resetPassword}">reset</button>
<a th:href="@{/registration.html}" th:text="#{label.form.loginSignUp}">
registration
</a>
<a th:href="@{/login}" th:text="#{label.form.loginLink}">login</a>
<script src="jquery.min.js"></script>
<script th:inline="javascript">
var serverContext = [[function resetPass(){
var email = $("#email").val();
$.post(serverContext + "user/resetPassword",{email: email} ,
function(data){
window.location.href =
serverContext + "login?message=" + data.message;
})
.fail(function(data) {
if(data.responseJSON.error.indexOf("MailError") > -1)
{
window.location.href = serverContext + "emailError.html";
}
else{
window.location.href =
serverContext + "login?message=" + data.responseJSON.message;
}
});
}
</script>
</body>
</html>
We now need to link to this new “reset password” page from the login page:
<a th:href="@{/forgetPassword.html}"
th:text="#{message.resetPassword}">reset</a>
4. Create the PasswordResetToken
@RequestMapping(value = "/user/resetPassword",
method = RequestMethod.POST)
@ResponseBody
public GenericResponse resetPassword(HttpServletRequest request,
@RequestParam("email") String userEmail) {
User user = userService.findUserByEmail(userEmail);
if (user == null) {
throw new UserNotFoundException();
}
String token = UUID.randomUUID().toString();
userService.createPasswordResetTokenForUser(user, token);
mailSender.send(constructResetTokenEmail(getAppUrl(request),
request.getLocale(), token, user));
return new GenericResponse(
messages.getMessage("message.resetPasswordEmail", null,
request.getLocale()));
}
And here is method createPasswordResetTokenForUser():
public void createPasswordResetTokenForUser(User user, String token) {
PasswordResetToken myToken = new PasswordResetToken(token, user);
passwordTokenRepository.save(myToken);
}
And here is method constructResetTokenEmail() – used to send an email with the reset token:
private SimpleMailMessage constructResetTokenEmail(
String contextPath, Locale locale, String token, User user) {
String url = contextPath + "/user/changePassword?id=" +
user.getId() + "&token=" + token;
String message = messages.getMessage("message.resetPassword",
null, locale);
return constructEmail("Reset Password", message + " \r\n" + url, user);
}
private SimpleMailMessage constructEmail(String subject, String body,
User user) {
SimpleMailMessage email = new SimpleMailMessage();
email.setSubject(subject);
email.setText(body);
email.setTo(user.getEmail());
email.setFrom(env.getProperty("support.email"));
return email;
}
Note how we used a simple object GenericResponse to represent our response to the client:
public class GenericResponse {
private String message;
private String error;
public GenericResponse(String message) {
super();
this.message = message;
}
public GenericResponse(String message, String error) {
super();
this.message = message;
this.error = error;
}
}
5. Process the PasswordResetToken
@RequestMapping(value = "/user/changePassword", method = RequestMethod.GET)
public String showChangePasswordPage(Locale locale, Model model,
@RequestParam("id") long id, @RequestParam("token") String token) {
String result = securityService.validatePasswordResetToken(id, token);
if (result != null) {
model.addAttribute("message",
messages.getMessage("auth.message." + result, null, locale));
return "redirect:/login?lang=" + locale.getLanguage();
}
return "redirect:/updatePassword.html?lang=" + locale.getLanguage();
}
And here is validatePasswordResetToken() method:
public String validatePasswordResetToken(long id, String token) {
PasswordResetToken passToken =
passwordTokenRepository.findByToken(token);
if ((passToken == null) || (passToken.getUser()
.getId() != id)) {
return "invalidToken";
}
Calendar cal = Calendar.getInstance();
if ((passToken.getExpiryDate()
.getTime() - cal.getTime()
.getTime()) <= 0) {
return "expired";
}
User user = passToken.getUser();
Authentication auth = new UsernamePasswordAuthenticationToken(
user, null, Arrays.asList(
new SimpleGrantedAuthority("CHANGE_PASSWORD_PRIVILEGE")));
SecurityContextHolder.getContext().setAuthentication(auth);
return null;
}
As you can see – if the token is valid, the user will be authorized to change their password by granting them a CHANGE_PASSWORD_PRIVILEGE, and direct them to a page to update their password.
The interesting note here is – this new privilege will only be usable to change the password (as the name implies) – and so granting it programmatically to the user is safe.
6. Change Password
At this point, the user sees the simple Password Reset page – where the only possible option is to provide a new password:
6.1. updatePassword.html
<html> <body> <div sec:authorize="hasAuthority('CHANGE_PASSWORD_PRIVILEGE')"> <h1 th:text="#{message.resetYourPassword}">reset</h1> <form> <label th:text="#{label.user.password}">password</label> <input id="password" name="newPassword" type="password" value="" /> <label th:text="#{label.user.confirmPass}">confirm</label> <input id="matchPassword" type="password" value="" /> <div id="globalError" style="display:none" th:text="#{PasswordMatches.user}">error</div> <button type="submit" onclick="savePass()" th:text="#{message.updatePassword}">submit</button> </form> <script th:inline="javascript"> var serverContext = [[document).ready(function () { $('form').submit(function(event) { savePass(event); }); $(":password").keyup(function(){ if($("#password").val() != $("#matchPassword").val()){ $("#globalError").show().html(/*[[PasswordMatches.user}]]*/); }else{ $("#globalError").html("").hide(); } }); }); function savePass(event){ event.preventDefault(); if($("#password").val() != $("#matchPassword").val()){ $("#globalError").show().html(/*[[PasswordMatches.user}]]*/); return; } var formData= $('form').serialize(); $.post(serverContext + "user/savePassword",formData ,function(data){ window.location.href = serverContext + "login?message="+data.message; }) .fail(function(data) { if(data.responseJSON.error.indexOf("InternalError") > -1){ window.location.href = serverContext + "login?message=" + data.responseJSON.message; } else{ var errors = $.parseJSON(data.responseJSON.message); $.each( errors, function( index,item ){ $("#globalError").show().html(item.defaultMessage); }); errors = $.parseJSON(data.responseJSON.error); $.each( errors, function( index,item ){ $("#globalError").show().append(item.defaultMessage+"<br/>"); }); } }); } </script> </div> </body> </html>
6.2. Save User Password
@RequestMapping(value = "/user/savePassword", method = RequestMethod.POST)
@ResponseBody
public GenericResponse savePassword(Locale locale,
@Valid PasswordDto passwordDto) {
User user =
(User) SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
userService.changeUserPassword(user, passwordDto.getNewPassword());
return new GenericResponse(
messages.getMessage("message.resetPasswordSuc", null, locale));
}
And here is changeUserPassword() method:
public void changeUserPassword(User user, String password) {
user.setPassword(passwordEncoder.encode(password));
repository.save(user);
}
Note that we are securing update and save password requests – as follows:
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/updatePassword*",
"/user/savePassword*",
"/updatePassword*")
.hasAuthority("CHANGE_PASSWORD_PRIVILEGE")
...
7. Conclusion
In this article, we implemented a simple but very useful feature for a mature Authentication process – the option to reset your own password, as a user of the system.
The full implementation of this tutorial can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.