Spring Custom Password Validation

Introduction

These days, password policies are very common and exist on most platforms online. While certain users don't really like them, there's a reason why they exist – making passwords safer.

You've most certainly had experience with applications forcing certain rules for your password like the minimum or maximum number of characters allowed, including digits, capital letters etc.

No matter how good the security system is, if a user chooses a weak password such as "password", sensitive data might be exposed. While some users might get irritated by the password policies, they keep your user's data safe as it makes attacks much more inefficient.

To implement this in our Spring-based applications, we'll be using Passay – a library made specifically for this purpose which makes enforcing password policies easy in Java.

Please note: This tutorial assumes that you have basic knowledge of the Spring framework so we'll focus more on Passay for brevity.

Aside from password policies, a good and fundamental technique to implement for security is Password Encoding.

Registration Form

The simplest way to start with a skeleton Spring Boot project, as always, is using Spring Initializr.

Select your preferred version of Spring Boot and add the Web and Thymeleaf dependencies:

After this, generate it as a Maven project and you're all set!

Let's define a simple Data Transfer Object in which we'll include all the attributes that we want to capture from our form:

public class UserDto {

    @NotEmpty
    private String name;

    @Email
    @NotEmpty
    private String email;

    private String password;

We haven't annotated the password field yet, because we'll be implementing a custom annotation for this.

Then we have a simple controller class that serves the signup form and captures its data when submitted using the GET/POST mappings:

@Controller
@RequestMapping("/signup")
public class SignUpController {

    @ModelAttribute("user")
    public UserDto userDto() {
        return new UserDto();
    }

    @GetMapping
    public String showForm() {
        return "signup";
    }

    @PostMapping
    public String submitForm(@Valid @ModelAttribute("user") UserDto user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "signup";
        }
        return "success";
    }

}

We first defined a @ModelAttribute("user") and assigned it a UserDto instance. This is the object that will hold the information once submitted.

Using this object, we can extract the data and persist it in the database.

The showForm() method returns a String with the value "signup". Since we have Thymeleaf in our classpath, Spring will search for signup.html in the templates folder in resources.

Similarly, we have a submitForm() POST mapping that will check if the form has any errors. If it does, it will redirect back to the signup.html page. Otherwise, it will forward the user to the "success" page.

Thymeleaf is a modern server-side Java template engine for processing and creating HTML, XML, JavaScript, CSS, and text. It's a modern alternative for older templating engines like Java Server Pages (JSP).

Let's go ahead and define a signup.html page:

<form action="#" th:action="@{/signup}" th:object="${user}" method="post">

    <div class="form-group">
        <input type="text" th:field="*{name}" class="form-control"
               id="name" placeholder="Name"> <span
               th:if="${#fields.hasErrors('name')}" th:errors="*{name}"
               class="text-danger"></span>
     </div>
     <div class="form-group">
        <input type="text" th:field="*{email}" class="form-control"
               id="email" placeholder="Email"> <span
               th:if="${#fields.hasErrors('email')}" th:errors="*{email}"
               class="text-danger"></span>
     </div>
     <div class="form-group">
         <input type="text" th:field="*{password}" class="form-control"
                id="password" placeholder="Password">
         <ul class="text-danger" th:each="error: ${#fields.errors('password')}">
             <li th:each="message : ${error.split(',')}">
                 <p class="error-message" th:text="${message}"></p>
             </li>
         </ul>
     </div>

     <div class="col-md-6 mt-5">
         <input type="submit" class="btn btn-primary" value="Submit">
     </div>
</form>

There are a few things that should be pointed out here:

Free eBook: Git Essentials

Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!

  • th:action = "@{/signup}" - The action attribute refers to the URL we're calling upon submitting the form. We're targeting the "signup" URL mapping in our controller.
  • method="post" - The method attribute refers to the type of request we're sending. This has to match the type of request defined in the submitForm() method.
  • th:object="${user}" - The object attribute refers to the object name we've defined in the controller earlier using @ModelAttribute("user"). Using the rest of the form, we'll populate the fields of the UserDto instance, and then save the instance.

We have 3 other input fields that are mapped to name, email, and password using the th:field tag. If the fields have errors, the user will be notified via the th:errors tag.

Let's run our application and navigate to http://localhost:8080/signup:

Custom @ValidPassword Annotation

Depending on the project requirements, we sometimes have to define custom code specific for our applications.

Since we can enforce different policies and rules, let's go ahead and define a custom annotation that checks for a valid password, which we'll be using in our UserDto class.

Annotations are just metadata for the code and do not contain any business logic. They can only provide information about the attribute (class/method/package/field) on which it's defined.

Lets create our @ValidPassword annotation:

@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ValidPassword {

    String message() default "Invalid Password";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

As you can see, to create an annotation we use the @interface keyword. Let's take a look at a few of the keywords and fully understand them before proceeding:

  • @Documented: A simple marker annotations which tell whether to add Annotation in Javadocs or not.
  • @Constraint: Marks an annotation as being a Bean Validation Constraint. The element validatedBy specifies the classes implementing the constraint. We will create the PasswordConstraintValidator class a bit later on.
  • @Target: Is where our annotations can be used. If you don't specify this, the annotation can be placed anywhere. Currently, our annotation can be placed over an instance variable and on other annotations.
  • @Retention: Defines for how long the annotation should be kept. We have chosen RUNTIME so that it can be used by the runtime environment.

To use this in our UserDto class simple annotate the password field:

@ValidPassword
private String password;

Custom Password Constraint Validator

Now that we have our annotation, let's implement the validation logic for it. Before that make sure you have the Passay Maven dependency included in your pom.xml file:

<dependency>
    <groupId>org.passay</groupId>
    <artifactId>passay</artifactId>
    <version>{$version}</version>
</dependency>

You can check for the latest dependency here.

Finally, let's write our PasswordConstraintValidator class:

public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {

    @Override
    public void initialize(ValidPassword arg0) {
    }

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        PasswordValidator validator = new PasswordValidator(Arrays.asList(
            // at least 8 characters
            new LengthRule(8, 30),

            // at least one upper-case character
            new CharacterRule(EnglishCharacterData.UpperCase, 1),

            // at least one lower-case character
            new CharacterRule(EnglishCharacterData.LowerCase, 1),

            // at least one digit character
            new CharacterRule(EnglishCharacterData.Digit, 1),

            // at least one symbol (special character)
            new CharacterRule(EnglishCharacterData.Special, 1),

            // no whitespace
            new WhitespaceRule()

        ));
        RuleResult result = validator.validate(new PasswordData(password));
        if (result.isValid()) {
            return true;
        }
        List<String> messages = validator.getMessages(result);

        String messageTemplate = messages.stream()
            .collect(Collectors.joining(","));
        context.buildConstraintViolationWithTemplate(messageTemplate)
            .addConstraintViolation()
            .disableDefaultConstraintViolation();
        return false;
    }
}

We implemented the ConstraintValidator interface which forces us to implement a couple of methods.

We first created a PasswordValidator object by passing an array of constraints that we want to enforce in our password.

The constraints are self-explanatory:

  • It must be between 8 and 30 characters long as defined by the LengthRule
  • It must have at least 1 lowercase character as defined by the CharacterRule
  • It must have at least 1 uppercase character as defined by the CharacterRule
  • It must have at least 1 special character as defined by the CharacterRule
  • It must have at least 1 digit character as defined by the CharacterRule
  • It must not contain whitespaces as defined by the WhitespaceRule

The full list of rules that can be written using Passay can be found on the official website.

Finally, we validated the password and returned true if it passes all conditions. If some conditions fail, we aggregated all the failed condition's error messages in a String separated by "," and then put it into the context and returned false.

Let's run our project again and type in an invalid password to verify the validation works:

Conclusion

In this article, we covered how to enforce certain password rules using the Passay library. We created a custom annotation and password constraint validator for this and used it in our instance variable, and the actual business logic was implemented in a separate class.

As always, the code for the examples used in this article can be found on GitHub.

Last Updated: August 2nd, 2023
Was this article helpful?

© 2013-2024 Stack Abuse. All rights reserved.

AboutDisclosurePrivacyTerms