kamer.dev

Spring Boot User Registration and Login

- #java - #spring

Update 2020/05/02: I removed useless csrf().disabled() config and CustomAuthenticatorProvider class. Also, I changed Optional logic and used orElseThrow() method. Thanks @gokcensedat and @utkuozdemir for their valuable comments.

Hello! In this article, I’ll explain how to register users to your application, how to send confirmation mail and handle user logins. My main resource will be official docs and you can access Github repository at the end of the article. I will not explain exception handling parts since our main subject is different.

Requirements

You need to know Java and must have developed at least one Spring project to understand this article.

1. Creating a project with Spring Initialzr

Create a project with Spring Web, Lombok, Thymeleaf, Spring Security, Java Mail Sender, H2 and Spring Data JPA dependencies with Spring Initialzr. You can use your IDE’s plugin or https://start.spring.io/.

2. Create User entity

Create an entity package and UserRole enum inside the package. We will add ADMIN and USER values for now but you can add more according to your needs.

enum UserRole {

    ADMIN, USER
}

Now, create a User class. This class has to implement UserDetails interface. This interface has basic user methods. You can see an implementation in org.springframework.security.core.userdetails.User class. You can also extend this class and add your own fields. I will not extend this class because I want to show the whole process. So, let’s create a few fields, implement the required methods and fill them.

@Getter
@Setter
@Builder
@EqualsAndHashCode
@NoArgsConstructor`
@AllArgsConstructor
@Entity(name = "Users")
public class User implements UserDetails {

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

    private String name;

    private String surname;

    private String email;

    private String password;

    @Builder.Default
    private UserRole userRole = UserRole.USER;

    @Builder.Default
    private Boolean locked = false;

    @Builder.Default
    private Boolean enabled = false;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

	final SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userRole.name());
	return Collections.singletonList(simpleGrantedAuthority);
    }

    @Override
    public String getPassword() {
	return password;
    }

    @Override
    public String getUsername() {
	return email;
    }

    @Override
    public boolean isAccountNonExpired() {
	return true;
    }

    @Override
    public boolean isAccountNonLocked() {
	return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
	return true;
    }

    @Override
    public boolean isEnabled() {
	return enabled;
    }
}

We will not use expired field for this example, so always return true and we will also use email instead of username. So return email from getUsername() method. You can deduce others from the names.

3. Create UserService

Create a user package and UserService class inside the package. Then, implement UserDetailsService which requires you to implement loadByUsername(). But here’s important, we will use email instead of username. So, this method will take email as parameter and return the user. To achieve this, we need to interact with database, so we need a repository. Create a UserRepository interface inside the same package and add findByEmail() method.

interface UserRepository extends CrudRepository<User, Long> {

    Optional<User> findByEmail(String email);
}

Now, turn back to UserService, add UserRepository as a field and complete the method. UserService will be as below.

@Service
@AllArgsConstructor
public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {

	final Optional<User> optionalUser = userRepository.findByEmail(email);

	if (optionalUser.isPresent()) {
	    return optionalUser.get();
	}
	else {
	    throw new UsernameNotFoundException(MessageFormat.format("User with email {0} cannot be found.", email));
	}
    }
}

4. Create ConfirmationToken class

We will create a token and a unique link for every user registration and send them with email. Create a ConfirmationToken class inside user package and add these fields:

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
class ConfirmationToken {

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

    private String confirmationToken;

    private LocalDate createdDate;

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

    ConfirmationToken(User user) {
	this.user = user;
	this.createdDate = LocalDate.now();
	this.confirmationToken = UUID.randomUUID().toString();
    }
}

This is a simple entity as you see. Since every user will have a single token, we create a OneToOne relation. createdDate field will be used to check if the token is expired or not.

5. User registration process

I want you to experience a real-like development process instead of writing all the methods and classes. So let’s think of the process step by step.

  1. Take user information with the registration form.
  2. Encode the password with BCryptPasswordEncoder and create the user with enabled=false value as we set default in User class.
  3. Create a ConfirmationToken and assign this token to the user.
  4. Create a unique url with this token and send it via email.
  5. When the user clicks the link change enabled field to true for that user.
  6. Delete the token.

So, we will create ConfirmationTokenRepository and ConfirmationTokenService before signUpUser() method. Because we need this method in signing-up process.

@Repository
interface ConfirmationTokenRepository extends CrudRepository<ConfirmationToken, Long> { }
@Service
@AllArgsConstructor
class ConfirmationTokenService {

    private final ConfirmationTokenRepository confirmationTokenRepository;

    void saveConfirmationToken(ConfirmationToken confirmationToken) {

	confirmationTokenRepository.save(confirmationToken);
    }
}

Now, go to UserService class and create signUpUser() method.

void signUpUser(User user) {

    final String encryptedPassword = bCryptPasswordEncoder.encode(user.getPassword());

    user.setPassword(encryptedPassword);

    final User createdUser = userRepository.save(user);

    final ConfirmationToken confirmationToken = new ConfirmationToken(user);

    confirmationTokenService.saveConfirmationToken(confirmationToken);

}

6. Create confirmation methods

We have user registration process. Now, we need confirmation process. First of all, create the method that will delete token after confirmation.

void deleteConfirmationToken(Long id){

    confirmationTokenRepository.deleteById(id);
}

Then, create confirmUser() method.

void confirmUser(ConfirmationToken confirmationToken) {

    final User user = confirmationToken.getUser();

    user.setEnabled(true);

    userRepository.save(user);

    confirmationTokenService.deleteConfirmationToken(confirmationToken.getId());

}

7. Spring Mail config

I use application.yml instead of application.properties. (see this) Here’s the Spring Mail config for Gmail. If you use 2-factor, you have to create a password for application. (see this)

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: <MAIL_ADDRESS>
    password: <PASSWORD>
    properties:
      mail:
	smtp:
	  auth: true
	  starttls:
	    enable: true
	  connectiontimeout: 5000
	  timeout: 3000
	  writetimeout: 5000

8. Create EmailService

Create an EmailService class inside user package and create sendEmail() method inside it.

@Service
@AllArgsConstructor
public class EmailSenderService {

    private JavaMailSender javaMailSender;

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

Now, create the sendConfirmationMail() inside UserService.

void sendConfirmationMail(String userMail, String token) {

    final SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setTo(userMail);
    mailMessage.setSubject("Mail Confirmation Link!");
    mailMessage.setFrom("<MAIL>");
    mailMessage.setText(
	    "Thank you for registering. Please click on the below link to activate your account." + "http://localhost:8080/sign-up/confirm?token="
		    + token);

    emailSenderService.sendEmail(mailMessage);
}

9. Spring Security Configuration

We created the whole process related to Spring Security. Now we will set configuration for all. Create WebSecurityConfig and WebConfig inside config package.

@Configuration
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserService userService;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

	http
	    .authorizeRequests()
	    .antMatchers("/sign-up/**", "/sign-in/**")
	    .permitAll()
	    .anyRequest()
	    .authenticated()
	    .and()
	    .formLogin()
	    .loginPage("/sign-in")
	    .permitAll();
    }

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

}
@Configuration
public class WebConfig {

      @Bean
      public BCryptPasswordEncoder bCryptPasswordEncoder() {

	      return new BCryptPasswordEncoder();
      }
}

These configuration classes simply set sign-in and sign-up pages, set encoder and authentication provider. You can learn them with this great article.

10. Create controller and pages

We have all we need for an application except HTML pages. We will create those and make them accessible with controller.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <title>Spring Boot User Authentication</title>
</head>
<body>
<form role="form" th:action="@{/sign-in}" th:method="post">
    <label>Username</label>
    <input type="text" id="username" name="username">

    <label>Password</label>
    <input type="password" id="password" name="password">

    <input type="submit">
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <title>Spring Boot User Authentication</title>
</head>
<body>
<form role="form" th:action="@{/sign-up}" th:method="post" th:object="${user}">
    <label>Name</label>
    <input type="text" id="name" name="name" th:field="*{name}">

    <label>Surname</label>
    <input type="text" id="surname" name="surname" th:field="*{surname}">

    <label>Email</label>
    <input type="text" id="email" name="email" th:field="*{email}">

    <label>Password</label>
    <input type="password" id="password" name="password" th:field="*{password}">

    <input type="submit">
</form>
</body>
</html>
@Controller
@AllArgsConstructor
public class UserController {

    private final UserService userService;

    private final ConfirmationTokenService confirmationTokenService;

    @GetMapping("/sign-in")
    String signIn() {

	return "sign-in";
    }

    @GetMapping("/sign-up")
    String signUp() {

	return "sign-up";
    }

    @PostMapping("/sign-up")
    String signUp(User user) {

	userService.signUpUser(user);

	return "redirect:/sign-in";
    }

    @GetMapping("/confirm")
    String confirmMail(@RequestParam("token") String token) {

	Optional<ConfirmationToken> optionalConfirmationToken = confirmationTokenService.findConfirmationTokenByToken(token);

	optionalConfirmationToken.ifPresent(userService::confirmUser);

	return "/sign-in";
    }

}

Conclusion

If you have followed the article properly, you can go to http://localhost:8080/sign-up to sign up. But do not forget, it is not a production ready application since we didn’t create validations(including token expiration) and provide proper exception handling. If you want to learn about validations you can see https://medium.com/@kamer.dev/validations-in-spring-boot-4f15598c3d6a.

Github Repository: https://github.com/kamer/spring-boot-user-registration