If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
β’ The Registration Process With Spring Security
β’ Registration with Spring Security β Password Encoding
β’ The Registration API becomes RESTful
β’ Spring Security β Reset Your Password
β’ Registration β Password Strength and Rules
β’ Updating Your Password
β’ Notify User of Login From New Device or Location
1. Overview
This article continues the ongoing Registration with Spring Security series with one of the missing pieces of the registration process β verifying the userβs email to confirm their account.
The registration confirmation mechanism forces the user to respond to a βConfirm Registrationβ email sent after successful registration to verify his email address and activate their account. The user does this by clicking a unique activation link sent to them over email.
Following this logic, a newly registered user will not be able to log into the system until this process is completed.
2. A Verification Token
We will make use of a simple verification token as the key artifact through which a user is verified.
2.1. The VerificationToken Entity
The VerificationToken entity must meet the following criteria:
- It must link back to the User (via a unidirectional relation)
- It will be created right after registration
- It will expire within 24 hours following its creation
- Has a unique, randomly generated value
Requirements 2 and 3 are part of the registration logic. The other two are implemented in a simple VerificationToken entity like the one in Example 2.1.:
Example 2.1.
@Entity
public class VerificationToken {
private static final int EXPIRATION = 60 * 24;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String token;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
private Date expiryDate;
private Date calculateExpiryDate(int expiryTimeInMinutes) {
Calendar cal = Calendar.getInstance();
cal.setTime(new Timestamp(cal.getTime().getTime()));
cal.add(Calendar.MINUTE, expiryTimeInMinutes);
return new Date(cal.getTime().getTime());
}
// standard constructors, getters and setters
}
Note the nullable = false on the User to ensure data integrity and consistency in the VerificationToken<->User association.
2.2. Add the enabled Field to User
Initially, when the User is registered, this enabled field will be set to false. During the account verification process β if successful β it will become true.
Let us start by adding the field to our User entity:
public class User {
...
@Column(name = "enabled")
private boolean enabled;
public User() {
super();
this.enabled=false;
}
...
}
Note how we also set the default value of this field to false.
3. During Account Registration
Letβs add two additional pieces of business logic to the user registration use case:
- Generate the VerificationToken for the User and persist it
- Send out the email message for account confirmation β which includes a confirmation link with the VerificationTokenβs value
3.1. Using a Spring Event to Create the Token and Send the Verification Email
These two additional pieces of logic should not be performed by the controller directly because they are βcollateralβ back-end tasks.
The controller will publish a Spring ApplicationEvent to trigger the execution of these tasks. This is as simple as injecting the ApplicationEventPublisher and then using it to publish the registration completion.
Example 3.1. shows this simple logic:
Example 3.1.
@Autowired
ApplicationEventPublisher eventPublisher
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
String appUrl = request.getContextPath();
eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registered,
request.getLocale(), appUrl));
} catch (UserAlreadyExistException uaeEx) {
ModelAndView mav = new ModelAndView("registration", "user", userDto);
mav.addObject("message", "An account for that username/email already exists.");
return mav;
} catch (RuntimeException ex) {
return new ModelAndView("emailError", "user", userDto);
}
return new ModelAndView("successRegister", "user", userDto);
}
One additional thing to notice is the try catch block surrounding the publishing of the event. This piece of code will display an error page whenever there is an exception in the logic executed after the publishing of the event, which in this case is the sending of the email.
3.2. The Event and the Listener
Letβs now see the actual implementation of this new OnRegistrationCompleteEvent that our controller is sending out, as well as the listener that is going to handle it:
Example 3.2.1. β The OnRegistrationCompleteEvent
public class OnRegistrationCompleteEvent extends ApplicationEvent {
private String appUrl;
private Locale locale;
private User user;
public OnRegistrationCompleteEvent(
User user, Locale locale, String appUrl) {
super(user);
this.user = user;
this.locale = locale;
this.appUrl = appUrl;
}
// standard getters and setters
}
Example 3.2.2. β The RegistrationListener Handles the OnRegistrationCompleteEvent
@Component
public class RegistrationListener implements
ApplicationListener<OnRegistrationCompleteEvent> {
@Autowired
private IUserService service;
@Autowired
private MessageSource messages;
@Autowired
private JavaMailSender mailSender;
@Override
public void onApplicationEvent(OnRegistrationCompleteEvent event) {
this.confirmRegistration(event);
}
private void confirmRegistration(OnRegistrationCompleteEvent event) {
User user = event.getUser();
String token = UUID.randomUUID().toString();
service.createVerificationToken(user, token);
String recipientAddress = user.getEmail();
String subject = "Registration Confirmation";
String confirmationUrl
= event.getAppUrl() + "/regitrationConfirm?token=" + token;
String message = messages.getMessage("message.regSucc", null, event.getLocale());
SimpleMailMessage email = new SimpleMailMessage();
email.setTo(recipientAddress);
email.setSubject(subject);
email.setText(message + "\r\n" + confirmationUrl);
mailSender.send(email);
}
}
Here, the confirmRegistration method will receive the OnRegistrationCompleteEvent, extract all the necessary User information from it, create the verification token, persist it, and then send it as a parameter in the βConfirm Registrationβ link.
As was mentioned above, any jakarta.mail.AuthenticationFailedException thrown by JavaMailSender will be handled by the controller.
3.3. Processing the Verification Token Parameter
When the user receives the βConfirm Registrationβ link they should click on it.
Once they do β the controller will extract the value of the token parameter in the resulting GET request and will use it to enable the User.
Letβs see this process in Example 3.3.1.:
Example 3.3.1. β RegistrationController Processing the Registration Confirmation
@Autowired
private IUserService service;
@GetMapping("/regitrationConfirm")
public String confirmRegistration
(WebRequest request, Model model, @RequestParam("token") String token) {
Locale locale = request.getLocale();
VerificationToken verificationToken = service.getVerificationToken(token);
if (verificationToken == null) {
String message = messages.getMessage("auth.message.invalidToken", null, locale);
model.addAttribute("message", message);
return "redirect:/badUser.html?lang=" + locale.getLanguage();
}
User user = verificationToken.getUser();
Calendar cal = Calendar.getInstance();
if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
String messageValue = messages.getMessage("auth.message.expired", null, locale)
model.addAttribute("message", messageValue);
return "redirect:/badUser.html?lang=" + locale.getLanguage();
}
user.setEnabled(true);
service.saveRegisteredUser(user);
return "redirect:/login.html?lang=" + request.getLocale().getLanguage();
}
The user will be redirected to an error page with the corresponding message if:
- The VerificationToken does not exist, for some reason or
- The VerificationToken has expired
See Example 3.3.2. to see the error page.
Example 3.3.2. β The badUser.html
<html>
<body>
<h1 th:text="${param.message[0]}>Error Message</h1>
<a th:href="@{/registration.html}"
th:text="#{label.form.loginSignUp}">signup</a>
</body>
</html>
If no errors are found, the user is enabled.
There are two opportunities for improvement in handling the VerificationToken checking and expiration scenarios:
- We can use a Cron Job to check for token expiration in the background
- We can give the user the opportunity to get a new token once it has expired
Weβll defer the generation of a new token for a future article and assume that the user does indeed successfully verify their token here.
4. Adding Account Activation Checking to the Login Process
We need to add the code that will check if the user is enabled:
Letβs see this in Example 4.1. which shows the loadUserByUsername method of MyUserDetailsService.
Example 4.1.
@Autowired
UserRepository userRepository;
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
try {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(
"No user found with username: " + email);
}
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword().toLowerCase(),
user.isEnabled(),
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getAuthorities(user.getRole()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
As we can see, now MyUserDetailsService not uses the enabled flag of the user β and so it will only allow enabled the user to authenticate.
Now, we will add an AuthenticationFailureHandler to customize the exception messages coming from MyUserDetailsService. Our CustomAuthenticationFailureHandler is shown in Example 4.2.:
Example 4.2. β CustomAuthenticationFailureHandler:
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private MessageSource messages;
@Autowired
private LocaleResolver localeResolver;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
setDefaultFailureUrl("/login.html?error=true");
super.onAuthenticationFailure(request, response, exception);
Locale locale = localeResolver.resolveLocale(request);
String errorMessage = messages.getMessage("message.badCredentials", null, locale);
if (exception.getMessage().equalsIgnoreCase("User is disabled")) {
errorMessage = messages.getMessage("auth.message.disabled", null, locale);
} else if (exception.getMessage().equalsIgnoreCase("User account has expired")) {
errorMessage = messages.getMessage("auth.message.expired", null, locale);
}
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage);
}
}
We will need to modify login.html to show the error messages.
Example 4.3. β Display error messages at login.html:
<div th:if="${param.error != null}"
th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>
5. Adapting the Persistence Layer
Letβs now provide the actual implementation of some of these operations involving the verification token as well as the users.
Weβll cover:
- A new VerificationTokenRepository
- New methods in the IUserInterface and its implementation for new CRUD operations needed
Examples 5.1 β 5.3. show the new interfaces and implementation:
Example 5.1. β The VerificationTokenRepository
public interface VerificationTokenRepository
extends JpaRepository<VerificationToken, Long> {
VerificationToken findByToken(String token);
VerificationToken findByUser(User user);
}
Example 5.2. β The IUserService Interface
public interface IUserService {
User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException;
User getUser(String verificationToken);
void saveRegisteredUser(User user);
void createVerificationToken(User user, String token);
VerificationToken getVerificationToken(String VerificationToken);
}
Example 5.3. The UserService
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Autowired
private VerificationTokenRepository tokenRepository;
@Override
public User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException {
if (emailExist(userDto.getEmail())) {
throw new UserAlreadyExistException(
"There is an account with that email adress: "
+ userDto.getEmail());
}
User user = new User();
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
user.setPassword(userDto.getPassword());
user.setEmail(userDto.getEmail());
user.setRole(new Role(Integer.valueOf(1), user));
return repository.save(user);
}
private boolean emailExist(String email) {
return userRepository.findByEmail(email) != null;
}
@Override
public User getUser(String verificationToken) {
User user = tokenRepository.findByToken(verificationToken).getUser();
return user;
}
@Override
public VerificationToken getVerificationToken(String VerificationToken) {
return tokenRepository.findByToken(VerificationToken);
}
@Override
public void saveRegisteredUser(User user) {
repository.save(user);
}
@Override
public void createVerificationToken(User user, String token) {
VerificationToken myToken = new VerificationToken(token, user);
tokenRepository.save(myToken);
}
}
6. Conclusion
In this article, weβve expanded the registration process to include an email based account activation procedure.
The account activation logic requires sending a verification token to the user via email so that they can send it back to the controller to verify their identity.
