www.baeldung.com/registratio…

Eugen Paraschiv

From stackGC

1, an overview of the

This article continues the missing part of the registration process in the previous Spring Security series – validating a user’s email to confirm an account.

The registration confirmation mechanism forces the user to perform the actions in the confirmation registration email to verify their email address and activate their account after successful registration. The user completes the activation by clicking the unique activation link in the E-mail.

According to this logic, newly registered users cannot log in to the system until the process is completed.

2. Verify the Token

We will use a simple authentication token as the credential to authenticate the user.

2.1 VerificationToken Entity

VerificationToken entities must meet the following criteria:

  1. It must point to User (through a one-way relationship)
  2. It will be created immediately after registration
  3. It will expire within 24 hours of creation
  4. There is a unique, randomly generated value

Points 2 and 3 are part of the registration logic. The remaining two implementations are in simple VerificationToken entities, as 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());
    }
     
    // Omit the constructor, getter, and setter
}
Copy the code

Note that nullable = false on User ensures data integrity and consistency in the VerificationToken <-> User association.

2.2. Add the Enabled field to User

This Enabled field will be set to false when the user registers. During account authentication, it is set to true if it passes.

Add a field to the User entity:

public class User {...@Column(name = "enabled")
    private boolean enabled;
     
    public User(a) {
        super(a);this.enabled=false; }... }Copy the code

Note that we also set the default value of this field to false.

3. During account registration

Add two additional business logic to the user registration use case:

  1. Generate and save a VerificationToken for the User
  2. Send an email for account confirmation — which contains a confirmation link with a VerificationToken value

3.1. Use Spring Event to create a token and send an authentication email

These two additional logic should not be performed directly by the controller because they are parallel background tasks.

The controller will issue a Spring ApplicationEvent to trigger the execution of these tasks. This and injection ApplicationEventPublisher and use it to distribute as simple registration.

Example 3.1 shows this simple logic:

Example 3.1

@Autowired
ApplicationEventPublisher eventPublisher
 
@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto, 
  BindingResult result, 
  WebRequest request, 
  Errors errors) {
  
    if (result.hasErrors()) {
        return new ModelAndView("registration"."user", accountDto);
    }
     
    User registered = createUserAccount(accountDto);
    if (registered == null) {
        result.rejectValue("email"."message.regError");
    }
    try {
        String appUrl = request.getContextPath();
        eventPublisher.publishEvent(new OnRegistrationCompleteEvent
          (registered, request.getLocale(), appUrl));
    } catch (Exception me) {
        return new ModelAndView("emailError"."user", accountDto);
    }
    return new ModelAndView("successRegister"."user", accountDto);
}
Copy the code

Another thing to note is the try catch block that wraps events. This code represents displaying an error page whenever there is an exception in the logic executed after the publishing event. The logic here is to send an email.

3.2 events and Listeners

Now let’s look at OnRegistrationCompleteEvent actual implementation, as well as to deal with its listeners:

3.2.1 – 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
}
Copy the code

3.2.2 – OnRegistrationCompleteEvent RegistrationListener processing

@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.html? token=" + token;
        String message = messages.getMessage("message.regSucc".null, event.getLocale());
         
        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(recipientAddress);
        email.setSubject(subject);
        email.setText(message + " rn" + "http://localhost:8080"+ confirmationUrl); mailSender.send(email); }}Copy the code

Here, will receive OnRegistrationCompleteEvent confirmRegistration method, extract all the necessary User information, create the authentication token, save it, then upon confirmation of registration link will be sent as a parameter.

As mentioned above, any javax.mail JavaMailSender trigger. Mail. AuthenticationFailedException will be handled by controller.

3.3. Handle the validation token parameters

Click on the confirmation link when the user receives it.

Once clicked, the controller will extract the value of the token parameter in the GET request and will use it to enable User.

Example 3.3.1 – RegistrationController handles registration confirmation

@Autowired
private IUserService service;
 
@RequestMapping(value = "/regitrationConfirm", method = RequestMethod.GET)
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(); 
}
Copy the code

The user will be redirected to the error page and displayed with the appropriate message if:

  1. The VerificationToken does not exist for some reason
  2. VerificationToken expired

See the error page in example 3.3.2.

In 3.3.2 rainfall distribution on 10-12 – 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>
Copy the code

If no errors are found, the user is enabled.

There are two areas that can be improved in the process of handling VerificationToken checking and expiration:

  1. We can use the Cron job to check in the background if the token is expired
  2. Once expired, we can give the user the opportunity to get a new token

We’ll defer the process of generating a new token to a later article, and now assume that the user did successfully validate the token here.

4. Add account activation checks to the login process

We need to add code to check whether the user is enabled:

Example 4.1 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 newRuntimeException(e); }}Copy the code

As you can see, MyUserDetailsService now does not use the Enabled flag of User.

Now add a AuthenticationFailureHandler from definition from MyUserDetailsService exception message. Our CustomAuthenticationFailureHandler as shown in example 4.2:

Example – CustomAuthenticationFailureHandler 4.2:

@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); }}Copy the code

Login.html needs to be modified to display error messages.

Example 4.3 – Displaying error messages at login.html:

<div th:if="${param.error ! = null}"
  th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>
Copy the code

5, adapt persistent layer

Let’s look at some actual implementations involving validation tokens and user actions.

Covers the following:

  1. A new VerificationTokenRepository
  2. New methods in IUserInterface and their implementation requirements for new CRUD operations

Examples 5.1-5.3 show the new interface and implementation:

5.1 – VerificationTokenRepository example

public interface VerificationTokenRepository 
  extends JpaRepository<VerificationToken.Long> {
 
    VerificationToken findByToken(String token);
 
    VerificationToken findByUser(User user);
}
Copy the code

Example 5.2 – IUserService interface

public interface IUserService {
     
    User registerNewUserAccount(UserDto accountDto) 
      throws EmailExistsException;
 
    User getUser(String verificationToken);
 
    void saveRegisteredUser(User user);
 
    void createVerificationToken(User user, String token);
 
    VerificationToken getVerificationToken(String VerificationToken);
}
Copy the code

Example 5.3 – UserService

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
 
    @Autowired
    private VerificationTokenRepository tokenRepository;
 
    @Override
    public User registerNewUserAccount(UserDto accountDto) 
      throws EmailExistsException {
         
        if (emailExist(accountDto.getEmail())) {
            throw new EmailExistsException(
              "There is an account with that email adress: "
              + accountDto.getEmail());
        }
         
        User user = new User();
        user.setFirstName(accountDto.getFirstName());
        user.setLastName(accountDto.getLastName());
        user.setPassword(accountDto.getPassword());
        user.setEmail(accountDto.getEmail());
        user.setRole(new Role(Integer.valueOf(1), user));
        return repository.save(user);
    }
 
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if(user ! =null) {
            return true;
        }
        return false;
    }
     
    @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 = newVerificationToken(token, user); tokenRepository.save(myToken); }}Copy the code

6, summary

This article introduced the registration process — the email-based account activation process.

The account activation logic is to send authentication tokens to users via email so that they can send information back to the controller for authentication.

The registration sample and implementation of the Spring Security tutorial can be found in the GitHub project.

Original project source code

  • Github.com/eugenp/spri…