Quantcast
Channel: Baeldung
Viewing all articles
Browse latest Browse all 3522

Registration – Activate a New Account by Email

$
0
0

I usually post about Spring stuff on Google+ - you can follow me there:

1. Overview

This article continues our ongoing Registration with Spring Security series with one of the missing pieces of the registration process – verifying the users 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:

  1. It must link back to the User (via a unidirectional relation)
  2. It will be created right after registration
  3. It will expire within 24 hours following its creation
  4. 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;

    public VerificationToken() {
        super();
    }
    public VerificationToken(String token, User user) {
        super();
        this.token = token;
        this.user = user;
        this.expiryDate = calculateExpiryDate(EXPIRATION);
        this.verified = false;
    }
    
    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 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.

Lets 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

Lets add two additional pieces of business logic to the user registration use case:

  1. Generate the VerificationToken for the User and persist it
  2. 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

@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 = 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);
}

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 final String appUrl;
    private final Locale locale;
    private final 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.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 + " \r\n" + "http://localhost:8080" + 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 javax.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.

Lets see this process in Example 3.3.1.:

Example 3.3.1. – RegistrationController Processing the 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) {
        model.addAttribute("message", messages.getMessage("auth.message.expired", null, locale));
        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:

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

See Example 3.3.2. to see the error page.

Example 3.3.2. – The badUser.html

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<fmt:setBundle basename="messages" />
<%@ page session="true"%>
<html>
<head>
    <link href="<c:url value="/resources/bootstrap.css" />" rel="stylesheet">
    <title>Expired</title>
</head>
<body>
    <h1>${message}</h1>
    <br>
    <a href="<c:url value="/user/registration" />">
        <spring:message code="label.form.loginSignUp"></spring:message>
    </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:

  1. We can use a Cron Job to check for token expiration in the background
  2. We can give the user the opportunity to get a new token once it has expired

We’ll deffer 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:

Lets 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) {
            return new org.springframework.security.core.userdetails.User
              (" ", " ", enabled, true, true, true, getAuthorities(new Integer(1)));
        }
        return new org.springframework.security.core.userdetails.User(
          user.getEmail(), 
          user.getPassword().toLowerCase(), 
          user.isEnabled(), 
          accountNonExpired, 
          credentialsNonExpired, 
          accountNonLocked, 
          getAuthorities(user.getRole().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 user to authenticate. .

Now, we need to modify our login.html page to show the exception messages coming from MyUserDetailsService. The error checking code we added to login.html is shown in Example 4.2.:

Example 4.2. – Adding Account Activation Error Checking to login.html

<c:if test="${param.error != null}">
    <c:choose>
        <c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'User is disabled'}">
            <div class="alert alert-error">
                <spring:message code="auth.message.disabled"></spring:message>
            </div>
        </c:when>
        <c:when test="${SPRING_SECURITY_LAST_EXCEPTION.message == 'User account has expired'}">
            <div class="alert alert-error">
                <spring:message code="auth.message.expired"></spring:message>
            </div>
        </c:when>
        <c:otherwise>
            <div class="alert alert-error">
	      <spring:message code="message.badCredentials"></spring:message>
           </div>
        </c:otherwise>
    </c:choose>
</c:if>

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:

  1. A new VerificationTokenRepository
  2. 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 accountDto) throws EmailExistsException;

    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 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 = 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.

The implementation of this Spring Security REST Tutorial can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.

I usually post about Spring stuff on Google+ - you can follow me there:

 


Viewing all articles
Browse latest Browse all 3522

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>