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 tofalse
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 totrue
. - Delete the token from the database.
- Let the user know that the account is activated.
- Set
- 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.
- If the token is valid, then
Angular Frontend
User Registration Page
We have already implemented the Sign In/Sing Up functionalities in our previous tutorials.
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.”
Verification Email after User Account Registration
User Email Verification Success Page
Link Expired 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:
Email Verification Re-sent Page
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:
- Email Account for Sending Emails
- Spring Tool Suite 4 or any other IDE of your choice
- JDK 11
- Maven
- Spring Boot + Angular Application Source Code
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
calledTokenStatus
to define different types of token status. Learn more on how to use an Enum in an Angular Component - A
status
field of typeTokenStatus
to hold the current status of the token - An
errorMessage
field to hold the error message if any
- An
- The
ngOnInit()
method gets the token from the URL and calls theauthService.verifyToken()
that returns anObservable
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.
- If the token validated successfully, then, it sets the returned validation result in the
- Binds the resend button
click
event toauthService.resendToken()
method that returns anObservable
object. If the email resent successfully, then, it sets the tokenstatus
toSENT
.
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 tofalse
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 totrue
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 macro
, include
, nested
, 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.