The source code of this project, is hosted on Github – Backend – https://github.com/caseyscottmckay/sprinit-backend Frontend – https://github.com/caseyscottmckay/sprinit-frontend So let’s start building the backend of this application, the first thing we have to do, go to Spring Initializr website. - Enter spring-reddit-clone in Artifact field - Add these dependencies – Lombok, Spring Web, Spring Security, Spring Data JPA, MySQL Java Driver, Java Mail Sender - Click on the Generate button, and the project will be downloaded.

Additional Dependencies

Open the pom.xml file and add the below additional dependencies which are needed to implement the JWT authentication and other functionalities like dynamically displaying the relative duration ( like “Posted 1 day ago”).

<!-- JWT related dependencies-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.10.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <scope>runtime</scope>
        <version>0.10.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <scope>runtime</scope>
        <version>0.10.5</version>
    </dependency>
    <!-- For Displaying time as Relative Time Ago ("Posted 1 Day ago"),
     as this is a Kotlin library, we also need Kotlin runtime libraries-->
    <dependency>
        <groupId>com.github.marlonlom</groupId>
        <artifactId>timeago</artifactId>
        <version>4.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib-jdk8</artifactId>
        <version>${kotlin.version}</version>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-test</artifactId>
        <version>${kotlin.version}</version>
        <scope>test</scope>
    </dependency>

Configure Database, Hibernate and Java Mail Properties

Let’s now configure the MySQL Database, Hibernate JPA and Java Mail functionality in our application by adding the following properties to src/main/resources/application.properties file

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring-reddit-clone?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false
spring.datasource.username=<your-db-username>
spring.datasource.password=<your-db-password>
spring.jpa.hibernate.ddl-auto=update
spring.datasource.initialize=true
spring.jpa.show-sql=true
spring.mail.host=smtp.mailtrap.io
spring.mail.port=25
spring.mail.username=<your-username>
spring.mail.password=<your-password>
spring.mail.protocol=smtp

In our application, we will be sending Account Activation Emails and Comment Notification Emails to the users, for that reason we need an SMTP server to send the emails, we can use a Fake SMTP Server called as MailTrap for this application, you can signup to Mailtrap using your Github or Google account.

Database Schema Diagram

Here is the DB Schema Diagram Database Schema

Creating Domain Entities

Now let’s create domain entities for our Reddit Clone Application. In our application, we have Users, who can create Subreddits and Posts, other users can add Comments on the Posts, and can Vote.

Note that, we are using Lombok Annotations like @Data, @AllArgsConstructor, and @NoArgsConstructor. These annotations will generate the corresponding Getters/Setters/Equals and HashCode/toString methods and Constructors at compile time. To be able to use these annotations, you have to enable Annotation Processing in your IDE. For more details check this link on how to enable Lombok in your favorite IDE – https://www.baeldung.com/lombok-ide

User Entity

package com.programming.techie.springredditclone.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.time.Instant;

import static javax.persistence.GenerationType.SEQUENCE;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long userId;
    @NotBlank(message = "Username is required")
    private String username;
    @NotBlank(message = "Password is required")
    private String password;
    @Email
    @NotEmpty(message = "Email is required")
    private String email;
    private Instant created;
    private boolean enabled;
}

Post Entity

package us.csmckay.springit.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.lang.Nullable;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.time.Instant;

import static javax.persistence.FetchType.LAZY;
import static javax.persistence.GenerationType.SEQUENCE;

@Data
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long postId;
    @NotBlank(message = "Post Name cannot be empty or Null")
    private String postName;
    @Nullable
    private String url;
    @Nullable
    @Lob
    private String description;
    private Integer voteCount;
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "userId", referencedColumnName = "userId")
    private User user;
    private Instant createdDate;
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "id", referencedColumnName = "id")
    private Subreddit subreddit;
}

Subreddit Entity

package us.csmckay.springit.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import java.time.Instant;
import java.util.List;

import static javax.persistence.FetchType.LAZY;
import static javax.persistence.GenerationType.SEQUENCE;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Builder
public class Subreddit {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long id;
    @NotBlank(message = "Community name is required")
    private String name;
    @NotBlank(message = "Description is required")
    private String description;
    @OneToMany(fetch = LAZY)
    private List<Post> posts;
    private Instant createdDate;
    @ManyToOne(fetch = LAZY)
    private User user;
}

Vote Entity

package us.csmckay.springit.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.NotNull;

import static javax.persistence.FetchType.LAZY;
import static javax.persistence.GenerationType.SEQUENCE;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Builder
public class Vote {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long voteId;
    private VoteType voteType;
    @NotNull
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "postId", referencedColumnName = "postId")
    private Post post;
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "userId", referencedColumnName = "userId")
    private User user;
}

Comment Entity

package us.csmckay.springit.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import java.time.Instant;

import static javax.persistence.FetchType.LAZY;
import static javax.persistence.GenerationType.SEQUENCE;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long id;
    @NotEmpty
    private String text;
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "postId", referencedColumnName = "postId")
    private Post post;
    private Instant createdDate;
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "userId", referencedColumnName = "userId")
    private User user;
}

VoteType enum

package us.csmckay.springit.model;

public enum VoteType {
    UPVOTE(1), DOWNVOTE(-1),
    ;

    VoteType(int direction) {
    }
}

VerificationToken.java

package us.csmckay.springit.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.Instant;

import static javax.persistence.FetchType.LAZY;
import static javax.persistence.GenerationType.IDENTITY;
import static javax.persistence.GenerationType.SEQUENCE;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "token")
public class VerificationToken {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;
    private String token;
    @OneToOne(fetch = LAZY)
    private User user;
    private Instant expiryDate;
}

NotificationEmail.java

package us.csmckay.springit.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class NotificationEmail {
    private String subject;
    private String recipient;
    private String body;

}

Configure Repositories

So these are our domain entities, all of the fields are pretty self-explanatory. Now let’s create the repositories which are responsible to store these entities in the database. Create the package repository and create below repositories.

User Repository

package us.csmckay.springit.repository;

import us.csmckay.springit.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

Post Repository

package us.csmckay.springit.repository;

import us.csmckay.springit.model.Post;
import us.csmckay.springit.model.Subreddit;
import us.csmckay.springit.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface PostRepository extends JpaRepository<Post, Long> {
    List<Post> findAllBySubreddit(Subreddit subreddit);

    List<Post> findByUser(User user);
}

Comment Repository

package us.csmckay.springit.repository;
import us.csmckay.springit.model.Comment;
import us.csmckay.springit.model.Post;
import us.csmckay.springit.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {
    List<Comment> findByPost(Post post);

    List<Comment> findAllByUser(User user);
}

Subreddit Repository

package us.csmckay.springit.repository;

import us.csmckay.springit.model.Subreddit;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface SubredditRepository extends JpaRepository<Subreddit, Long> {
    Optional<Subreddit> findByName(String subredditName);
}

Vote Repository

package us.csmckay.springit.repository;

import us.csmckay.springit.model.Post;
import us.csmckay.springit.model.User;
import us.csmckay.springit.model.Vote;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface VoteRepository extends JpaRepository<Vote, Long> {
    Optional<Vote> findTopByPostAndUserOrderByVoteIdDesc(Post post, User currentUser);
}

VerificationToken Repository

package us.csmckay.springit.repository;

import us.csmckay.springit.model.VerificationToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface VerificationTokenRepository extends JpaRepository<VerificationToken, Long> {
    Optional<VerificationToken> findByToken(String token);
}

Explore the project structure and Running the application After creating all the models and repositories, the project structure looks like a Spring project should look. Now let’s run the application by typing the below command by using the maven wrapper in the project. ./mvnw spring-boot:run Now you can check the logs and you should see your Spring Application starting on a tomcat webserver (go to localhost:8080/api).

The next steps are as follows

  1. First, we will see how to setup Spring Security in our application
  2. Then, we will build an API to register users in our application
  3. We will see how to encode the password of the user, before storing them in the database.
  4. We will send account activation emails to the user.
  5. Build an API to verify the users and enable them.

Configure Spring Security Let’s create the following class under the new package called as config, this class holds the complete security configuration of our application, that’s why the class is named SecurityConfig.java.

package us.csmckay.springit.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/auth/__")
                .permitAll()
                .anyRequest()
                .authenticated();
    }

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

Let’s understand what did we configure in the above class:

  • @EnableWebSecurity
    • This is the main annotation which enables the Web Security module in our Project.
  • WebSecurityConfigurerAdapter
    • This is the base class for our SecurityConfig class, it provides us the default security configurations, which we can override in our SecurityConfig and customize them.
  • Configurations
    • Next, we have the configure method which we have overridden from the base class which takes HttpSecurity as an argument.
    • Here, we are configuring Spring to allow all the requests which match the endpoint “/api/auth/__” , as these endpoints are used for authentication and registration we don’t expect the user to be authenticated at that point of time.
  • PasswordEncoder
    • Now before storing the user in the database, we ideally want to encode the passwords. One of the best hashing algorithms for passwords is the Bcrypt Algorithm. We are using the BCryptPasswordEncoder class provided by Spring Security. You can read more about this here

Now that is the minimum configuration we need to implement the Registration Functionality, in the upcoming blog posts, we will add more functionality to our SecurityConfig class as we implement the Login functionality using JWT. Let’s write the API for registering users.

Implement API to Register Users

So to register users, first of all, we need a Controller class, I will create it inside the following package: us.csmckay.springit.controller. I will name our class file as AuthController.java

AuthController.java

package us.csmckay.springit.controller;

import us.csmckay.springit.dto.RegisterRequest;
import us.csmckay.springit.service.AuthService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.HttpStatus.OK;

@RestController
@RequestMapping("/api/auth")
@AllArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping("/signup")
    public ResponseEntity signup(@RequestBody RegisterRequest registerRequest) {
        authService.signup(registerRequest);
        return new ResponseEntity(OK);
    }
}

So we have created a RestController and inside this controller, we first created a method that will be invoked whenever a POST request is made to register the user’s in our application.

The API call should contain the request body which is of type RegisterRequest. Through this class we are transferring the user details like username, password and email as part of the RequestBody. We call this kind of classes as a DTO (Data Transfer Object). We will create this class inside a different package called us.csmckay.springit.dto

RegisterRequest.java

package us.csmckay.springit.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
    private String username;
    private String email;
    private String password;
}

The signup method inside the AuthController is calling another method inside the AuthService class, which is mainly responsible to create the User object and storing it in the database. Now let’s create this AuthService class inside the package us.csmckay.springit.service.

AuthService.java

package us.csmckay.springit.service;

import us.csmckay.springit.dto.RegisterRequest;
import us.csmckay.springit.model.User;
import us.csmckay.springit.repository.UserRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static java.time.Instant.now;

@Service
@AllArgsConstructor
@Slf4j
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public void signup(RegisterRequest registerRequest) {
        User user = new User();
        user.setUsername(registerRequest.getUsername());
        user.setEmail(registerRequest.getEmail());
        user.setPassword(encodePassword(registerRequest.getPassword()));
        user.setCreated(now());
        user.setEnabled(false);

        userRepository.save(user);
    }

    private String encodePassword(String password) {
        return passwordEncoder.encode(password);
    }
}

Inside the AuthService class, we are mapping the RegisterRequest object to the User object and when setting the password, we are calling the encodePassword() method. This method is using the BCryptPasswordEncoder to encode our password. After that, we save the user into the database. Note that we are setting the enabled flag as false, as we want to disable the user after registration, and we only enable the user after verifying the user’s email address.

Activating new account via email

This brings us to the next section. Now, let us enhance the registration process by only allowing the user to log in after they verify their email. We will generate a verification token, right after we save the user to the database and send that token as part of the verification email. Once the user is verified, then we enable the user to login to our application.

Let us enhance our AuthService.java by adding the logic to generate a verification token. This is how our class looks like:

package us.csmckay.springit.service;

import us.csmckay.springit.dto.RegisterRequest;
import us.csmckay.springit.model.User;
import us.csmckay.springit.model.VerificationToken;
import us.csmckay.springit.repository.UserRepository;
import us.csmckay.springit.repository.VerificationTokenRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

import static java.time.Instant.now;

@Service
@AllArgsConstructor
@Slf4j
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final VerificationTokenRepository verificationTokenRepository;

    @Transactional
    public void signup(RegisterRequest registerRequest) {
        User user = new User();
        user.setUsername(registerRequest.getUsername());
        user.setEmail(registerRequest.getEmail());
        user.setPassword(encodePassword(registerRequest.getPassword()));
        user.setCreated(now());
        user.setEnabled(false);

        userRepository.save(user);

        String token = generateVerificationToken(user);
    }

    private String generateVerificationToken(User user) {
        String token = UUID.randomUUID().toString();
        VerificationToken verificationToken = new VerificationToken();
        verificationToken.setToken(token);
        verificationToken.setUser(user);
        verificationTokenRepository.save(verificationToken);
        return token;
    }

    private String encodePassword(String password) {
        return passwordEncoder.encode(password);
    }
}

We added the generateVerificationToken() method and calling that method right after we saved the user into UserRepository. Note that, we are creating a random UUID as our token, creating an object for VerificationToken, fill in the data for that object and save it into the VerificationTokenRepository. As we have the token, now its time to send an email that contains this verification token.

We need to add some additional dependencies to our project, if we want to send HTML emails from our application, Thymeleaf provides us the template engine, which we can use to create HTML templates and use those templates to send the emails. Let’s add the below thymeleaf dependency to our pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Let’s add the below classes which contain the logic to build the content of our email and send the email to the user.

MailContentBuilder.java

This class contains the logic to create our email message using the HTML template we are going to provide.

package us.csmckay.springit.service;

import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Service
@AllArgsConstructor
class MailContentBuilder {

    private final TemplateEngine templateEngine;

    String build(String message) {
        Context context = new Context();
        context.setVariable("message", message);
        return templateEngine.process("mailTemplate", context);
    }
}

MailContentBuilder.java contains the method build() which takes our email message as input and it uses the Thymeleaf‘s TemplateEngine to generate the email message. Note that we gave the name mailTemplate as an argument to the method call templateEngine.process(“mailTemplate”, context); That would be the name of the html template which looks like below:

mailTemplate.html

Create this html file under src/main/resources/templates

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

We are injecting the email message into the HTML template by setting the message into the Context of the TemplateEngine.

Context context = new Context();
context.setVariable("message", message);

Let’s generate the email message and call the build() method of the MailContentBuilder.java class from AuthService.

String message = mailContentBuilder.build("Thank you for signing up to Spring Reddit, please click on the below url to activate your account : "
                + ACTIVATION_EMAIL + "/" + token);`

For the activation URL, as we are using this from our local machines let’s provide the URL as – “http://localhost:8080/api/auth/accountVerification”. Let us create this value as a constant ACTIVATION_EMAIL. Let’s create a separate class to maintain all these constants in our application in one place.

Constants.java

package com.programming.techie.springredditclone.util;

import lombok.experimental.UtilityClass;

@UtilityClass
public class Constants {
    public static final String ACTIVATION_EMAIL = "http://localhost:8080/api/auth/accountVerification";
}

Note that, we are creating this class inside the package com.programming.techie.springredditclone.util.

As this is a Utility Class, we have annotated this class with @UtilityClass which is a Lombok annotation, this annotation will make the following changes at compile time to our class: - Marks the class as final. - It generates a private no-arg constructor. - It only allows the methods or fields to be static.

A Utility class, by definition, should not contain any state. Hence it is usual to put shared constants or methods inside utility class so that they can be reused. As they are shared and not tied to any specific object it makes sense to mark them as static.

We have generated our email message, now its time to send this email to the user.

Using MailTrap to send emails In this application, we will use a Fake SMTP server called MailTrap to check whether our email functionality is working or not. Create an account in Mailtrap and after registration, you should get the following details, which you can use to send emails in our application.

Host:smtp.mailtrap.io
Port:25 or 465 or 587 or 2525
Username:<your-username>
Password:<your-password>
Auth:PLAIN, LOGIN and CRAM-MD5
TLS:Optional (STARTTLS on all ports)

The above details can be configured in the application.properties file like below:

spring.mail.host=smtp.mailtrap.io
spring.mail.port=25
spring.mail.username=<your-username>
spring.mail.password=<your-password>
spring.mail.protocol=smtp

Now let’s create the below class called MailService, to send out the emails.

package com.programming.techie.springredditclone.service;

import com.programming.techie.springredditclone.exception.SpringRedditException;
import com.programming.techie.springredditclone.model.NotificationEmail;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
@Slf4j
class MailService {

    private final JavaMailSender mailSender;
    private final MailContentBuilder mailContentBuilder;

    void sendMail(NotificationEmail notificationEmail) {
        MimeMessagePreparator messagePreparator = mimeMessage -> {
            MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
            messageHelper.setFrom("springreddit@email.com");
            messageHelper.setTo(notificationEmail.getRecipient());
            messageHelper.setSubject(notificationEmail.getSubject());
            messageHelper.setText(mailContentBuilder.build(notificationEmail.getBody()));
        };
        try {
            mailSender.send(messagePreparator);
            log.info("Activation email sent!!");
        } catch (MailException e) {
            throw new SpringRedditException("Exception occurred when sending mail to " + notificationEmail.getRecipient());
        }
    }

}

Let us understand what we are doing in this class, so we have our sendMail method which takes NotificationEmail as input, and inside the method we are creating a MimeMessage by passing in the sender, recipient, subject and body fields. The message body we are receiving from the build() method of our MailContentBuilder class.

Once the email message is prepared, we send the email message. If there are any unexpected exceptions raised during sending the email, we are catching those exceptions and rethrowing them as custom exceptions. This is a good practice as we don’t expose the internal technical exception details to the user, by creating custom exceptions, we can create our own error messages and provide them to the users. Let’s create our custom exception SpringRedditException class inside the us.csmckay.springit.exception package:

package us.csmckay.springit.exception;

public class SpringRedditException extends RuntimeException {
    public SpringRedditException(String message) {
        super(message);
    }
}

Finally, let’s call the sendMail method from our signup method inside the AuthService class.

mailService.sendMail(new NotificationEmail("Please Activate your account", user.getEmail(), message));

At the end this is how our AuthService class should look like:

package us.csmckay.springit.service;
import us.csmckay.springit.dto.RegisterRequest;
import us.csmckay.springit.exception.SpringRedditException;
import us.csmckay.springit.model.NotificationEmail;
import us.csmckay.springit.model.User;
import us.csmckay.springit.model.VerificationToken;
import us.csmckay.springit.repository.UserRepository;
import us.csmckay.springit.repository.VerificationTokenRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import java.util.UUID;
import static us.csmckay.springit.util.Constants.ACTIVATION_EMAIL;
import static java.time.Instant.now;
@Service
@AllArgsConstructor
@Slf4j
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final VerificationTokenRepository verificationTokenRepository;
private final MailContentBuilder mailContentBuilder;
private final MailService mailService;
@Transactional
public void signup(RegisterRequest registerRequest) {
User user = new User();
user.setUsername(registerRequest.getUsername());
user.setEmail(registerRequest.getEmail());
user.setPassword(encodePassword(registerRequest.getPassword()));
user.setCreated(now());
user.setEnabled(false);
userRepository.save(user);
String token = generateVerificationToken(user);
String message = mailContentBuilder.build("Thank you for signing up to Spring Reddit, please click on the below url to activate your account : "
+ ACTIVATION_EMAIL + "/" + token);
mailService.sendMail(new NotificationEmail("Please Activate your account", user.getEmail(), message));
}
private String generateVerificationToken(User user) {
String token = UUID.randomUUID().toString();
VerificationToken verificationToken = new VerificationToken();
verificationToken.setToken(token);
verificationToken.setUser(user);
verificationTokenRepository.save(verificationToken);
return token;
}
private String encodePassword(String password) {
return passwordEncoder.encode(password);
}
public void verifyAccount(String token) {
Optional<VerificationToken> verificationTokenOptional = verificationTokenRepository.findByToken(token);
verificationTokenOptional.orElseThrow(() -> new SpringRedditException("Invalid Token"));
fetchUserAndEnable(verificationTokenOptional.get());
}
@Transactional
private void fetchUserAndEnable(VerificationToken verificationToken) {
String username = verificationToken.getUser().getUsername();
User user = userRepository.findByUsername(username).orElseThrow(() -> new SpringRedditException("User Not Found with id - " + username));
user.setEnabled(true);
userRepository.save(user);
}
}

Create Endpoint to Verify Users So we have created the logic to send out emails after registration, now let’s create an endpoint to Verify Users. Add the below method to AuthController

@GetMapping("accountVerification/{token}")
public ResponseEntity<String> verifyAccount(@PathVariable String token) {
authService.verifyAccount(token);
return new ResponseEntity<>("Account Activated Successully", OK);

}

Let’s also update the AuthService class.

    public void verifyAccount(String token) {
Optional<VerificationToken> verificationTokenOptional = verificationTokenRepository.findByToken(token);
verificationTokenOptional.orElseThrow(() -> new SpringRedditException("Invalid Token"));
fetchUserAndEnable(verificationTokenOptional.get());
}
@Transactional
private void fetchUserAndEnable(VerificationToken verificationToken) {
String username = verificationToken.getUser().getUsername();
User user = userRepository.findByUsername(username).orElseThrow(() -> new SpringRedditException("User Not Found with id - " + username));
user.setEnabled(true);
userRepository.save(user);
}

Testing the API’s

Now let’s test the user signup and verification API’s, make sure you are running the application, open PostMan and make the following calls to the endpoint – http://localhost:8080/api/auth/signup

Make sure you create the RequesBody as seen in the image and call the API, you should receive an HTTP OK status back from the API, with message User Registration Successful. If you have already set up the account and used those details, you should receive an email, check your inbox, you should see an email like below:

Send emails asynchronously If you observe the logs, you can see that there is a slight delay to send email after saving the user in the database. You can check this in the logs.

If you check the timestamp, we have a delay of more than 10 seconds to send out the email, that means even though the registration is completed, the user has to wait 10 more seconds to see the response. Even though this is not much time, in the real world situation we should handle the Email Sending functionality Asynchronously, we can also handle it by using a Message Queue like RabbitMQ, but I think that would be an overkill for our use-case. Let’s enable the Async module in spring by adding the @EnableAsync to our Main class.

package us.csmckay.springit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class SpringRedditCloneApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRedditCloneApplication.class, args);
}
}

Also, add the @Async to our sendMail method inside the MailService class. Here is how our MailService class looks like:

package us.csmckay.springit.service;

import us.csmckay.springit.exception.SpringRedditException;
import us.csmckay.springit.model.NotificationEmail;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@AllArgsConstructor
@Slf4j
class MailService {

    private final JavaMailSender mailSender;
    private final MailContentBuilder mailContentBuilder;

    @Async
    void sendMail(NotificationEmail notificationEmail) {
        MimeMessagePreparator messagePreparator = mimeMessage -> {
            MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
            messageHelper.setFrom("springreddit@email.com");
            messageHelper.setTo(notificationEmail.getRecipient());
            messageHelper.setSubject(notificationEmail.getSubject());
            messageHelper.setText(mailContentBuilder.build(notificationEmail.getBody()));
        };
        try {
            mailSender.send(messagePreparator);
            log.info("Activation email sent!!");
        } catch (MailException e) {
            throw new SpringRedditException("Exception occurred when sending mail to " + notificationEmail.getRecipient());
        }
    }

}

Testing Response time after activating Async Now if you try to register another user, you can observe that we got back the response pretty quickly compared to our previous request. You can compare that the previous request took around 11132 ms but after activating Async we got back the response in just 495 ms.

Now lets, copy the activation link from the email message and paste it into the Postman, and click on Send. Make sure that you don’t copy the characters &lt. You should receive the message – “Account Activated Successfully” back from the server.

Next Steps: - First, we will see how the authentication mechanism works in our application. - We will see what are the components used to build the Login API. - We will implement the Login API using JWT Authentication and test it.

Authentication Flow

First, let us see how the authentication flow will be in our application. The below picture should represent the complete authentication flow.

  1. So it starts with the Client sending a login request to the server.
  2. The server checks the credentials provided by the user, if the credentials are right, it creates a JSON Web Token (JWT).
  3. It responds with a success message (HTTP Status 200) and the JWT.
  4. The client uses this JWT in all the subsequent requests to the user, it provides this JWT as an Authorization header with Bearer authentication scheme.
  5. When the server, receives a request against a secured endpoint, it checks the JWT and validates whether the token is generated and signed by the server or not.
  6. If the validation is successful, the server responds accordingly to the client.

Spring Security Authentication Flow

Let us see what are the different components involved in the authentication mechanism inside our backend. - The login request is received by AuthenticationController and is passed on to the AuthService class. - This class creates an object of type UserNamePasswordAuthenticationToken which encapsulates the username and password provided by the user as part of the login request. - Then this is passed on to AuthenticationManager which takes care of the authentication part when using Spring Security. It implements lot of functionality in the background and provides us nice API we can use. - The AuthenticationManager further interacts with an interface called UserDetailsService, this interface as the name suggests deals with user data. There are several implementations that can be used depending on the kind of authentication we want. There is support for in-memory authentication, database-authentication, LDAP based authentication. - As we store our user information inside the Database, we used Database authentication, so the implementation access the database and retrieves the user details and passes UserDetails back to AuthenticationManager. - The AuthenticationManger now checks the credentials, and if they match it creates an object of type Authentication and passes it back to the AuthService class. - Then we create the JWT and respond back to the user.

I hope this provides the necessary high-level overview of how we are going to implement the authentication mechanism. Now without any further ado, let’s start coding!!

Configure SecurityConfig with AuthenticationManager

The first thing we have to do is, change our SecurityConfig class and add the necessary configuration for AuthenticationManger, the configuration file looks like below:

package com.example.springredditclone.config;

import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/auth/__")
                .permitAll()
                .anyRequest()
                .authenticated();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

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

We have defined a bean for AuthenticationManager and configured AuthenticationManagerBuilder to use the UserDetailsService interface to check for user information.

As UserDetailsService is an interface, we have to provide an implementation where it fetches the user information from our MySQL Database. Defining custom UserDetailsService class

This is how our UserDetailsServiceImpl class looks like:

package com.example.springredditclone.service;

import com.example.springredditclone.model.User;
import com.example.springredditclone.repository.UserRepository;
import lombok.AllArgsConstructor;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.Optional;

import static java.util.Collections.singletonList;

@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) {
        Optional<User> userOptional = userRepository.findByUsername(username);
        User user = userOptional
                .orElseThrow(() -> new UsernameNotFoundException("No user " +
                        "Found with username : " + username));

        return new org.springframework.security
                .core.userdetails.User(user.getUsername(), user.getPassword(),
                user.isEnabled(), true, true,
                true, getAuthorities("USER"));
    }

    private Collection<? extends GrantedAuthority> getAuthorities(String role) {
        return singletonList(new SimpleGrantedAuthority(role));
    }
}

The class overrides the method loadUserByUsername() which is used by Spring Security to fetch the user details. Inside the method, we are querying the UserRepository and fetching those details and wrapping them in another User object which implements the UserDetails interface.

Define REST Endpoint for Login

Let’s define an endpoint for our Login API.

AuthController.java

package com.programming.techie.springredditclone.controller;

import com.programming.techie.springredditclone.dto.AuthenticationResponse;
import com.programming.techie.springredditclone.dto.LoginRequest;
import com.programming.techie.springredditclone.dto.RegisterRequest;
import com.programming.techie.springredditclone.service.AuthService;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import static org.springframework.http.HttpStatus.OK;

@RestController
@RequestMapping("/api/auth")
@AllArgsConstructor
public class AuthController {

    private final AuthService authService;

    @PostMapping("/signup")
    public ResponseEntity signup(@RequestBody RegisterRequest registerRequest) {
        authService.signup(registerRequest);
        return new ResponseEntity(OK);
    }

    @PostMapping("/login")
    public AuthenticationResponse login(@RequestBody LoginRequest loginRequest) {
        return authService.login(loginRequest);
    }

    @GetMapping("accountVerification/{token}")
    public ResponseEntity<String> verifyAccount(@PathVariable String token) {
        authService.verifyAccount(token);
        return new ResponseEntity<>("Account Activated Successully", OK);
    }
}

LoginRequest.java

package com.example.springredditclone.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginRequest {

    private String username;
    private String password;
}

The login endpoint accepts POST request and passes the LoginRequest object to the login() method of the AuthService class

AuthService.java

package com.programming.techie.springredditclone.service;
import com.programming.techie.springredditclone.dto.AuthenticationResponse;
import com.programming.techie.springredditclone.dto.LoginRequest;
import com.programming.techie.springredditclone.dto.RegisterRequest;
import com.programming.techie.springredditclone.exception.SpringRedditException;
import com.programming.techie.springredditclone.model.NotificationEmail;
import com.programming.techie.springredditclone.model.User;
import com.programming.techie.springredditclone.model.VerificationToken;
import com.programming.techie.springredditclone.repository.UserRepository;
import com.programming.techie.springredditclone.repository.VerificationTokenRepository;
import com.programming.techie.springredditclone.security.JwtProvider;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import java.util.UUID;
import static com.programming.techie.springredditclone.util.Constants.ACTIVATION_EMAIL;
import static java.time.Instant.now;
@Service
@AllArgsConstructor
@Slf4j
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
private final JwtProvider jwtProvider;
private final MailContentBuilder mailContentBuilder;
private final MailService mailService;
private final VerificationTokenRepository verificationTokenRepository;
@Transactional
public void signup(RegisterRequest registerRequest) {
User user = new User();
user.setUsername(registerRequest.getUsername());
user.setEmail(registerRequest.getEmail());
user.setPassword(encodePassword(registerRequest.getPassword()));
user.setCreated(now());
user.setEnabled(false);
userRepository.save(user);
log.info("User Registered Successfully, Sending Authentication Email");
String token = generateVerificationToken(user);
String message = mailContentBuilder.build("Thank you for signing up to Spring Reddit, please click on the below url to activate your account : "
+ ACTIVATION_EMAIL + "/" + token);
mailService.sendMail(new NotificationEmail("Please Activate your account", user.getEmail(), message));
}
private String generateVerificationToken(User user) {
String token = UUID.randomUUID().toString();
VerificationToken verificationToken = new VerificationToken();
verificationToken.setToken(token);
verificationToken.setUser(user);
verificationTokenRepository.save(verificationToken);
return token;
}
private String encodePassword(String password) {
return passwordEncoder.encode(password);
}
public AuthenticationResponse login(LoginRequest loginRequest) {
Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(),
loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authenticate);
String authenticationToken = jwtProvider.generateToken(authenticate);
return new AuthenticationResponse(authenticationToken, loginRequest.getUsername());
}
public void verifyAccount(String token) {
Optional<VerificationToken> verificationTokenOptional = verificationTokenRepository.findByToken(token);
fetchUserAndEnable(verificationTokenOptional.orElseThrow(() -> new SpringRedditException("Invalid Token")));
}
@Transactional
private void fetchUserAndEnable(VerificationToken verificationToken) {
String username = verificationToken.getUser().getUsername();
User user = userRepository.findByUsername(username).orElseThrow(() -> new SpringRedditException("User Not Found with id - " + username));
user.setEnabled(true);
userRepository.save(user);
}
}

Creating JWT We will use the service class JWTProvider to create our JWT.

JWTProvider.java

package com.example.springredditclone.security;
import com.example.springredditclone.exceptions.SpringRedditException;
import io.jsonwebtoken.Jwts;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
@Service
public class JwtProvider {
private KeyStore keyStore;
@PostConstruct
public void init() {
try {
keyStore = KeyStore.getInstance("JKS");
InputStream resourceAsStream = getClass().getResourceAsStream("/springblog.jks");
keyStore.load(resourceAsStream, "secret".toCharArray());
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
throw new SpringRedditException("Exception occurred while loading keystore");
}
}
public String generateToken(Authentication authentication) {
org.springframework.security.core.userdetails.User principal = (User) authentication.getPrincipal();
return Jwts.builder()
.setSubject(principal.getUsername())
.signWith(getPrivateKey())
.compact();
}
private PrivateKey getPrivateKey() {
try {
return (PrivateKey) keyStore.getKey("springblog", "secret".toCharArray());
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
throw new SpringRedditException("Exception occured while retrieving public key from keystore");
}
}
}

We are using AsymmetricEncryption to sign our JWT’s using Java Keystore (using Public-Private Key)

TODO PART 4 HERE

Next Steps - First, we will see how to validate the JWT’s we receive from the client. - Implement API to create Subreddits.

Implement JWT Validation

Previous PostPost with a slider and lightbox
Next PostPost with YouTube Video
Comments (2)
John Doe
Posted at 15:32h, 06 December Reply

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

John Doe
Posted at 15:32h, 06 December Reply

It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal

John Doe
Posted at 15:32h, 06 December Reply

There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text.

John Doe
Posted at 15:32h, 06 December Reply

The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.

Leave a Comment