How to Implement Spring Boot Angular User Registration Email Verification

In the previous article,  we have integrated the Razorpay payment gateway with our Spring Boot Angular application. In this article, we are gonna implement user registration email verification using the Freemarker template engine. If you want to send emails in multiple languages with attachments or to learn more about the Freemarker template engine, then you can refer to the article on how to Send Email using Spring Boot 2 and FreeMarker HTML Template.

Disclosure: Please note that some of the links in the post are referral links. Read more about the policy here.

What You’ll Build

  • Set user.enabled flag to false while creating a new user.
  • Generate a verification token with a validity of 24 hours and store the token with user_id in the database.
  • Generate an activation link with this token and send it to the user registered email address asynchronously.
  • Notify the user that a verification email has been sent in the registration success page.
  • When the user clicks on the link provided in the email, get the token in the URL and validate it.
    • If the token is valid, then
      • Set user.enabled flag to true.
      • Delete the token from the database.
      • Let the user know that the account is activated.
    • Else if the token is expired, provide an option to resend the email with the updated token.
    • Else if the token is invalid, then notify the same to the user.

Angular Frontend

User Registration Page

We have already implemented the Sign In/Sing Up functionalities in our previous tutorials.

Spring Boot Angular User Registration Page

User Registration Success Page

We have just modified this page to include the message “We’ve sent you a verification email to your email account.”

Spring Boot Angular User Registration Success

Verification Email after User Account Registration

Spring Boot Angular User Account Activation Email

User Email Verification Success Page

Spring Boot Angular User Account Activation Success Page

The link in the user account confirmation email is valid for 24 hours only. In case, if the user opens the link after 24 hours:

Spring Boot Angular User Account Activation Link Expired

Email Verification Re-sent Page

Resend User Account Activation Email

Spring Boot Backend

We will be implementing the following REST APIs for user email verification and account activation:

  • Send Verification Email – Enhance user registration API with mapping /signup to send user account activation email after the user registation using Freemarker Template.
  • Verify Token – Expose a POST API with mapping /token/verify. On passing the token, it will validate if the token is valid/invalid or expired and return the result in the response.
  • Resend Token – Expose a POST API with mapping /token/resend. On passing the expired token, it will update the token and re-send the verification email.

What You’ll Need

Run the following checklist before you begin the implementation:

Angular Client Implementation

Modify Registration Component

register.component.html

Update the message to notify the users that a verification email has been sent to their registered email address.

<div class="alert alert-success" *ngIf="isSuccessful">
			Your registration is successful! We've sent you a verification mail to your email account.
			<div *ngIf="isUsing2FA">
				<p>Scan this QR code using Google Authenticator app on your phone to use it later to login</p>
				<img src="{{qrCodeImage}}" class="img-fluid" />
			</div>
		</div>

Create Token Verification Page

token.component.ts

This component does the following:

  • Declares,
    • An enum called TokenStatus to define different types of token status. Learn more on how to use an Enum in an Angular Component
    • A status field of type TokenStatus to hold the current status of the token
    • An errorMessage field to hold the error message if any
  • The ngOnInit() method gets the token from the URL and calls the authService.verifyToken() that returns an Observable object.
    • If the token validated successfully, then, it sets the returned validation result in the status field.
    • Else it sets the returned error message into the errorMessage field.
  • Binds the resend button click event to authService.resendToken() method that returns an Observable object. If the email resent successfully, then, it sets the token status to SENT.
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../_services/auth.service';
import { Router, ActivatedRoute } from '@angular/router';

export enum TokenStatus {
	  VALID,
	  INVALID,
	  EXPIRED,
	  SENDING,
	  SENT
	}


@Component({
selector: 'app-token',
templateUrl: './token.component.html',
styleUrls: ['./register.component.css']
})
export class TokenComponent implements OnInit {

	token = '';
	tokenStatus = TokenStatus;
	status : TokenStatus;
	errorMessage = '';

	constructor(private authService: AuthService, private route: ActivatedRoute) {

	}

	ngOnInit(): void {
		this.token = this.route.snapshot.queryParamMap.get('token');
		if(this.token){
			this.authService.verifyToken(this.token).subscribe(
			data => {
				this.status = TokenStatus[data.message as keyof typeof TokenStatus];
			}
			,
			err => {
				this.errorMessage = err.error.message;
			}
			);
		}	
	}

	resendToken(): void {
		this.status = TokenStatus.SENDING;
		this.authService.resendToken(this.token).subscribe(
		data => {
			this.status = TokenStatus.SENT;
		}
		,
		err => {
			this.errorMessage = err.error.message;
		}
		);
	}
}

token.component.html

Displays the token status and resend option.

<div class="col-md-12">
	<div class="card card-container">
		<img id="profile-img" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" class="profile-img-card" />
		<div class="form-group" *ngIf="status == tokenStatus.VALID">
			<div class="alert alert-success" role="alert">Email verified successfully. Please login.</div>
		</div>
		<div class="form-group" *ngIf="status == tokenStatus.EXPIRED">
			<div class="alert alert-danger" role="alert">Link Expired!</div>
			<button class="btn btn-primary btn-block" (click)="resendToken()">Re-send Verification Email</button>
		</div>
		<div class="form-group" *ngIf="status == tokenStatus.SENDING">
			<div class="alert alert-info" role="alert">Sending mail...</div>
		</div>
		<div class="form-group" *ngIf="status == tokenStatus.SENT">
			<div class="alert alert-info" role="alert">Sent verification email</div>
		</div>
		<div class="form-group" *ngIf="status == tokenStatus.INVALID">
			<div class="alert alert-danger" role="alert">Invalid Link</div>
		</div>
		<div class="form-group" *ngIf="errorMessage">
			<div class="alert alert-danger" role="alert">{{errorMessage}}</div>
		</div>
	</div>
</div>

Modify Auth Service

auth.service.ts

Add the following methods to call the /token/verify and /token/resend REST services for token verification and resending the token respecitively.

  verifyToken(token): Observable<any> {
    return this.http.post(AppConstants.AUTH_API + 'token/verify', token, {
    	  headers: new HttpHeaders({ 'Content-Type': 'text/plain' })
    });
  }

  resendToken(token): Observable<any> {
    return this.http.post(AppConstants.AUTH_API + 'token/resend', token, {
    	  headers: new HttpHeaders({ 'Content-Type': 'text/plain' })
    });
  }

Define Module

app.module.ts

Import and add TokenComponent in the module declarations

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { TotpComponent } from './totp/totp.component';
import { OrderComponent } from './order/order.component';
import { TokenComponent } from './register/token.component';

import { authInterceptorProviders } from './_helpers/auth.interceptor';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    RegisterComponent,
    HomeComponent,
    ProfileComponent,
    BoardAdminComponent,
    BoardModeratorComponent,
    BoardUserComponent,
    TotpComponent,
    OrderComponent,
    TokenComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [authInterceptorProviders],
  bootstrap: [AppComponent]
})
export class AppModule { }

Define Module Routing

app-routing.module.ts

Import and add TokenComponent in the route declarations

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { RegisterComponent } from './register/register.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { TotpComponent } from './totp/totp.component';
import { OrderComponent } from './order/order.component';
import { TokenComponent } from './register/token.component';

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'profile', component: ProfileComponent },
  { path: 'user', component: BoardUserComponent },
  { path: 'mod', component: BoardModeratorComponent },
  { path: 'admin', component: BoardAdminComponent },
  { path: 'totp', component: TotpComponent },
  { path: 'order', component: OrderComponent },
  { path: 'verify', component: TokenComponent },
  { path: '', redirectTo: 'home', pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Spring Boot Backend Implementation

Add Mail and Freemarker Dependencies

pom.xml

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

Configure Email Properties

application.properties

GMAIL SMTP server is being used for sending the mails.

################### GMail Configuration ##########################
spring.mail.host=smtp.gmail.com
spring.mail.port=465
spring.mail.protocol=smtps
[email protected]
spring.mail.password=secret
spring.mail.properties.mail.transport.protocol=smtps
spring.mail.properties.mail.smtps.auth=true
spring.mail.properties.mail.smtps.starttls.enable=true
spring.mail.properties.mail.smtps.timeout=8000
[email protected]
app.client.baseUrl=http://localhost:8081/

AppProperties.java

Modify AppProperties class to include client baseUrl configuration

package com.javachinna.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
	private final Auth auth = new Auth();
	private final OAuth2 oauth2 = new OAuth2();
	private final Client client = new Client();

	@Getter
	@Setter
	public static class Auth {
		private String tokenSecret;
		private long tokenExpirationMsec;
	}

	@Getter
	public static final class OAuth2 {
		private List<String> authorizedRedirectUris = new ArrayList<>();

		public OAuth2 authorizedRedirectUris(List<String> authorizedRedirectUris) {
			this.authorizedRedirectUris = authorizedRedirectUris;
			return this;
		}
	}

	@Getter
	@Setter
	public static class Client {
		private String baseUrl;
	}
}

Configure Message Sources

The file for the default locale will have the name messages.properties, and files for each locale will be named messages_XX.properties, where XX is the locale code. Learn more about how to configure message sources for different languages.

messages_en.properties

message.mail.verification=Thank you for creating an account. Please click the link below to activate your account. This link will expire in 24 hours.

Configure Asynchronous Execution

Sending emails should be done asynchorously since we don’t want to block the user operation during this process. Hence, we are gonna use the asynchronous execution support provided by Spring framework with help of @EnableAsync and @Async annotations.

CustomAsyncExceptionHandler.java

CustomAsyncExceptionHandler class is responsibile for handling the exceptions that occurs during asynchronous execution.

package com.javachinna.exception.handler;

import java.lang.reflect.Method;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;

public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
	private final Logger logger = LogManager.getLogger(getClass());

	@Override
	public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
		logger.error("Method name - " + method.getName(), throwable);
		for (Object param : obj) {
			logger.error("Parameter value - " + param);
		}
	}
}

SpringAsyncConfig.java

@EnableAsync annotation enables Spring’s asynchronous method execution capability, similar to functionality found in Spring’s <task:*> XML namespace. To be used together with @Configuration classes as follows, enabling annotation-driven async processing for an entire Spring application context:

package com.javachinna.config;

import java.util.concurrent.Executor;

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import com.javachinna.exception.handler.CustomAsyncExceptionHandler;

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

	@Override
	public Executor getAsyncExecutor() {
		ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
		scheduler.setPoolSize(10);
		scheduler.initialize();
		return scheduler;
	}

	@Override
	public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		return new CustomAsyncExceptionHandler();
	}
}

Create Application Constants

AppConstants.java

package com.javachinna.config;

public class AppConstants {
	public static final String TOKEN_INVALID = "INVALID";
	public static final String TOKEN_EXPIRED = "EXPIRED";
	public static final String TOKEN_VALID = "VALID";
	public final static String SUCCESS = "success";
}

Modify Utilities Class

GeneralUtils.java

Add the following method for calculating the expiry date of tokens.

public static Date calculateExpiryDate(final int expiryTimeInMinutes) {
		final Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis(new Date().getTime());
		cal.add(Calendar.MINUTE, expiryTimeInMinutes);
		return new Date(cal.getTime().getTime());
	}

Create JPA Entity

AbstractToken.java

This is an abstract class for token entity. Based on this we can create different type of tokens for different purposes like account activation, forgot password, etc.

package com.javachinna.model;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MappedSuperclass;
import javax.persistence.OneToOne;

import com.javachinna.util.GeneralUtils;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


@NoArgsConstructor
@Getter
@Setter
@MappedSuperclass
public abstract class AbstractToken implements Serializable {
	private static final long serialVersionUID = 1L;

	private static final int EXPIRATION = 60 * 24;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	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 AbstractToken(final String token) {
		super();
		this.token = token;
		this.expiryDate = GeneralUtils.calculateExpiryDate(EXPIRATION);
	}

	public AbstractToken(final String token, final User user) {
		super();
		this.token = token;
		this.user = user;
		this.expiryDate = GeneralUtils.calculateExpiryDate(EXPIRATION);
	}

	public void updateToken(final String token) {
		this.token = token;
		this.expiryDate = GeneralUtils.calculateExpiryDate(EXPIRATION);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((expiryDate == null) ? 0 : expiryDate.hashCode());
		result = prime * result + ((token == null) ? 0 : token.hashCode());
		result = prime * result + ((user == null) ? 0 : user.hashCode());
		return result;
	}

	@Override
	public boolean equals(final Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		final AbstractToken other = (AbstractToken) obj;
		if (expiryDate == null) {
			if (other.expiryDate != null) {
				return false;
			}
		} else if (!expiryDate.equals(other.expiryDate)) {
			return false;
		}
		if (token == null) {
			if (other.token != null) {
				return false;
			}
		} else if (!token.equals(other.token)) {
			return false;
		}
		if (user == null) {
			if (other.user != null) {
				return false;
			}
		} else if (!user.equals(other.user)) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();
		builder.append("Token [String=").append(token).append("]").append("[Expires").append(expiryDate).append("]");
		return builder.toString();
	}
}

VerificationToken.java

This VerificationToken entity maps to the verification_token table in the database

package com.javachinna.model;

import javax.persistence.Entity;

@Entity
public class VerificationToken extends AbstractToken {

	private static final long serialVersionUID = -6551160985498051566L;

	public VerificationToken() {
		super();
	}

	public VerificationToken(final String token) {
		super(token);
	}

	public VerificationToken(final String token, final User user) {
		super(token, user);
	}
}

Create Spring Data Repository

VerificationTokenRepository.java

This repository is responsible for querying the verification_token table.

package com.javachinna.repo;

import org.springframework.data.jpa.repository.JpaRepository;

import com.javachinna.model.User;
import com.javachinna.model.VerificationToken;

public interface VerificationTokenRepository extends JpaRepository<VerificationToken, Long> {

	VerificationToken findByToken(String token);

	VerificationToken findByUser(User user);
}

Create/Modify Service Class

Create Mail Service

MailService.java

This is just an interface used to define methods required for Mail services.

package com.javachinna.service;

import com.javachinna.model.User;

public interface MailService {

	void sendVerificationToken(String token, User user);
}

MailServiceImpl.java

This service class is responsible sending HTML email using Freemarker templates asynchronously.

@Async annotation marks a method as a candidate for asynchronous execution. Can also be used at the type level, in which case all of the type’s methods are considered as asynchronous. Note, however, that @Async is not supported on methods declared within a @Configuration class.

package com.javachinna.service;

import java.util.HashMap;
import java.util.Map;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;

import com.javachinna.config.AppProperties;
import com.javachinna.model.User;

import freemarker.template.Configuration;

/**
 * @author hp
 *
 */
@Service
public class MailServiceImpl implements MailService {

	private final Logger logger = LogManager.getLogger(getClass());
	private static final String SUPPORT_EMAIL = "support.email";
	public static final String LINE_BREAK = "<br>";
	public final static String BASE_URL = "baseUrl";
	
	@Autowired
	private MessageService messageService;

	@Autowired
	private JavaMailSender mailSender;

	@Autowired
	private Environment env;

	@Autowired
	Configuration freemarkerConfiguration;

	@Autowired
	AppProperties appProperties;

	@Async
	@Override
	public void sendVerificationToken(String token, User user) {
		final String confirmationUrl = appProperties.getClient().getBaseUrl() + "verify?token=" + token;
		final String message = messageService.getMessage("message.mail.verification");
		sendHtmlEmail("Registration Confirmation", message + LINE_BREAK + confirmationUrl, user);
	}

	private String geFreeMarkerTemplateContent(Map<String, Object> model, String templateName) {
		StringBuffer content = new StringBuffer();
		try {
			content.append(FreeMarkerTemplateUtils.processTemplateIntoString(freemarkerConfiguration.getTemplate(templateName), model));
			return content.toString();
		} catch (Exception e) {
			System.out.println("Exception occured while processing fmtemplate:" + e.getMessage());
		}
		return "";
	}

	private void sendHtmlEmail(String subject, String msg, User user) {
		Map<String, Object> model = new HashMap<String, Object>();
		model.put("name", user.getDisplayName());
		model.put("msg", msg);
		model.put("title", subject);
		model.put(BASE_URL, appProperties.getClient().getBaseUrl());
		try {
			sendHtmlMail(env.getProperty(SUPPORT_EMAIL), user.getEmail(), subject, geFreeMarkerTemplateContent(model, "mail/verification.ftl"));
		} catch (MessagingException e) {
			logger.error("Failed to send mail", e);
		}
	}

	public void sendHtmlMail(String from, String to, String subject, String body) throws MessagingException {
		MimeMessage mail = mailSender.createMimeMessage();
		MimeMessageHelper helper = new MimeMessageHelper(mail, true, "UTF-8");
		helper.setFrom(from);
		if (to.contains(",")) {
			helper.setTo(to.split(","));
		} else {
			helper.setTo(to);
		}
		helper.setSubject(subject);
		helper.setText(body, true);
		mailSender.send(mail);
		logger.info("Sent mail: {0}", subject);
	}
}

Modify User Service

UserService.java

This is just an interface used to define methods required for user services. We are gonna add 3 new methods as shown below:

package com.javachinna.service;

import java.util.Map;
import java.util.Optional;

import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;

import com.javachinna.dto.LocalUser;
import com.javachinna.dto.SignUpRequest;
import com.javachinna.exception.UserAlreadyExistAuthenticationException;
import com.javachinna.model.User;

/**
 * @author Chinna
 * @since 26/3/18
 */
public interface UserService {

	public User registerNewUser(SignUpRequest signUpRequest) throws UserAlreadyExistAuthenticationException;

	User findUserByEmail(String email);

	Optional<User> findUserById(Long id);

	LocalUser processUserRegistration(String registrationId, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo);
	
	void createVerificationTokenForUser(User user, String token);

	boolean resendVerificationToken(String token);

	String validateVerificationToken(String token);
}

UserServiceImpl.java

Modify this class to perform the following operations:

  • Set user.enabled flag to false while creating a new user.
  • Generate a verification token and store it with user_id in the database.
  • Validate the verification token. If the token is valid, then, set user.enabled flag to true and delete the token from the database.
  • Resend the email with the updated token.
package com.javachinna.service;

import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.javachinna.config.AppConstants;
import com.javachinna.dto.LocalUser;
import com.javachinna.dto.SignUpRequest;
import com.javachinna.dto.SocialProvider;
import com.javachinna.exception.OAuth2AuthenticationProcessingException;
import com.javachinna.exception.UserAlreadyExistAuthenticationException;
import com.javachinna.model.Role;
import com.javachinna.model.User;
import com.javachinna.model.VerificationToken;
import com.javachinna.repo.RoleRepository;
import com.javachinna.repo.UserRepository;
import com.javachinna.repo.VerificationTokenRepository;
import com.javachinna.security.oauth2.user.OAuth2UserInfo;
import com.javachinna.security.oauth2.user.OAuth2UserInfoFactory;
import com.javachinna.util.GeneralUtils;

import dev.samstevens.totp.secret.SecretGenerator;

/**
 * @author Chinna
 * @since 26/3/18
 */
@Service
public class UserServiceImpl implements UserService {

	@Autowired
	private UserRepository userRepository;
	
	@Autowired
	private RoleRepository roleRepository;

	@Autowired
	private VerificationTokenRepository tokenRepository;

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Autowired
	private SecretGenerator secretGenerator;

	@Autowired
	MailService mailService;
	
	@Override
	@Transactional(value = "transactionManager")
	public User registerNewUser(final SignUpRequest signUpRequest) throws UserAlreadyExistAuthenticationException {
		if (signUpRequest.getUserID() != null && userRepository.existsById(signUpRequest.getUserID())) {
			throw new UserAlreadyExistAuthenticationException("User with User id " + signUpRequest.getUserID() + " already exist");
		} else if (userRepository.existsByEmail(signUpRequest.getEmail())) {
			throw new UserAlreadyExistAuthenticationException("User with email id " + signUpRequest.getEmail() + " already exist");
		}
		User user = buildUser(signUpRequest);
		Date now = Calendar.getInstance().getTime();
		user.setCreatedDate(now);
		user.setModifiedDate(now);
		user = userRepository.save(user);
		userRepository.flush();
		return user;
	}

	private User buildUser(final SignUpRequest formDTO) {
		User user = new User();
		user.setDisplayName(formDTO.getDisplayName());
		user.setEmail(formDTO.getEmail());
		user.setPassword(passwordEncoder.encode(formDTO.getPassword()));
		final HashSet<Role> roles = new HashSet<Role>();
		roles.add(roleRepository.findByName(Role.ROLE_USER));
		user.setRoles(roles);
		user.setProvider(formDTO.getSocialProvider().getProviderType());
		user.setEnabled(false);
		user.setProviderUserId(formDTO.getProviderUserId());
		if (formDTO.isUsing2FA()) {
			user.setUsing2FA(true);
			user.setSecret(secretGenerator.generate());
		}
		return user;
	}

	@Override
	public User findUserByEmail(final String email) {
		return userRepository.findByEmail(email);
	}

	@Override
	@Transactional
	public LocalUser processUserRegistration(String registrationId, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo) {
		OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, attributes);
		if (StringUtils.isEmpty(oAuth2UserInfo.getName())) {
			throw new OAuth2AuthenticationProcessingException("Name not found from OAuth2 provider");
		} else if (StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
			throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
		}
		SignUpRequest userDetails = toUserRegistrationObject(registrationId, oAuth2UserInfo);
		User user = findUserByEmail(oAuth2UserInfo.getEmail());
		if (user != null) {
			if (!user.getProvider().equals(registrationId) && !user.getProvider().equals(SocialProvider.LOCAL.getProviderType())) {
				throw new OAuth2AuthenticationProcessingException(
						"Looks like you're signed up with " + user.getProvider() + " account. Please use your " + user.getProvider() + " account to login.");
			}
			user = updateExistingUser(user, oAuth2UserInfo);
		} else {
			user = registerNewUser(userDetails);
		}

		return LocalUser.create(user, attributes, idToken, userInfo);
	}

	private User updateExistingUser(User existingUser, OAuth2UserInfo oAuth2UserInfo) {
		existingUser.setDisplayName(oAuth2UserInfo.getName());
		return userRepository.save(existingUser);
	}

	private SignUpRequest toUserRegistrationObject(String registrationId, OAuth2UserInfo oAuth2UserInfo) {
		return SignUpRequest.getBuilder().addProviderUserID(oAuth2UserInfo.getId()).addDisplayName(oAuth2UserInfo.getName()).addEmail(oAuth2UserInfo.getEmail())
				.addSocialProvider(GeneralUtils.toSocialProvider(registrationId)).addPassword("changeit").build();
	}

	@Override
	public Optional<User> findUserById(Long id) {
		return userRepository.findById(id);
	}
	
	@Override
	public void createVerificationTokenForUser(final User user, final String token) {
		final VerificationToken myToken = new VerificationToken(token, user);
		tokenRepository.save(myToken);
	}

	@Override
	@Transactional
	public boolean resendVerificationToken(final String existingVerificationToken) {
		VerificationToken vToken = tokenRepository.findByToken(existingVerificationToken);
		if(vToken != null) {
			vToken.updateToken(UUID.randomUUID().toString());
			vToken = tokenRepository.save(vToken);
			mailService.sendVerificationToken(vToken.getToken(), vToken.getUser());
			return true;
		}
		return false;
	}
	
	@Override
	public String validateVerificationToken(String token) {
		final VerificationToken verificationToken = tokenRepository.findByToken(token);
		if (verificationToken == null) {
			return AppConstants.TOKEN_INVALID;
		}

		final User user = verificationToken.getUser();
		final Calendar cal = Calendar.getInstance();
		if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
			return AppConstants.TOKEN_EXPIRED;
		}

		user.setEnabled(true);
		tokenRepository.delete(verificationToken);
		userRepository.save(user);
		return AppConstants.TOKEN_VALID;
	}
}

Creat Message Service

MessageService.java

This is just a convenience class for getting the messages by locale.

package com.javachinna.service;

import java.util.Locale;

import javax.annotation.Resource;

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

@Component
public class MessageService {

	@Resource
	private MessageSource messageSource;

	private Locale locale = LocaleContextHolder.getLocale();

	public String getMessage(String code) {
		return messageSource.getMessage(code, null, locale);
	}

	public String getMessage(String code, Object... params) {
		return messageSource.getMessage(code, params, locale);
	}
}

Modify AuthController

AuthController.java

We are gonna enhance the /signup API for sending email after registration and add 2 new APIs for token verification and resending respectively.

package com.javachinna.controller;

import static dev.samstevens.totp.util.Utils.getDataUriForImage;

import java.util.UUID;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
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.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.javachinna.config.AppConstants;
import com.javachinna.config.CurrentUser;
import com.javachinna.dto.ApiResponse;
import com.javachinna.dto.JwtAuthenticationResponse;
import com.javachinna.dto.LocalUser;
import com.javachinna.dto.LoginRequest;
import com.javachinna.dto.SignUpRequest;
import com.javachinna.dto.SignUpResponse;
import com.javachinna.exception.UserAlreadyExistAuthenticationException;
import com.javachinna.model.User;
import com.javachinna.security.jwt.TokenProvider;
import com.javachinna.service.MailService;
import com.javachinna.service.UserService;
import com.javachinna.util.GeneralUtils;

import dev.samstevens.totp.code.CodeVerifier;
import dev.samstevens.totp.exceptions.QrGenerationException;
import dev.samstevens.totp.qr.QrData;
import dev.samstevens.totp.qr.QrDataFactory;
import dev.samstevens.totp.qr.QrGenerator;
import lombok.extern.slf4j.Slf4j;

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

	@Autowired
	AuthenticationManager authenticationManager;

	@Autowired
	UserService userService;

	@Autowired
	TokenProvider tokenProvider;

	@Autowired
	private QrDataFactory qrDataFactory;

	@Autowired
	private QrGenerator qrGenerator;

	@Autowired
	private CodeVerifier verifier;

	@Autowired
	MailService mailService;

	@PostMapping("/signin")
	public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
		Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
		SecurityContextHolder.getContext().setAuthentication(authentication);
		LocalUser localUser = (LocalUser) authentication.getPrincipal();
		boolean authenticated = !localUser.getUser().isUsing2FA();
		String jwt = tokenProvider.createToken(localUser, authenticated);
		return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, authenticated, authenticated ? GeneralUtils.buildUserInfo(localUser) : null));
	}

	@PostMapping("/signup")
	public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
		try {
			User user = userService.registerNewUser(signUpRequest);
			final String token = UUID.randomUUID().toString();
			userService.createVerificationTokenForUser(user, token);
			mailService.sendVerificationToken(token, user);
			if (signUpRequest.isUsing2FA()) {
				QrData data = qrDataFactory.newBuilder().label(user.getEmail()).secret(user.getSecret()).issuer("JavaChinna").build();
				// Generate the QR code image data as a base64 string which can
				// be used in an <img> tag:
				String qrCodeImage = getDataUriForImage(qrGenerator.generate(data), qrGenerator.getImageMimeType());
				return ResponseEntity.ok().body(new SignUpResponse(true, qrCodeImage));
			}
		} catch (UserAlreadyExistAuthenticationException e) {
			log.error("Exception Ocurred", e);
			return new ResponseEntity<>(new ApiResponse(false, "Email Address already in use!"), HttpStatus.BAD_REQUEST);
		} catch (QrGenerationException e) {
			log.error("QR Generation Exception Ocurred", e);
			return new ResponseEntity<>(new ApiResponse(false, "Unable to generate QR code!"), HttpStatus.BAD_REQUEST);
		}
		return ResponseEntity.ok().body(new ApiResponse(true, "User registered successfully"));
	}

	@PostMapping("/verify")
	@PreAuthorize("hasRole('PRE_VERIFICATION_USER')")
	public ResponseEntity<?> verifyCode(@NotEmpty @RequestBody String code, @CurrentUser LocalUser user) {
		if (!verifier.isValidCode(user.getUser().getSecret(), code)) {
			return new ResponseEntity<>(new ApiResponse(false, "Invalid Code!"), HttpStatus.BAD_REQUEST);
		}
		String jwt = tokenProvider.createToken(user, true);
		return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, true, GeneralUtils.buildUserInfo(user)));
	}

	@PostMapping("/token/verify")
	public ResponseEntity<?> confirmRegistration(@NotEmpty @RequestBody String token) {
		final String result = userService.validateVerificationToken(token);
		return ResponseEntity.ok().body(new ApiResponse(true, result));
	}

	// user activation - verification
	@PostMapping("/token/resend")
	@ResponseBody
	public ResponseEntity<?> resendRegistrationToken(@NotEmpty @RequestBody String expiredToken) {
		if (!userService.resendVerificationToken(expiredToken)) {
			return new ResponseEntity<>(new ApiResponse(false, "Token not found!"), HttpStatus.BAD_REQUEST);
		}
		return ResponseEntity.ok().body(new ApiResponse(true, AppConstants.SUCCESS));
	}
}

Create Freemarker Email Templates

These template files will be used to create different types of emails with the same layout, header, and footer.

defaultLayout.ftl

This file defines the basic layout of the email which includes the header and footer. To learn more about the Freemarker directives like macroincludenested, etc., please refer to the official Freemarker documentation

<#macro myLayout>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <style>
 .card {
 	position: relative;
 	display: block;
 	-webkit-box-orient: vertical;
 	-webkit-box-direction: normal;
 	min-width: 0;
 	word-wrap: break-word;
 	background-color: #fff;
 	background-clip: border-box;
 	border: 1px solid rgba(0, 0, 0, .125);
 	border-radius: .25rem
 }
 
 .card-body {
 	-webkit-box-flex: 1;
 	-ms-flex: 1 1 auto;
 	flex: 1 1 auto;
 	padding: 0 1.25rem 0 1.25rem
 }
 .card-header {
  	-webkit-box-flex: 1;
 	-ms-flex: 1 1 auto;
 	flex: 1 1 auto;
	padding: .25rem 1.25rem;
	margin-bottom: 0;
	background-color: rgba(0, 0, 0, .03);
	border-bottom: 1px solid rgba(0, 0, 0, .125)
}

 .media {
 	display: -webkit-box;
 	display: -ms-flexbox;
 	display: flex;
 	-webkit-box-align: start;
 	-ms-flex-align: start;
 	align-items: flex-start
 }
 
 .media-body {
 	-webkit-box-flex: 1;
 	-ms-flex: 1;
 	flex: 1
 }
 
 .rounded-circle {
 	border-radius: 50% !important
 }
 
 .pagelink-dark {
 	cursor: pointer;
 	color: #343a40 !important;
    text-decoration: none;
 }
 
 .pagelink-dark:hover {
 	text-shadow: 1px 1px 2px #343a40;
 	text-decoration: none;
 }
 </style>
 
</head>
    <body style="width:100%;height:100%">
      <table cellspacing="0" cellpadding="0" style="width:100%;height:100%">
        <tr>
          <td colspan="2" align="center">
            <#include "header.ftl"/>
          </td>
        </tr>
        <tr>
          <td>
            <#nested/>
          </td>
        </tr>
        <tr>
          <td colspan="2">
            <#include "footer.ftl"/>
          </td>
        </tr>
      </table>
    </body>
  </html>
</#macro>

header.ftl

<img src="https://www.javachinna.com/wp-content/uploads/2020/02/cropped-JavaChinna_logo.jpg" alt="https://javachinna.com" style="display: block;" width="130" height="50"/>
<h3><span style="border-bottom: 4px solid #32CD32;">${title}</span></h3>

footer.ftl

<div style="background: #F0F0F0; text-align: center; padding: 5px; margin-top: 40px;">
            Message Generated from: javachinna.com
</div>

Create Verification Email Template

verification.ftl

This file is used to generate the Registration confirmation emails.

<#import "layout/defaultLayout.ftl" as layout>
<@layout.myLayout>
<div>
<table align="center" border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse;">
		<tr>
			<td style="padding: 0px 30px 0px 30px;">
				<p>Dear ${name},</p>
				<p>${msg}</p>
			</td>
		</tr>
		<tr>
			<td style="padding-left: 30px;">
				<p>
					Cheers, <br /> <em>Chinna</em>
				</p>
			</td>
		</tr>
	</table>
</div>
</@layout.myLayout>

Run Spring Boot App with Maven

You can run the application with mvn clean spring-boot:run and the REST API services can be accessed via http://localhost:8080

Run the Angular App

You can run this App with the below command and hit the URL http://localhost:8081/ in browser

ng serve --port 8081

Source Code

https://github.com/JavaChinna/angular-spring-boot-email-integration

Conclusion

That’s all folks. In this article, we have implemented user account activation via email post registration with our Spring Boot Angular application.

Thank you for reading.

Leave a Reply