With the rapid development in the computer industry, computers are becoming more and more powerful. With this power, they can perform many calculations in a given period. While computers are getting stronger, the passwords that keep our accounts safe becomes weaker. In this blog, I’ll help you to add some password policies for your users in the spring boot application.
Project tech stack:
- Java 17
- Maven (4.0.0)
- Spring Boot (2.7.2 with Spring Data JPA, Spring Web, and Spring Validation)
- Passay (For password validation and enforcement)
- H2 Database (Database for test purposes)
- Lombok (To reduce boilerplate code)
- Springdoc Swagger-UI (For API testing)
Source Codes: https://github.com/AykutBuyukkaya/spring-boot-password-validation-example
Pom.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.aykutbuyukaya</groupId>
<artifactId>java-password-validation-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>java-password-validation-example</name>
<description>java-password-validation-example</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Passay is an open-source library for adding password policies for your application. With Passay, you can let your users create stronger passwords. You can set min-max length, character rules, dictionary rules, etc. You can find the source code for passay here.
User Entity
@Entity(name = "test_user")
@RequiredArgsConstructor
@NoArgsConstructor
@Getter
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NonNull
@Column(name = "email", unique = true, nullable = false)
private String email;
@NonNull
@Column(name = "password", nullable = false)
private String password;
}
This simple user entity table will hold an id, email as username, and a password.
SignUpController:
@RestController
@RequestMapping("/authentication")
public class SignupController {
private final UserRepository userRepository;
public SignupController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@PostMapping("/signup")
public ResponseEntity<SignupResponse> signUp(@RequestBody @Valid SignupRequest request) {
SignupResponse response = SignupResponse.builder()
.user(userRepository.save(new User(request.getEmail(), request.getPassword())))
.message("Sign Up Complete!")
.timestamp(Instant.now())
.build();
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public SignupResponse handlePasswordValidationException(MethodArgumentNotValidException e) {
//Returning password error message as a response.
return SignupResponse.builder()
.message(String.join(",", e.getBindingResult().getFieldError().getDefaultMessage()))
.timestamp(Instant.now())
.build();
}
}
Signup Response:
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SignupResponse {
private String message;
private Instant timestamp;
@JsonInclude(JsonInclude.Include.NON_NULL)
private User user;
}
Signup Request:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SignupRequest {
@Email
private String email;
@Password
private String password;
}
JSON file passed through an endpoint will be mapped as SignUpRequest class. Here as you can see we are receiving two input fields as strings. The email field is validated by built-in @Email annotation by Spring Framework. I created @Password annotation for custom validation annotation.
@Password annotation:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PasswordConstraintsValidator.class)
public @interface Password {
String message() default "Invalid password!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
This is our custom annotation for password validation. This is just the annotation. All the work is done in PasswordConstraintsValidator class.
PasswordConstraintsValidator.class:
@Override
public boolean isValid(String password, ConstraintValidatorContext constraintValidatorContext) {
PasswordValidator passwordValidator = new PasswordValidator(
Arrays.asList(
//Length rule. Min 10 max 128 characters
new LengthRule(10, 128),
//At least one upper case letter
new CharacterRule(EnglishCharacterData.UpperCase, 1),
//At least one lower case letter
new CharacterRule(EnglishCharacterData.LowerCase, 1),
//At least one number
new CharacterRule(EnglishCharacterData.Digit, 1),
//At least one special characters
new CharacterRule(EnglishCharacterData.Special, 1),
new WhitespaceRule()
)
);
RuleResult result = passwordValidator.validate(new PasswordData(password));
if (result.isValid()) {
return true;
}
//Sending one message each time failed validation.
constraintValidatorContext.buildConstraintViolationWithTemplate(passwordValidator.getMessages(result).stream().findFirst().get())
.addConstraintViolation()
.disableDefaultConstraintViolation();
return false;
}
}
This is the class that we use to validate annotated fields. IsValid method comes from the ConstraintValidator interface. This method can create a custom validator for a given field. Since our password is marked as a string on the request class, our validator will validate a given string.
On the PasswordValidator constructor, we can pass several password rules served by Passay. Passay has a vast amount of policies for password creation. The rules given in this post are the most common password creation techniques I have seen on many apps and sites. You can find other password policies served by Passay here.
Demo:
I hope this helps 🙂
Feel free to reach me out.
Thanks.