The Master Class of "Learn Spring Security" is out:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial – we’re continuing the ongoing Registration with Spring Security series with a look at the basic “I forgot my password” feature – so that the user can safely reset their own password when they need to.

2. The Password Reset Token

Let’s start by creating a PasswordResetToken entity to use it for resetting the user’s password:

@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

Let’s start by creating the new PasswordResetToken and send it via email to the user:

@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

The user gets the email with the unique link for resetting their password, and clicks the link:

@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

Finally, when the previous post request is submitted – the new user password is saved:

@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.

The Master Class "Learn Spring Security" is out:

>> CHECK OUT THE COURSE

  • Andreia Santos

    Hello

    I had some problems while trying to run the application, please if you could help…
    To run the project, I just have to change the files email.properties and create the database registration_02? I can run using Jetty;run right?

    Thank you very much

    • That is pretty much it. On the persistence side of things, also make sure that you also create the user that is defined in persistence.properties and give it the right permissions – so that the app is able to access the DB. If you run into any kind of problem, go ahead and open an issue over on github and I’ll take a look. Cheers,
      Eugen.

  • Marcel Brimus

    Hello,

    Thanks for this awesome tutorial.
    I have one question, wont this line:

    Authentication auth = new UsernamePasswordAuthenticationToken(
    user, null, userDetailsService.loadUserByUsername(user.getEmail()).getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(auth);

    make user Authenticated for /updatePassword.html and thus for every other page where user needs to have READ_PRIVILEGE? If so how can i “de-authenticate” user when he does another request?

  • David Clark

    Very nice tutorial, but I was unable to find the referenced source code on GitHub. Is it still available somewhere?

    • Hey David – yeah, I updated/moved the code on github yesterday – I’m updating the links now. Cheers,
      Eugen.

  • Dre

    Thank you for your tutorial.

  • Ranjan Kumar

    I dont know the application is working or not but my server is crashing every time ….shit

    • Hey Ranjan – that’s interesting. When you say “crashes” – you’ll have to be more specific than that. Is there a stack trace? Does the JVM process dies? What does the crash look like exactly?

      • Ranjan Kumar

        Hey actually the problem is on IDE I changed my work space and now its fine but help me when I am using the gmail smtp there is a problem while connecting MySql server.. If its possible tell me how to configure gmail smtp into your application.. Thanks dude.

        • Hey Ranjan – so, the configuration would look like this:

          smtp.host=smtp.gmail.com
          smtp.port=465
          smtp.protocol=smtps
          [email protected]
          spring.mail.password=TODO

          Hope it helps. Cheers,
          Eugen.

  • Jossy

    When I submit update Password its not working. Its showing following error
    name:savePassword
    statuts:403 forbidden
    type: xhr
    initiator:jquery.min.js:6

    • Hey Jossy – can you have another look now – should be sorted. Cheers,
      Eugen.

      • Jossy

        Yeah , I sort-out the error . Its because of parameter mismatch . Thanks for consideration Mr. Parashiv

  • Hzmr

    hi, i want to know that is it the table in database is auto created when we run this project? or not? and why i cannot run this project? it says that this project cannot be deployed? please help me

    • Yes, the DB structure should be auto-created. Of course that’s only for development and you can change that in persistence.properties.
      Cheers,
      Eugen.

  • Jim Clayson

    Hi Eugen,

    I just wondered if you had any suggestion for encrypting the smtp password on the server-side, as it will be stored in the code(or in the email.properties file) in plain text.

    As you probably know, the one-way encryption technique used for user password storage and authentication is fine if you have an incoming password for comparison. But an SMTP password is encrypted before being sent out, implying it is not encrypted(plain text) beforehand.

    So it seems this – storing the smtp password in plain text – could be a vulnerability in terms of access to an email account.

    Any ideas?

    • Hey Jim,
      Not yet, but working on it – keep an eye on the feed of the site in the next couple of months.
      Cheers,
      Eugen.

  • Oleg

    Hi Eugen,
    Thank you for the tutorial.

    After implementing this into my project I get “org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.proj.persistence.model.User.roles, could not initialize proxy – no Session” when user is being retrieved from “userRepository” in “loadUserByUsername” method.

    Are you familiar with this?

    Thank you!

    • Hey Oleg – sure, that’s a very common error, but there might be several things going on that might trigger it. Are you able to reproduce the problem on the main codebase of the article? Let me know if you are – I’ll definitely have a look.
      Cheers,
      Eugen.

      • Oleg

        Thanks for answer!
        No, I am actually not. Your code is fine.
        All advises I have read say that this happens because no connection to a DB is opened when a User object is trying to retrieve roles from a DB. I see you do not use FetchType.EAGER to get roles. I am not able to figure out why your code retrieves roles successfully and keep a connection with no session error.

        Cheers
        Oleg

        • First, yes – that’s a good way to think about it (although not entirely accurate). Keep in mind that the error refers to a missing Session – which is an abstraction that doesn’t map 1-1 to a connection.
          However, that’s why I was asking for code reproducing the issue – it’s not possible to diagnose why it’s working here but not in your codebase – without looking at code.
          Now – one way to go is – you can start from this codebase, and slowly introduce your own changes. Run the tests for each change you’re adding and see where it breaks.
          Hope that helps. Cheers,
          Eugen.

      • Oleg

        Hi again!
        What part of your code is responsible for keeping connection opened?

  • Vineet Chauhan

    Can I get your email -? I need to speak regarding a refund.

    • Hey Vineet – sure – simply reply to any email that you have from me – those go right to my inbox.
      Cheers,
      Eugen.

  • Hey Dave,
    First, I’m glad you’re enjoying the material.
    I did try to replicate your issue and the project starts perfectly fine for me (via the Boot runner).
    And given your solution – it looks like you’re not running the project from Github as it is right?
    Cheers,
    Eugen.