HelloKoding

Practical coding guides

Email Verification Example with Spring Boot, MySQL, and Amazon SES

This tutorial walks you through the steps of creating an Email Verification web application Example with Spring Boot, MySQL, and Amazon AWS SES

What you’ll build

  • Email Verification Form Security Email Verification
  • Send verification email through Amazon SES Security Email Verification
  • Verify email service Security Email Verification

What you’ll need

Stack

  • Spring Boot
  • Java Mail API
  • Spring Data JPA
  • MySQL

Init project structure

You can create and init a new Spring Boot project by using Spring CLI or Spring Initializr. Learn more about using these tools here

The final project structure as below

├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── hellokoding
│       │           └── account
│       │               ├── model
│       │               │   ├── MailProperties.java
│       │               │   ├── User.java
│       │               │   ├── VerificationForm.java
│       │               │   └── VerificationToken.java
│       │               ├── repository
│       │               │   ├── UserRepository.java
│       │               │   └── VerificationTokenRepository.java
│       │               ├── service
│       │               │   ├── SendingMailService.java
│       │               │   └── VerificationTokenService.java
│       │               ├── web
│       │               │   └── AccountController.java
│       │               └── WebApplication.java
│       └── resources
│           ├── static
│           │   ├── css
│           │   │   └── main.css
│           │   └── js
│           │       └── main.js
│           ├── templates
│           │   ├── email-verification.ftl
│           │   └── verification-form.ftl
│           └── application.properties
├── Dockerfile
├── docker-compose.yaml
└── pom.xml

Application dependencies

pom.xml

<?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.hellokoding.springboot</groupId>
    <artifactId>email.verification</artifactId>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </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-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-java8</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>javax.mail</artifactId>
            <version>1.6.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Create JPA Entities

User.java

package com.hellokoding.account.model;

import javax.persistence.*;

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private Boolean isActive;

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
    private VerificationToken verificationToken;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Boolean getIsActive() {
        return isActive;
    }

    public void setIsActive(Boolean isActive) {
        this.isActive = isActive;
    }

    public VerificationToken getVerificationToken() {
        return verificationToken;
    }

    public void setVerificationToken(VerificationToken verificationToken) {
        this.verificationToken = verificationToken;
    }
}

VerificationToken.java

package com.hellokoding.account.model;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
public class VerificationToken {
    public static final String STATUS_PENDING = "PENDING";
    public static final String STATUS_VERIFIED = "VERIFIED";

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String token;
    private String status;
    private LocalDateTime expiredDateTime;
    private LocalDateTime issuedDateTime;
    private LocalDateTime confirmedDateTime;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "user_id")
    private User user;

    public VerificationToken(){
        this.token = UUID.randomUUID().toString();
        this.issuedDateTime = LocalDateTime.now();
        this.expiredDateTime = this.issuedDateTime.plusDays(1);
        this.status = STATUS_PENDING;
    }

    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 String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public LocalDateTime getExpiredDateTime() {
        return expiredDateTime;
    }

    public void setExpiredDateTime(LocalDateTime expiredDateTime) {
        this.expiredDateTime = expiredDateTime;
    }

    public LocalDateTime getIssuedDateTime() {
        return issuedDateTime;
    }

    public void setIssuedDateTime(LocalDateTime issuedDateTime) {
        this.issuedDateTime = issuedDateTime;
    }

    public LocalDateTime getConfirmedDateTime() {
        return confirmedDateTime;
    }

    public void setConfirmedDateTime(LocalDateTime confirmedDateTime) {
        this.confirmedDateTime = confirmedDateTime;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }
}

Create JPA Repositories

UserRepository.java

package com.hellokoding.account.repository;

import com.hellokoding.account.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User, String> {
    List<User> findByEmail(String email);
}

VerificationTokenRepository.java

package com.hellokoding.account.repository;

import com.hellokoding.account.model.VerificationToken;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface VerificationTokenRepository extends JpaRepository<VerificationToken, String> {
    List<VerificationToken> findByUserEmail(String email);
    List<VerificationToken> findByToken(String token);
}

Create Services

SendingMailService.java

package com.hellokoding.account.service;

import com.hellokoding.account.model.MailProperties;
import freemarker.template.Configuration;
import freemarker.template.Template;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

@Service
public class SendingMailService {
    private final MailProperties mailProperties;
    private final Configuration templates;

    @Autowired
    SendingMailService(MailProperties mailProperties, Configuration templates){
        this.mailProperties = mailProperties;
        this.templates = templates;
    }

    public boolean sendVerificationMail(String toEmail, String verificationCode) {
        String subject = "Please verify your email";
        String body = "";
        try {
            Template t = templates.getTemplate("email-verification.ftl");
            Map<String, String> map = new HashMap<>();
            map.put("VERIFICATION_URL", mailProperties.getVerificationapi() + verificationCode);
            body = FreeMarkerTemplateUtils.processTemplateIntoString(t, map);
        } catch (Exception ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, ex.getMessage(), ex);
        }
        return sendMail(toEmail, subject, body);
    }

    private boolean sendMail(String toEmail, String subject, String body) {
        try {
            Properties props = System.getProperties();
            props.put("mail.transport.protocol", "smtp");
            props.put("mail.smtp.port", mailProperties.getSmtp().getPort());
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.auth", "true");

            Session session = Session.getDefaultInstance(props);
            session.setDebug(true);

            MimeMessage msg = new MimeMessage(session);
            msg.setFrom(new InternetAddress(mailProperties.getFrom(), mailProperties.getFromName()));
            msg.setRecipient(Message.RecipientType.TO, new InternetAddress(toEmail));
            msg.setSubject(subject);
            msg.setContent(body, "text/html");

            Transport transport = session.getTransport();
            transport.connect(mailProperties.getSmtp().getHost(), mailProperties.getSmtp().getUsername(), mailProperties.getSmtp().getPassword());
            transport.sendMessage(msg, msg.getAllRecipients());
            return true;
        } catch (Exception ex) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, ex.getMessage(), ex);
        }

        return false;
    }
}

VerificationTokenService.java

package com.hellokoding.account.service;

import com.hellokoding.account.model.User;
import com.hellokoding.account.model.VerificationToken;
import com.hellokoding.account.repository.UserRepository;
import com.hellokoding.account.repository.VerificationTokenRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class VerificationTokenService {
    private UserRepository userRepository;
    private VerificationTokenRepository verificationTokenRepository;
    private SendingMailService sendingMailService;

    @Autowired
    public VerificationTokenService(UserRepository userRepository, VerificationTokenRepository verificationTokenRepository, SendingMailService sendingMailService){
        this.userRepository = userRepository;
        this.verificationTokenRepository = verificationTokenRepository;
        this.sendingMailService = sendingMailService;
    }

    public void createVerification(String email){
        List<User> users = userRepository.findByEmail(email);
        User user;
        if (users.isEmpty()) {
            user = new User();
            user.setEmail(email);
            userRepository.save(user);
        } else {
            user = users.get(0);
        }

        List<VerificationToken> verificationTokens = verificationTokenRepository.findByUserEmail(email);
        VerificationToken verificationToken;
        if (verificationTokens.isEmpty()) {
            verificationToken = new VerificationToken();
            verificationToken.setUser(user);
            verificationTokenRepository.save(verificationToken);
        } else {
            verificationToken = verificationTokens.get(0);
        }

        sendingMailService.sendVerificationMail(email, verificationToken.getToken());
    }

    public ResponseEntity<String> verifyEmail(String token){
        List<VerificationToken> verificationTokens = verificationTokenRepository.findByToken(token);
        if (verificationTokens.isEmpty()) {
            return ResponseEntity.badRequest().body("Invalid token.");
        }

        VerificationToken verificationToken = verificationTokens.get(0);
        if (verificationToken.getExpiredDateTime().isBefore(LocalDateTime.now())) {
            return ResponseEntity.unprocessableEntity().body("Expired token.");
        }

        verificationToken.setConfirmedDateTime(LocalDateTime.now());
        verificationToken.setStatus(VerificationToken.STATUS_VERIFIED);
        verificationToken.getUser().setIsActive(true);
        verificationTokenRepository.save(verificationToken);

        return ResponseEntity.ok("You have successfully verified your email address.");
    }
}

Create Controllers

AccountController.java

package com.hellokoding.account.web;

import com.hellokoding.account.model.VerificationForm;
import com.hellokoding.account.service.VerificationTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.validation.Valid;

@Controller
public class AccountController {
    @Autowired
    VerificationTokenService verificationTokenService;

    @GetMapping("/")
    public String index() {
        return "redirect:/email-verification";
    }

    @GetMapping("/email-verification")
    public String formGet(Model model) {
        model.addAttribute("verificationForm", new VerificationForm());
        return "verification-form";
    }

    @PostMapping("/email-verification")
    public String formPost(@Valid VerificationForm verificationForm, BindingResult bindingResult, Model model) {
        if (!bindingResult.hasErrors()) {
            model.addAttribute("noErrors", true);
        }
        model.addAttribute("verificationForm", verificationForm);

        verificationTokenService.createVerification(verificationForm.getEmail());
        return "verification-form";
    }

    @GetMapping("/verify-email")
    @ResponseBody
    public String verifyEmail(String code) {
        return verificationTokenService.verifyEmail(code).getBody();
    }
}

Create FreeMarker View template

verification-form.ftl

<#import "/spring.ftl" as spring />

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Activate account with Spring Boot, MongoDB, NGINX, Docker Compose</title>
</head>
<body>
    <h2>Verify your email</h2>

    <@spring.bind "verificationForm"/>
    <#if verificationForm?? && noErrors??>
    Sent a confirmation link to your inbox ${verificationForm.email}<br>
    <#else>
    <form action="/email-verification" method="post">
        Email:<br>
        <@spring.formInput "verificationForm.email"/>
        <@spring.showErrors "<br>"/>
        <br><br>
        <input type="submit" value="Submit">
    </form>
    </#if>
</body>
</html>

email-verification.ftl

Hi Giau,<br/>

Thanks for using Hello Koding! Please confirm your email address by clicking on the link below.<br/>

${VERIFICATION_URL}<br/>

If you did not sign up for a Hello Koding account please disregard this email.<br/>

The Hello Koding team,

Application Configuration

WebApplication.java

package com.hellokoding.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

application.properties

spring.freemarker.template-loader-path: classpath:/templates
spring.freemarker.suffix: .ftl

spring.datasource.url=jdbc:mysql://hk-mysql:3306/test?useSSL=false
spring.datasource.username=root
spring.datasource.password=hellokoding
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.initialization-mode=always
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect
spring.jpa.generate-ddl=true
spring.jpa.show-sql=true

mail.smtp.host={YOUR_SES_SMTP_HOST}
mail.smtp.port={YOUR_SES_SMTP_PORT}
mail.smtp.username={YOUR_SES_SMTP_USERNAME}
mail.smtp.password={YOUR_SES_SMTP_PASSWORD}
mail.from={YOUR_FROM_EMAIL}
mail.from-name={YOUR_FROM_NAME}

mail.verificationapi=http://localhost:8080/verify-email?code=

Run with Docker

Prepare Dockerfile for Java/Spring Boot application and docker-compose.yml for MySQL Server

Dockerfile

FROM maven:3.5-jdk-8

docker-compose.yaml

version: '3'
services:
  hk-mysql:
    container_name: hk-mysql
    image: mysql/mysql-server:5.7
    environment:
      MYSQL_DATABASE: test
      MYSQL_ROOT_PASSWORD: hellokoding
      MYSQL_ROOT_HOST: '%'
    ports:
    - "3306:3306"
    restart: always

  app:
    build: .
    volumes:
    - .:/app
    - ~/.m2:/root/.m2
    working_dir: /app
    ports:
    - "8080:8080"
    command: mvn clean spring-boot:run
    depends_on:
    - hk-mysql

Type the below command at the project root directory, make sure your local Docker is running

docker-compose up

Run with JDK/OpenJDK, Maven and MySQL Server local

  • On application.properties, update data source user name + password to your local configs, update data source url hk-mysql to localhost
  • Then type this command at the project root directory mvn clean spring-boot:run

Test

Open your browser and access to http://localhost:8080

Source code

https://github.com/hellokoding/hellokoding-courses/tree/master/springboot-examples/springboot-email-verification-mysql

Follow HelloKoding