Featured image for "E-mail confirmation with Spring Security"

E-mail confirmation with Spring Security

March 12th, 2024
6 minute read
Spring Security Spring Spring boot

When registering a new user with an e-mail address, you usually want to verify that the e-mail address of the user is legitimate. The easiest way to do so, is by sending them a mail with a unique link that they have to click in order to fully register.

System design

Project setup

In order to implement this feature, we need a few dependencies:

If you’re using Maven, you can add the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</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>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

Setting up Spring Security

Before we can implement the registration part, we first need to configure Spring Security. One way to implement this is by using a login mechanism like basic authentication or a form login in addition to a PasswordEncoder. For example:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(customizer -> customizer
            .requestMatchers(HttpMethod.POST, "/api/user").anonymous()
            .requestMatchers(HttpMethod.POST, "/api/user/verify").anonymous()
            .anyRequest().authenticated())
        .httpBasic(withDefaults())
        .sessionManagement(withDefaults())
        .build();
}

With this configuration, we’re requiring authentication for all endpoints except two:

  1. POST /api/user: This will be the endpoint to create a new user from.
  2. POST /api/user/verify: This endpoint will be used to verify a user their e-mail address.

The final part to configure Spring Security is to create a UserDetailsService:

@Service
@RequiredArgsConstructor
class SecurityUserDetailsService implements UserDetailsService {
    private final UserEntityRepository repository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return repository
            .findByEmail(email)
            .map(entity -> User
                .builder()
                .username(entity.getEmail())
                .password(entity.getPassword())
                .enabled(entity.isEnabled())
                .build())
            .orElseThrow(() -> new UsernameNotFoundException("No user found for the given e-mail address"));
    }
}

Within this UserDetailsService we have to implement a single method called loadUserByUsername(). This method expects us to return an object of type UserDetails, which has several fields, including:

The way Spring Security will invoke this is by doing the following:

  1. First it will hash the password that was used during login.
  2. Then it will retrieve the UserDetails from the UserDetailsService using the given username.
  3. After that, it will match the given hashed password with the one within the UserDetails.
  4. Finally, it will also check whether the UserDetails is enabled, non-expired, non-locked etc.

Spring Security flow

In our example, we will use three of those fields, being the username (which will be our e-mail address), the password and the enabled flag, which we’ll toggle as soon as the user verifies their e-mail address. Since UserDetails is an interface, we need to create our own class, or use Spring’s User class.

The registration process

Now that the authentication process is ready, we can move to the registration part. The first part is to store the user their information inside the database:

@RestController
@RequestMapping("/api/user")
@RequiredArgConstructor
class UserController {
    private final PasswordEncoder passwordEncoder;

    @PostMapping
    @Transactional
    public void register(RegisterRequestDTO request) {
        UserEntity entity = repository.save(UserEntity.builder()
            .email(request.email())
            .password(passwordEncoder.encode(request.password()))
            .build());
    }
}

Normally we do this by saving the e-mail together with the hashed password inside a database. Important is that the password should be hashed with the same PasswordEncoder that’s being used with Spring Security.

To enable the e-mail verification, we will add two things:

  1. We will add an enabled flag to our user entity and initially disable the user.
  2. We will add a verificationCode field to our entity and generate a random string with Apache Commons.

The result would be something like this:

UserEntity entity = repository.save(UserEntity.builder()
    .email(request.email())
    .password(passwordEncoder.encode(request.password()))
    .enabled(false) // Add this
    .verificationCode(RandomStringUtils.randomAlphanumeric(32)) // Add this
    .build());

Sending the e-mail

The next part of our registration process is to send the e-mail to the user. To do this, we first need to configure our SMTP server. This configuration depends on your SMTP provider, but in my case I will be using MailHog Docker container for development purposes:

spring.mail.host=localhost
spring.mail.port=1025

In addition, I will also set up a Thymeleaf template for our e-mail inside src/main/resources/templates/user-verify.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<body>
<p>Welcome to MyApp XYZ!</p>
<p>Before you can use the application, you first need to verify your account by clicking the following link:</p>
<p><a th:href="${applicationUrl}" th:text="${applicationUrl}"></a></p>
</body>
</html>

This template is rather simple, and contains a single applicationUrl variable.

To generate the e-mail, we will create a new class and autowire JavaMailSender and SpringTemplateEngine into it:

@Service
@RequiredArgConstructor
class UserMailService {
    private final JavaMailSender mailSender;
    private final SpringTemplateEngine templateEngine;
}

The JavaMailSender is automatically created as soon as we provide the right properties, and is a wrapper around the Jakarta Mail specification. SpringTemplateEngine on the other hand is Thymeleaf’s templating engine with certain Spring features enabled.

Within this class, we first need to write a method to render the HTML with Thymeleaf:

private String getVerificationMailContent(UserEntity entity) {
    Context context = new Context();
    String verificationUrl = String.format("http://localhost:8080/verify?code=%s", entity.getVerificationCode());
    context.setVariable("applicationUrl", verificationUrl);
    return templateEngine.process("user-verify", context);
}

In this method, I’m using String.format() to add the generated verification code to a URL. For this demo I’m using a hardcoded URL, but in reality you probably want to put this in a separate property. After that, I’m passing the URL as the applicationUrl variable so that the template can be rendered.

Another method we’ll need is to create the e-mail itself. I will use a MimeMessage here:

private MimeMessage createMessage(UserEntity entity, String content) throws MessagingException {
    MimeMessage mimeMessage = mailSender.createMimeMessage();
    MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
    message.setText(content, true);
    message.setSubject("Welcome to MyApp XYZ");
    message.setFrom("noreply@myapp.xyz");
    message.setTo(entity.getEmail());
    return mimeMessage;
}

And then finally, we’ll create a method to put it all together and send the e-mail:

public void sendVerificationMail(UserEntity entity) {
    String content = getVerificationMailContent(entity);
    try {
        mailSender.send(createMessage(entity, content));
    } catch (MessagingException ex) {
        throw new UserMailFailedException("Could not send e-mail to verify user with e-mail '" + entity.getEmail() + "'", ex);
    }
}

Now we have to change our controller so that it calls the sendVerificationMail() method we just wrote:

@PostMapping
@Transactional
public void register(RegisterRequestDTO request) {
    UserEntity entity = repository.save(UserEntity.builder()
        .email(request.email())
        .password(passwordEncoder.encode(request.password()))
        .enabled(false)
        .verificationCode(RandomStringUtils.randomAlphanumeric(32))
        .build());
    userMailService.sendVerificationMail(entity); // Add this
}

If we now register a new user, we’ll get an e-mail that looks like this:

Example of e-mail within MailHog

Verifying the user

The final step we have to take is to write an endpoint to verify the user. To do this, I added the following controller method:

@PostMapping("/verify")
@Transactional
public void verify(@RequestParam String code) {
    // TODO: implement
}

To implement this, we first need to retrieve the user by their verification code, and then enable the user:

UserEntity entity = repository.findByVerificationCode(code).orElseThrow();
entity.setEnabled(true);
entity.setVerificationCode(null);

And that’s basically it. Now you can write a verification landing page within your application to call this endpoint.