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:
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 thesubmitForm()
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 theUserDto
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 elementvalidatedBy
specifies the classes implementing the constraint. We will create thePasswordConstraintValidator
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 chosenRUNTIME
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.