Spring Security Forgot Password Send Email Reset Password
In this tutorial we demonstrate how to use Spring Security, Spring Boot, Hibernate and Thymeleaf to program a password reset flow by sending the user an email address to verify the reset password procedure. When a user has forgot his password, he is able to request a password reset. The application will generate a unique PasswordResetToken
and store it in the database. The user’ll receive an email with the unique token. When he clicks the link, the user is redirected to a page where he can change his password.
At the bottom we wrote some integration tests using spring-test
, h2 in-memory database
, GreenMail
, JUnit
and MockMvc
to verify the forgot password and reset password procedures.
Project Structure
Let’s start by looking at the project structure.
Maven Dependencies
We use Apache Maven to manage our project dependencies. Make sure the following dependencies reside on the class-path.
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.memorynotfound.spring.security</groupId>
<artifactId>reset-password</artifactId>
<version>1.0.0-SNAPSHOT</version>
<url>https://memorynotfound.com/spring-security-forgot-password-send-email-reset-password</url>
<name>Spring Security - ${project.artifactId}</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
</dependency>
<!-- bootstrap and jquery -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.2.1</version>
</dependency>
<!-- mysql connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.5.5</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Password Reset Token
We created a PasswordResetToken
which is responsible for mapping the unique token to the User
. This unique token is created and stored in the database when the users requests a forgot password action. We can retrieve this token again when the user received the email and changes his password.
package com.memorynotfound.spring.security.model;
import javax.persistence.*;
import java.util.Calendar;
import java.util.Date;
@Entity
public class PasswordResetToken {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false, unique = true)
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
@Column(nullable = false)
private Date expiryDate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Date getExpiryDate() {
return expiryDate;
}
public void setExpiryDate(Date expiryDate) {
this.expiryDate = expiryDate;
}
public void setExpiryDate(int minutes){
Calendar now = Calendar.getInstance();
now.add(Calendar.MINUTE, minutes);
this.expiryDate = now.getTime();
}
public boolean isExpired() {
return new Date().after(this.expiryDate);
}
}
We created the PasswordResetTokenRepository
and extend the spring data JpaRepository
this enables CRUD
operations to our entity. This class is responsible for storing and retrieving the unique token from the database.
package com.memorynotfound.spring.security.repository;
import com.memorynotfound.spring.security.model.PasswordResetToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PasswordResetTokenRepository extends JpaRepository<PasswordResetToken, Long> {
PasswordResetToken findByToken(String token);
}
Password Forgot Controller
You can submit a form post to the PasswordForgotController
which’ll handle the incoming password forgot request. We use standard hibernate validator annotations on the PasswordForgotDto
to validate the incoming request. We create a new unique PasswordResetToken
and store it in the database. We forward this token information to the user by email. This email contains a special link to reset his password.
package com.memorynotfound.spring.security.web;
import com.memorynotfound.spring.security.model.Mail;
import com.memorynotfound.spring.security.model.PasswordResetToken;
import com.memorynotfound.spring.security.model.User;
import com.memorynotfound.spring.security.repository.PasswordResetTokenRepository;
import com.memorynotfound.spring.security.service.EmailService;
import com.memorynotfound.spring.security.service.UserService;
import com.memorynotfound.spring.security.web.dto.PasswordForgotDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Controller
@RequestMapping("/forgot-password")
public class PasswordForgotController {
@Autowired private UserService userService;
@Autowired private PasswordResetTokenRepository tokenRepository;
@Autowired private EmailService emailService;
@ModelAttribute("forgotPasswordForm")
public PasswordForgotDto forgotPasswordDto() {
return new PasswordForgotDto();
}
@GetMapping
public String displayForgotPasswordPage() {
return "forgot-password";
}
@PostMapping
public String processForgotPasswordForm(@ModelAttribute("forgotPasswordForm") @Valid PasswordForgotDto form,
BindingResult result,
HttpServletRequest request) {
if (result.hasErrors()){
return "forgot-password";
}
User user = userService.findByEmail(form.getEmail());
if (user == null){
result.rejectValue("email", null, "We could not find an account for that e-mail address.");
return "forgot-password";
}
PasswordResetToken token = new PasswordResetToken();
token.setToken(UUID.randomUUID().toString());
token.setUser(user);
token.setExpiryDate(30);
tokenRepository.save(token);
Mail mail = new Mail();
mail.setFrom("[email protected]");
mail.setTo(user.getEmail());
mail.setSubject("Password reset request");
Map<String, Object> model = new HashMap<>();
model.put("token", token);
model.put("user", user);
model.put("signature", "https://memorynotfound.com");
String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
model.put("resetUrl", url + "/reset-password?token=" + token.getToken());
mail.setModel(model);
emailService.sendEmail(mail);
return "redirect:/forgot-password?success";
}
}
We created the PasswordForgotDto
to validate the form submission for correct input parameters using standard hibernate validator annotations.
package com.memorynotfound.spring.security.web.dto;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
public class PasswordForgotDto {
@Email
@NotEmpty
private String email;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Password Reset Email
Next, we take a look at how to send the password reset email. We start by creating a Mail
object which holds the meta-data
for the email.
package com.memorynotfound.spring.security.model;
import java.util.List;
import java.util.Map;
public class Mail {
private String from;
private String to;
private String subject;
private Map<String, Object> model;
public Mail() {
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public Map<String, Object> getModel() {
return model;
}
public void setModel(Map<String, Object> model) {
this.model = model;
}
}
Next, we create an EmailService
which is responsible for creating and sending the email. We used an HTML
email template and added the email meta-data to the Model
.
package com.memorynotfound.spring.security.service;
import com.memorynotfound.spring.security.model.Mail;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring4.SpringTemplateEngine;
import javax.mail.internet.MimeMessage;
import java.nio.charset.StandardCharsets;
@Service
public class EmailService {
@Autowired
private JavaMailSender emailSender;
@Autowired
private SpringTemplateEngine templateEngine;
public void sendEmail(Mail mail) {
try {
MimeMessage message = emailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message,
MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED,
StandardCharsets.UTF_8.name());
Context context = new Context();
context.setVariables(mail.getModel());
String html = templateEngine.process("email/email-template", context);
helper.setTo(mail.getTo());
helper.setText(html, true);
helper.setSubject(mail.getSubject());
helper.setFrom(mail.getFrom());
emailSender.send(message);
} catch (Exception e){
throw new RuntimeException(e);
}
}
}
This email email-template.html
is located in the src/main/resources/email
folder. We can use the values provided in the Model
to fill our e-mail template. We include a password reset link with the unique token that the user can use to reset his password.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Sending Email with Thymeleaf HTML Template Example</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link href='http://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css'/>
<!-- use the font -->
<style>
body {
font-family: 'Roboto', sans-serif;
font-size: 48px;
}
</style>
</head>
<body style="margin: 0; padding: 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse;">
<tr>
<td align="center" bgcolor="#78ab46" style="padding: 40px 0 30px 0;">
<p>Memorynotfound.com</p>
</td>
</tr>
<tr>
<td bgcolor="#eaeaea" style="padding: 40px 30px 40px 30px;">
<p th:text="${'Dear ' + user.firstName + ' ' + user.lastName}"></p>
<p>
You've requested a password reset.
<a th:href="${resetUrl}">reset your password</a>
</p>
<p>Thanks</p>
</td>
</tr>
<tr>
<td bgcolor="#777777" style="padding: 30px 30px 30px 30px;">
<p th:text="${signature}"></p>
</td>
</tr>
</table>
</body>
</html>
Password Reset Controller
When the user has received his password reset email. He is forwarded to the PasswordResetController
mapped to the /reset-password URL. This produces a HTTP GET
with the token as request parameter. We read the token and if the token is present and valid we put it in the Model
map. When the user posts his PasswordResetDto
, the form is validated and executed if no errors occur.
package com.memorynotfound.spring.security.web;
import com.memorynotfound.spring.security.model.PasswordResetToken;
import com.memorynotfound.spring.security.model.User;
import com.memorynotfound.spring.security.repository.PasswordResetTokenRepository;
import com.memorynotfound.spring.security.service.UserService;
import com.memorynotfound.spring.security.web.dto.PasswordResetDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
@Controller
@RequestMapping("/reset-password")
public class PasswordResetController {
@Autowired private UserService userService;
@Autowired private PasswordResetTokenRepository tokenRepository;
@Autowired private BCryptPasswordEncoder passwordEncoder;
@ModelAttribute("passwordResetForm")
public PasswordResetDto passwordReset() {
return new PasswordResetDto();
}
@GetMapping
public String displayResetPasswordPage(@RequestParam(required = false) String token,
Model model) {
PasswordResetToken resetToken = tokenRepository.findByToken(token);
if (resetToken == null){
model.addAttribute("error", "Could not find password reset token.");
} else if (resetToken.isExpired()){
model.addAttribute("error", "Token has expired, please request a new password reset.");
} else {
model.addAttribute("token", resetToken.getToken());
}
return "reset-password";
}
@PostMapping
@Transactional
public String handlePasswordReset(@ModelAttribute("passwordResetForm") @Valid PasswordResetDto form,
BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()){
redirectAttributes.addFlashAttribute(BindingResult.class.getName() + ".passwordResetForm", result);
redirectAttributes.addFlashAttribute("passwordResetForm", form);
return "redirect:/reset-password?token=" + form.getToken();
}
PasswordResetToken token = tokenRepository.findByToken(form.getToken());
User user = token.getUser();
String updatedPassword = passwordEncoder.encode(form.getPassword());
userService.updatePassword(updatedPassword, user.getId());
tokenRepository.delete(token);
return "redirect:/login?resetSuccess";
}
}
The PasswordResetDto
is used to validate the incoming form parameters using standard hibernate validator annotations. We emitted the @FieldMatch
annotations in this tutorial for simplicity. But you can read more about it in the Spring Security User Registration Example ” rel=”dofollow”>user registration example.
package com.memorynotfound.spring.security.web.dto;
import com.memorynotfound.spring.security.constraint.FieldMatch;
import org.hibernate.validator.constraints.NotEmpty;
@FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
public class PasswordResetDto {
@NotEmpty
private String password;
@NotEmpty
private String confirmPassword;
@NotEmpty
private String token;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getConfirmPassword() {
return confirmPassword;
}
public void setConfirmPassword(String confirmPassword) {
this.confirmPassword = confirmPassword;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
Spring Boot + Hibernate + Email Configuration
We configure Hibernate, JPA and Mail using the application.yml
file located in the src/main/resources
folder.
# ===============================
# = Hibernate datasource
# ===============================
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_security_hibernate
username: root
password:
# ===============================
# = JPA configurations
# ===============================
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
database-platform: MYSQL
properties:
hibernate.dialect: org.hibernate.dialect.MySQL5Dialect
# ===============================
# = MAIL configurations
# ===============================
mail:
default-encoding: UTF-8
host: smtp.gmail.com
username: [email protected]
password: secret
port: 587
properties:
mail:
smtp:
auth: true
starttls:
enable: true
protocol: smtp
test-connection: false
# ===============================
# = Logging configurations
# ===============================
logging:
level:
root: WARN
com.memorynotfound: DEBUG
org.springframework.web: INFO
org.springframework.security: INFO
Spring Security Configuration
This is the spring security configuration. Make sure to permit all access to the /forgot-password
and /reset-password
URI’s.
package com.memorynotfound.spring.security.config;
import com.memorynotfound.spring.security.service.UserService;
import com.memorynotfound.spring.security.service.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(
"/registration**",
"/forgot-password**",
"/reset-password**").permitAll()
.antMatchers(
"/js/**",
"/css/**",
"/img/**",
"/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.permitAll();
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(userService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}
User Management
We created a custom User
object and mapped it to the database using standard java persistence annotations.
package com.memorynotfound.spring.security.model;
import javax.persistence.*;
import java.util.Collection;
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "email"))
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(
name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(
name = "role_id", referencedColumnName = "id"))
private Collection<Role> roles;
public User() {
}
public User(String firstName, String lastName, String email, String password) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
}
public User(String firstName, String lastName, String email, String password, Collection<Role> roles) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
this.roles = roles;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Collection<Role> getRoles() {
return roles;
}
public void setRoles(Collection<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", email='" + email + '\'' +
", password='" + "*********" + '\'' +
", roles=" + roles +
'}';
}
}
We created a custom Role
object and mapped it to the database using standard java persistence annotations.
package com.memorynotfound.spring.security.model;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
public Role() {
}
public Role(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
The UserService
is used to manage the users.
package com.memorynotfound.spring.security.service;
import com.memorynotfound.spring.security.model.User;
import com.memorynotfound.spring.security.web.dto.UserRegistrationDto;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
User findByEmail(String email);
User save(UserRegistrationDto registration);
void updatePassword(String password, Long userId);
}
Here is the implementation of the UserService
.
package com.memorynotfound.spring.security.service;
import com.memorynotfound.spring.security.model.Role;
import com.memorynotfound.spring.security.model.User;
import com.memorynotfound.spring.security.repository.UserRepository;
import com.memorynotfound.spring.security.web.dto.UserRegistrationDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public User findByEmail(String email){
return userRepository.findByEmail(email);
}
public User save(UserRegistrationDto registration){
User user = new User();
user.setFirstName(registration.getFirstName());
user.setLastName(registration.getLastName());
user.setEmail(registration.getEmail());
user.setPassword(passwordEncoder.encode(registration.getPassword()));
user.setRoles(Arrays.asList(new Role("ROLE_USER")));
return userRepository.save(user);
}
@Override
public void updatePassword(String password, Long userId) {
userRepository.updatePassword(password, userId);
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email);
if (user == null){
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getEmail(),
user.getPassword(),
mapRolesToAuthorities(user.getRoles()));
}
private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles){
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
The UserRepository
is responsible for managing the User
object database state. We created a special updatePassword
method which updates the password for a particular user.
package com.memorynotfound.spring.security.repository;
import com.memorynotfound.spring.security.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
@Modifying
@Query("update User u set u.password = :password where u.id = :id")
void updatePassword(@Param("password") String password, @Param("id") Long id);
}
Spring Boot
We start the application using Spring Boot.
package com.memorynotfound.spring.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Run {
public static void main(String[] args) {
SpringApplication.run(Run.class, args);
}
}
Thymeleaf Templates
We used Bootstrap
and JQuery
to create our Thymeleaf templates.
The templates are located in the src/main/resources/templates/
folder.
Forgot Password Page
The forgot-password.html
page is responsible for requesting the password reset email.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/>
<title>Forgot Password</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-body">
<div class="text-center">
<h3><i class="glyphicon glyphicon-lock" style="font-size:2em;"></i></h3>
<h2 class="text-center">Forgot Password?</h2>
<p>Enter your e-mail address and we'll send you a link to reset your password.</p>
<div class="panel-body">
<div th:if="${param.success}">
<div class="alert alert-info">
You've successfully requested a new password reset!
</div>
</div>
<form th:action="@{/forgot-password}" th:object="${forgotPasswordForm}" method="post">
<div class="form-group" th:classappend="${#fields.hasErrors('email')}? 'has-error':''">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-envelope color-blue"></i>
</span>
<input id="email"
class="form-control"
placeholder="email address"
th:field="*{email}"/>
</div>
<p class="error-message"
th:each="error : ${#fields.errors('email')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success btn-block">Reset Password</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
New user? <a href="/" th:href="@{/registration}">Register</a>
</div>
<div class="col-md-12">
Already registered? <a href="/" th:href="@{/login}">Login</a>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</body>
</html>
Reset Password Page
The reset-password.html
page is responsible for requesting the actual password reset.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/css/main.css}"/>
<title>Forgot Password</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default">
<div class="panel-body">
<div class="text-center">
<h3><i class="glyphicon glyphicon-lock" style="font-size:2em;"></i></h3>
<h2 class="text-center">Reset password</h2>
<div class="panel-body">
<div th:if="${error}">
<div class="alert alert-danger">
<span th:text="${error}"></span>
</div>
</div>
<form th:action="@{/reset-password}" th:object="${passwordResetForm}" method="post">
<p class="error-message"
th:if="${#fields.hasGlobalErrors()}"
th:each="error : ${#fields.errors('global')}"
th:text="${error}">Validation error</p>
<input type="hidden" name="token" th:value="${token}"/>
<div class="form-group"
th:classappend="${#fields.hasErrors('password')}? 'has-error':''">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input id="password"
class="form-control"
placeholder="password"
type="password"
th:field="*{password}"/>
</div>
<p class="error-message"
th:each="error: ${#fields.errors('password')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group"
th:classappend="${#fields.hasErrors('confirmPassword')}? 'has-error':''">
<div class="input-group">
<span class="input-group-addon">
<i class="glyphicon glyphicon-lock"></i>
</span>
<input id="confirmPassword"
class="form-control"
placeholder="Confirm password"
type="password"
th:field="*{confirmPassword}"/>
</div>
<p class="error-message"
th:each="error: ${#fields.errors('confirmPassword')}"
th:text="${error}">Validation error</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-block btn-success">Reset password</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
New user? <a href="/" th:href="@{/registration}">Register</a>
</div>
<div class="col-md-12">
Already registered? <a href="/" th:href="@{/login}">Login</a>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
<script type="text/javascript" th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</body>
</html>
Demo
Access the http://localhost:8080/forgot-password
and provide an invalid email.
Access the http://localhost:8080/forgot-password
and provide a valid email.
This is an example email which the user receives upon requesting a password reset. Note that the email contains a unique link.
When the user clicks on the link inside the email, he is forwarded to the page where he can reset his password. When the user enters a password, but they do not match he receives the following output.
Integration Testing
Let’s write some integrations tests using H2
, JUnit
, spring-test
, GreenMail
and MockMvc
.
Spring Integration Test Configuration
We configure the integration tests using the application.yml
file located in the src/test/resources/
folder. This overrides the application configuration file.
# ===============================
# = H2 data source
# ===============================
spring:
datasource:
url: jdbc:h2:mem:spring-security-hibernate-test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
platform: h2
username: sa
password:
# ===============================
# = JPA configurations
# ===============================
jpa:
show-sql: true
hibernate:
ddl-auto: create-drop
naming-strategy: org.hibernate.cfg.ImprovedNamingStrategy
database-platform: H2
# ===============================
# = MAIL configurations
# ===============================
mail:
default-encoding: UTF-8
host: localhost
jndi-name:
username: username
password: secret
port: 2525
properties:
mail:
debug: false
smtp:
debug: false
auth: true
starttls: true
protocol: smtp
test-connection: false
We can initialize the H2
In Memory Database using the data.sql
file, located in the src/test/resources
folder.
INSERT INTO user (id, first_name, last_name, email, password) VALUES (1, 'Memory', 'Not Found', '[email protected]', '$2a$10$RyY4bXtV3LKkDCutlUTYDOKd2AiJYZGp4Y7MPVdLzWzT1RX.JRZyG');
INSERT INTO user (id, first_name, last_name, email, password) VALUES (2, 'Memory', 'Not Found', '[email protected]', '$2a$10$RyY4bXtV3LKkDCutlUTYDOKd2AiJYZGp4Y7MPVdLzWzT1RX.JRZyG');
INSERT INTO role (id, name) VALUES (1, 'ROLE_ADMIN');
INSERT INTO role (id, name) VALUES (2, 'ROLE_MANAGER');
INSERT INTO role (id, name) VALUES (3, 'ROLE_USER');
INSERT INTO users_roles (user_id, role_id) VALUES (1, 1);
INSERT INTO users_roles (user_id, role_id) VALUES (1, 2);
INSERT INTO users_roles (user_id, role_id) VALUES (2, 1);
INSERT INTO users_roles (user_id, role_id) VALUES (2, 2);
INSERT INTO password_reset_token (id, expiry_date, token, user_id) VALUES (1, '2017-01-01 00:00:00', 'expired-token', 1);
INSERT INTO password_reset_token (id, expiry_date, token, user_id) VALUES (2, '2222-01-01 00:00:00', 'valid-token', 2);
Intercept incoming emails with greenmail
We wrote a custom SmtpServerRule
which uses GreenMail
to intercept emails sent.
package com.memorynotfound.spring.security.test;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import org.junit.rules.ExternalResource;
import javax.mail.internet.MimeMessage;
public class SmtpServerRule extends ExternalResource {
private GreenMail smtpServer;
private int port;
public SmtpServerRule(int port) {
this.port = port;
}
@Override
protected void before() throws Throwable {
super.before();
smtpServer = new GreenMail(new ServerSetup(port, null, "smtp"));
smtpServer.start();
}
public MimeMessage[] getMessages() {
return smtpServer.getReceivedMessages();
}
@Override
protected void after() {
super.after();
smtpServer.stop();
}
}
Password Forgot Integration Test
This integration test validates the forgot password procedures.
package com.memorynotfound.spring.security.test;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import javax.mail.internet.MimeMessage;
import static org.junit.Assert.assertEquals;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@RunWith(SpringJUnit4ClassRunner.class)
public class PasswordForgotIT {
@Autowired
private MockMvc mockMvc;
@Rule
public SmtpServerRule smtpServerRule = new SmtpServerRule(2525);
@Test
public void submitPasswordForgotSuccess() throws Exception {
this.mockMvc
.perform(
post("/forgot-password")
.with(csrf())
.param("email", "[email protected]")
)
.andExpect(model().hasNoErrors())
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/forgot-password?success"));
MimeMessage[] receivedMessages = smtpServerRule.getMessages();
assertEquals(1, receivedMessages.length);
MimeMessage current = receivedMessages[0];
assertEquals("[email protected]", current.getAllRecipients()[0].toString());
}
@Test
public void submitPasswordForgotInvalidEmail() throws Exception {
this.mockMvc
.perform(
post("/forgot-password")
.with(csrf())
.param("email", "[email protected]")
)
.andExpect(model().hasErrors())
.andExpect(model().attributeHasFieldErrors("forgotPasswordForm", "email"))
.andExpect(status().isOk());
}
}
Password Reset Integration Test
This integration test validates the password reset procedures.
package com.memorynotfound.spring.security.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.validation.BindingResult;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
@RunWith(SpringJUnit4ClassRunner.class)
public class PasswordResetIT {
@Autowired
private MockMvc mockMvc;
@Test
public void accessPasswordResetWithoutToken() throws Exception {
this.mockMvc
.perform(
get("/reset-password")
)
.andExpect(model().attributeExists("error"))
.andExpect(status().isOk());
}
@Test
public void accessPasswordResetWithInvalidToken() throws Exception {
this.mockMvc
.perform(
get("/reset-password?token=invalid-token")
)
.andExpect(model().attributeExists("error"))
.andExpect(status().isOk());
}
@Test
public void accessPasswordResetWithExpiredToken() throws Exception {
this.mockMvc
.perform(
get("/reset-password?token=expired-token")
)
.andExpect(model().attributeExists("error"))
.andExpect(status().isOk());
}
@Test
public void submitPasswordResetSuccess() throws Exception {
this.mockMvc
.perform(
post("/reset-password")
.with(csrf())
.param("password", "password")
.param("confirmPassword", "password")
.param("token", "valid-token")
)
.andExpect(model().hasNoErrors())
.andExpect(redirectedUrl("/login?resetSuccess"))
.andExpect(status().is3xxRedirection());
}
@Test
public void submitPasswordResetPasswordDoNotMatch() throws Exception {
this.mockMvc
.perform(
post("/reset-password")
.with(csrf())
.param("password", "password")
.param("confirmPassword", "invalid-password")
.param("token", "valid-token")
)
.andExpect(flash().attributeExists(BindingResult.class.getName() + ".passwordResetForm"))
.andExpect(redirectedUrl("/reset-password?token=valid-token"))
.andExpect(status().is3xxRedirection());
}
}
Integration Test Results
When you run the tests, you receive the following output.
References
- Spring Security Documentation
- Thymeleaf Official Website
- Spring Security User Registration Example
- Spring Boot + Spring Security + Hibernate Configuration
- Adding Static Resources Thymeleaf
- Spring Security Thymeleaf Form Login
thanks a lot for the master tutorial, very helpful at all, success always
Please include UserRegistrationDto to your code or let us see the code . The class is absent.