Spring Security: Email Verification Registration

Overview

The first action a customer takes after visiting a website is creating an account, usually to place an order, book an appointment, pay for a service, etc. When creating an account it is important to persist the correct email address in the system and verify the user's ownership.

A common and effective strategy to do this is to send a confirmation link to the user's email post registration. Once the user clicks on the unique link, their account gets activated and they can undertake further actions on the website.

Spring allows us to implement this functionality easily, which is exactly what we'll be doing in this article.

Project Setup

As always, it's easiest to start off with a pre-configured Spring Boot project using Spring Initializr. Select dependencies for Web, Security, Mail, JPA, Thymeleaf and MySQL and generate the project:

spring_initializr

We'll be using Spring Security and Spring MVC for this project. For the Data Layer, we'll be using Spring Data as it already provides us with CRUD operations for a given entity and dynamic query derivation from repository method names.

Additionally, we'll be using Hibernate as the JPA provider and a MySQL database.

If you're interested in reading more about JPA, we've got it covered here: A Guide to Spring Data JPA.

Dependencies

Let's take a look at the dependencies in the pom.xml file, which imports all the required libraries as per the description above:

<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>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </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-mail</artifactId>
    </dependency>
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
</dependencies>

Now, with the project all set-up, we can get down coding!

Implementation

Spring Properties

Let's start off by configuring the Spring properties in application.properties:

server.port = 8082
logging.level.org.springframework = WARN
logging.level.org.hibernate = WARN
logging.level.com.springsecurity.demo = DEBUG

####### Data-Source Properties #######
spring.datasource.url = jdbc:mysql://localhost:3306/demodb?useSSL=false
spring.datasource.username = username
spring.datasource.password = password
spring.datasource.driver-class-name = com.mysql.jdbc.Driver

###### JPA Properties ######
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
spring.jpa.generate-ddl = true
spring.jpa.show-sql = true

###### Email Properties ######
spring.mail.host = smtp.gmail.com
spring.mail.port = 587
spring.mail.properties.mail.smtp.starttls.enable = true
spring.mail.username = [email protected]
spring.mail.password = examplepassword
spring.mail.properties.mail.smtp.starttls.required = true
spring.mail.properties.mail.smtp.auth = true
spring.mail.properties.mail.smtp.connectiontimeout = 5000
spring.mail.properties.mail.smtp.timeout = 5000
spring.mail.properties.mail.smtp.writetimeout = 5000

We are using the Gmail SMTP server for this example. I am running my tomcat server on port 8082.

Make sure to provide the correct MySQL and email account credentials as per your system. With the JPA properties set up we can start with our business logic.

Configuring JPA

We have two models for this application - User and ConfirmationToken:

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="user_id")
    private long userid;

    private String emailId;

    private String password;

    @Column(name="first_name")
    private String firstName;

    @Column(name="last_name")
    private String lastName;

    private boolean isEnabled;

    // getters and setters
}

A simple POJO class annotated with the standard Spring annotations.

Now let's move on to the second model:

@Entity
public class ConfirmationToken {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="token_id")
    private long tokenid;

    @Column(name="confirmation_token")
    private String confirmationToken;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
    @JoinColumn(nullable = false, name = "user_id")
    private User user;

    public ConfirmationToken(User user) {
        this.user = user;
        createdDate = new Date();
        confirmationToken = UUID.randomUUID().toString();
    }

    // getters and setters
}

ConfirmationToken has a one-to-many relationship with the User entity. Since we set jpa.generate-ddl to true, Hibernate creates the table schema as per the above entities.

Primary keys in both the tables is set to auto-increment because we have annotated the ID columns in both classes with @Generated(strategy = GenerationType.AUTO).

Here is how the generated schema looks like in the database:

database_schema

Now that the JPA configuration is done we will proceed to write the Data Access Layer. For that we will be using Spring Data as it provides basic CRUD operations out of the box which will be enough for the sake of this example.

Also, when using Spring Data it frees the code of the boiler-plate like getting the entity-manager or getting sessions, etc:

@Repository("userRepository")
public interface UserRepository extends CrudRepository<User, String> {
    User findByEmailIdIgnoreCase(String emailId);
}

Spring Data automatically provides implementation for querying databases on the basis of an attribute, provided we follow Java Bean specifications. For an example, in our POJO, we have emailId as a bean property and we want to find the User by that property irrespective of the case.

Similarly, we implement the repository for the ConfirmationToken as well:

public interface ConfirmationTokenRepository extends CrudRepository<ConfirmationToken, String> {
    ConfirmationToken findByConfirmationToken(String confirmationToken);
}

A common approach many developers take when using Spring Data is to use the basic CRUD repository provided in another service class and expose the methods from that class.

This keeps the application code loosely coupled with the Spring libraries.

Email Service

Once the user completes registration, we need to send an email to the user's email address. We will be using Spring Mail API to achieve that functionality.

We have added the configuration properties for this in the property file shown earlier, so we can proceed with defining an email service:

@Service("emailSenderService")
public class EmailSenderService {

    private JavaMailSender javaMailSender;

    @Autowired
    public EmailSenderService(JavaMailSender javaMailSender) {
        this.javaMailSender = javaMailSender;
    }

    @Async
    public void sendEmail(SimpleMailMessage email) {
        javaMailSender.send(email);
    }
}

We have annotated the class with @Service which is a variant of the @Component annotation. This allows Spring Boot to discover the service and register it for use.

Controller and View

Now we have all the services ready for our example and we can proceed with writing the UserAccountController:

@Controller
public class UserAccountController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ConfirmationTokenRepository confirmationTokenRepository;

    @Autowired
    private EmailSenderService emailSenderService;

    @RequestMapping(value="/register", method = RequestMethod.GET)
    public ModelAndView displayRegistration(ModelAndView modelAndView, User user)
    {
        modelAndView.addObject("user", user);
        modelAndView.setViewName("register");
        return modelAndView;
    }

    @RequestMapping(value="/register", method = RequestMethod.POST)
    public ModelAndView registerUser(ModelAndView modelAndView, User user)
    {

        User existingUser = userRepository.findByEmailIdIgnoreCase(user.getEmailId());
        if(existingUser != null)
        {
            modelAndView.addObject("message","This email already exists!");
            modelAndView.setViewName("error");
        }
        else
        {
            userRepository.save(user);

            ConfirmationToken confirmationToken = new ConfirmationToken(user);

            confirmationTokenRepository.save(confirmationToken);

            SimpleMailMessage mailMessage = new SimpleMailMessage();
            mailMessage.setTo(user.getEmailId());
            mailMessage.setSubject("Complete Registration!");
            mailMessage.setFrom("[email protected]");
            mailMessage.setText("To confirm your account, please click here : "
            +"http://localhost:8082/confirm-account?token="+confirmationToken.getConfirmationToken());

            emailSenderService.sendEmail(mailMessage);

            modelAndView.addObject("emailId", user.getEmailId());

            modelAndView.setViewName("successfulRegisteration");
        }

        return modelAndView;
    }

    @RequestMapping(value="/confirm-account", method= {RequestMethod.GET, RequestMethod.POST})
    public ModelAndView confirmUserAccount(ModelAndView modelAndView, @RequestParam("token")String confirmationToken)
    {
        ConfirmationToken token = confirmationTokenRepository.findByConfirmationToken(confirmationToken);

        if(token != null)
        {
            User user = userRepository.findByEmailIdIgnoreCase(token.getUser().getEmailId());
            user.setEnabled(true);
            userRepository.save(user);
            modelAndView.setViewName("accountVerified");
        }
        else
        {
            modelAndView.addObject("message","The link is invalid or broken!");
            modelAndView.setViewName("error");
        }

        return modelAndView;
    }
    // getters and setters
}

Let's take a look at the methods in the controller, what they do and which views do they return.

displayRegistration() - Starting point for the user in our application. As soon as the user opens our application, they are displayed the registration page via this method.

We've also added the user object to the view. The <form> tag on the registration page also includes this object and we'll use the fields in the form to populate the fields of the object.

This object, with the filled in information, will then be persisted in the database.

Here's how it looks like on the page:

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Register</title>
    </head>
    <body>
        <form action="#" th:action="@{/register}" th:object="${user}" method="post">
            <table>
                <tr>
                    <td><label for="firstName">First Name</label></td>
                    <td><input th:field="*{firstName}" type="text" name="firstName"></input></td>
                </tr>
                <tr>
                    <td><label for="lastName">Last Name</label></td>
                    <td><input th:field="*{lastName}" type="text" name="lastName"></input></td>
                </tr>
                <tr>
                    <td><label for="emailId">Email</label></td>
                    <td><input th:field="*{emailId}" type="text" name="emailId"></input></td>
                </tr>
                <tr>
                    <td><label for="password">Password</label></td>
                    <td><input th:field="*{password}" type="password" name="password"></input></td>
                </tr>
                <tr>
                    <td><input type="reset" value="Clear"/></td>
                    <td><input type="submit" value="Submit"></input></td>
                </tr>
            </table>
        </form>
    </body>
</html>

registerUser() - Accepts the user details entered on the registration page. Spring MVC automatically makes the user input available to us in the method.

We save the user details in the user table and create a random confirmation token. The token is saved with the user's emailId in the confirmation_token table, and sent via a URL to the user's email for verification.

The user is displayed a successful registration page:

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Registration Success</title>
    </head>
    <body>
        <center>
            <span th:text="'A verification email has been sent to: ' + ${emailId}"></span>
        </center>
    </body>
</html>

Lastly, once the URL from the email is accessed, the confirmUserAccount() method is called.

This method validates the token which should not be empty and should exist in the database, else the user is displayed an error page (error.html):

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Registration Success</title>
    </head>
    <body>
        <center>
            <span th:text="${message}"></span>
        </center>
    </body>
</html>

If there are no validation issues, the account associated with the token is verified. The user is displayed a successful activation message:

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Congratulations!</title>
    </head>
    <body>
        <center>
            <h3>Congratulations! Your account has been activated and email is verified!</h3>
        </center>
    </body>
</html>

Spring Security Configuration

Let's now configure the Spring Security module to secure our web-application. We need to make sure that no authentication is required for /register and /confirm URLs as they are the landing pages for a new user:

@Configuration
@EnableWebSecurity
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/register").permitAll()
                .antMatchers("/confirm").permitAll();
        }
}

Lastly, we'll need a main method as the starting point for our Spring Boot application:

@SpringBootApplication
public class RunApplication {
    public static void main(String[] args) {
        SpringApplication.run(RunApplication.class, args);
    }
}

The @SpringBootApplication annotation directs Spring Boot to load all the configuration and component classes and also enables auto-configuration. This is one of the great features of Spring Boot, we can run it using a simple main method.

Running the Application

We begin the testing by selecting and running the RunApplication.java. This starts the embedded Tomcat server on port 8082 and our application gets deployed.

Next, let's open the browser and access our application:

registration

Upon entering the required information, the user will submit:

register success

Upon submission, an email is sent to the user in order to verify the email:

email

Following the link, the account is verified using a unique token, and the user is redirected to the success page:

account verified

This is how it looks like in the database:

Database table view

Conclusion

Email verification is a very common requirement for web applications. Almost any kind of registration requires a verified email account, especially if the user enters any kind of sensitive information into the system.

In this article, we wrote a simple Spring Boot application to generate unique tokens for new users, send them an email, and verify them in our system.

If you're interested in playing with the source code, as always, it's available on GitHub