How to Secure Spring Boot Angular Application with Two Factor Authentication

This is the extension of the Spring Boot Angular Social Login application. In this article, we are going to implement Two Factor Authentication with Spring Security and a Soft Token.

What is Two Factor Authentication?

Two Factor Authentication follows the principle “something the user knows and something the user has“. Simply put, it adds one more level of security with a Time-based One Time Password (TOTP) verification on top of username and password based authentication.

What You’ll Build

Register

Angular User Registration with Two Factor (2FA) Authentication

Registeration Success

Angular User Registration Success with QR Code Image

Login

Angular User Login

TOTP Verification

Angular Time-based One Time Password (TOTP) Verification

Login Success

Angular Two Factor (2FA) Authentication Success

What You’ll Need

  • Spring Tool Suite 4
  • JDK 11
  • MySQL Server 8
  • node.js
  • Google / Microsoft Authenticator App

Design

Spring Boot Angular Two Factor Authentication (2FA) Sequence Diagram

Spring Boot (Backend) Implementation

We will be implementing 2FA authentication with Spring Security for performing 3 operations:

  • Generating JWT – On passing the correct username and password, If the user enabled 2FA during registration, then it will generate a JSON Web Token (JWT) with an expiry time of 5 minutes. Also, put an authenticated flag into the token to indicate that the user is not fully authenticated yet.
  • Validating Soft Token  (TOTP) – Expose a POST API with mapping /verify. On passing the correct token, it will generate a JSON Web Token (JWT) with an expiry time of 10 days and return it in the response along with the user details.
  • Validating JWT – If a user tries to access the REST API, it will allow access only if request has a valid token. If authenticated flag in the token is false, then only the PRE_VERIFICATION_USER role will be granted to the user. With this role, only the /verify endpoint can be accessed by the user.

We are going to use the Google / Microsoft Authenticator app to Scan the QR code for generating the soft token.

Add dependencies

There are multiple TOTP libraries available. We are going to use samstevens TOTP library since it has totp-spring-boot-starter dependency for easy integration.

pom.xml

		<dependency>
			<groupId>dev.samstevens.totp</groupId>
			<artifactId>totp-spring-boot-starter</artifactId>
			<version>1.7.1</version>
		</dependency>

Enable Two Factor Authentication

If the user opts for 2FA during registration, then we need to enable 2FA for that user and generate a secret key which will be used to validate the token when the user logs in.

User.java

Add the following fields in the User entity to store the 2FA option value and secret.

	@Column(name = "USING_2FA")
	private boolean using2FA;

	private String secret;

Role.java

Add a constant for the PRE_VERIFICATION_USER role.

public static final String ROLE_PRE_VERIFICATION_USER = "ROLE_PRE_VERIFICATION_USER";

UserServiceImpl.java

During user registration, if the user opted for 2FA authentication, then

  • Set using_2fa flag to true
  • Generate a secret using the SecretGenerator from the TOTP library and set it in the secret field.
	@Autowired
	private SecretGenerator secretGenerator;

	@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(true);
		user.setProviderUserId(formDTO.getProviderUserId());
		if (formDTO.isUsing2FA()) {
			user.setUsing2FA(true);
			user.setSecret(secretGenerator.generate());
		}
		return user;
	}

TokenProvider.java

If authenticated flag is true, then generate a long lived token else a short lived token

Put the authenticated flag into the JWT claims. So that we can use this flag to determine the user role while validating the token in the request.

Add a method to retrieve this flag from the token

@Service
public class TokenProvider {

	private static final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

	private static final String AUTHENTICATED = "authenticated";

	public static final long TEMP_TOKEN_VALIDITY_IN_MILLIS = 300000;

	private AppProperties appProperties;

	public TokenProvider(AppProperties appProperties) {
		this.appProperties = appProperties;
	}

	public String createToken(LocalUser userPrincipal, boolean authenticated) {
		Date now = new Date();
		Date expiryDate = new Date(now.getTime() + (authenticated ? appProperties.getAuth().getTokenExpirationMsec() : TEMP_TOKEN_VALIDITY_IN_MILLIS));

		return Jwts.builder().setSubject(Long.toString(userPrincipal.getUser().getId())).claim(AUTHENTICATED, authenticated).setIssuedAt(new Date()).setExpiration(expiryDate)
				.signWith(SignatureAlgorithm.HS512, appProperties.getAuth().getTokenSecret()).compact();
	}

	public Long getUserIdFromToken(String token) {
		Claims claims = Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(token).getBody();

		return Long.parseLong(claims.getSubject());
	}

	public Boolean isAuthenticated(String token) {
		Claims claims = Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(token).getBody();
		return claims.get(AUTHENTICATED, Boolean.class);
	}

	public boolean validateToken(String authToken) {
		try {
			Jwts.parser().setSigningKey(appProperties.getAuth().getTokenSecret()).parseClaimsJws(authToken);
			return true;
		} catch (SignatureException ex) {
			logger.error("Invalid JWT signature");
		} catch (MalformedJwtException ex) {
			logger.error("Invalid JWT token");
		} catch (ExpiredJwtException ex) {
			logger.error("Expired JWT token");
		} catch (UnsupportedJwtException ex) {
			logger.error("Unsupported JWT token");
		} catch (IllegalArgumentException ex) {
			logger.error("JWT claims string is empty.");
		}
		return false;
	}
}

TokenAuthenticationFilter.java

Retrieve the authenticated flag from the token,

  • If it is true means, the user is fully authenticated and we can assign the actual user roles.
  • Else, assign PRE_VERIFICATION_USER role only which can be used to access only the /verify REST endpoint.
public class TokenAuthenticationFilter extends OncePerRequestFilter {

	@Autowired
	private TokenProvider tokenProvider;

	@Autowired
	private LocalUserDetailService customUserDetailsService;

	private static final Logger logger = LoggerFactory.getLogger(TokenAuthenticationFilter.class);

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		try {
			String jwt = getJwtFromRequest(request);

			if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
				Long userId = tokenProvider.getUserIdFromToken(jwt);

				UserDetails userDetails = customUserDetailsService.loadUserById(userId);
				Collection<? extends GrantedAuthority> authorities = tokenProvider.isAuthenticated(jwt)
						? userDetails.getAuthorities()
						: List.of(new SimpleGrantedAuthority(Role.ROLE_PRE_VERIFICATION_USER));
				UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
				authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

				SecurityContextHolder.getContext().setAuthentication(authentication);
			}
		} catch (Exception ex) {
			logger.error("Could not set user authentication in security context", ex);
		}

		filterChain.doFilter(request, response);
	}

	private String getJwtFromRequest(HttpServletRequest request) {
		String bearerToken = request.getHeader("Authorization");
		if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
			return bearerToken.substring(7, bearerToken.length());
		}
		return null;
	}
}

AuthController.java

  • Modify the authenticateUser method to generate a short-lived token if the user is using 2FA. This token can be used for accessing the /verify REST endpoint for verification of the TOTP obtained from the Authenticator app.
  • Modify the registerUser method to generate QR code image data if the user opted for 2FA and return it in the response
  • Expose a POST API with mapping /verify. On passing the correct token, it will generate a JSON Web Token (JWT) with an expiry time of 10 days and return it in the response along with the user details. Secure this service for the users having PRE_VERIFICATION_USER role only.
@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;

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

JwtAuthenticationResponse.java

Add a new field to indicate if the user is fully authenticated. So that the frontend can show TOTP verification page if authenticated flag is false.

@Value
public class JwtAuthenticationResponse {
	private String accessToken;
	private boolean authenticated;
	private UserInfo user;
}

SignUpRequest.java

Add a field for capturing the user 2FA preference

private boolean using2FA;

SignUpResponse.java

If the user enabled 2FA, then return the QR code image data. So that the frontend can display the QR image after successful registration.

@Value
public class SignUpResponse {
	private boolean using2FA;
	private String qrCodeImage;
}

OAuth2AuthenticationSuccessHandler.java

Though it is not related to the 2FA implementation, we need to do the following change since we have changed the signature of tokenProvider.createToken method.

	@Override
	protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue);

		if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
			throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
		}

		String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
		LocalUser user = (LocalUser) authentication.getPrincipal();
		String token = tokenProvider.createToken(user, true);

		return UriComponentsBuilder.fromUriString(targetUrl).queryParam("token", token).build().toUriString();
	}

Angular Client (Frontend) Implementation

Create a TOTP component

This component binds form data (6 digit code) to AuthService.verify() method that returns an Observable object. If code verification is successful, then it stores the token and calls the login() method.

ngOnInit(): If a token is present in the Browser Session Storage then it sets the isLoggedIn flag to true and currentUser from the Storage.

login() method does the following:

  • Saves the user in Session Storage.
  • Sets the isLoggedIn flag to true
  • Sets the currentUser from the Storage.
  • Reloads the page

totp.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../_services/auth.service';
import { TokenStorageService } from '../_services/token-storage.service';


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

  form: any = {};
  isLoggedIn = false;
  isLoginFailed = false;
  errorMessage = '';
  currentUser: any;

  constructor(private authService: AuthService, private tokenStorage: TokenStorageService) {}

  ngOnInit(): void {
  	if (this.tokenStorage.getUser()) {
      this.isLoggedIn = true;
      this.currentUser = this.tokenStorage.getUser();
    }
  }

  onSubmit(): void {
    this.authService.verify(this.form).subscribe(
      data => {
        this.tokenStorage.saveToken(data.accessToken);
        this.login(data.user);
      },
      err => {
        this.errorMessage = err.error.message;
        this.isLoginFailed = true;
      }
    );
  }

  login(user): void {
	this.tokenStorage.saveUser(user);
	this.isLoginFailed = false;
	this.isLoggedIn = true;
	this.currentUser = this.tokenStorage.getUser();
    window.location.reload();
  }
}

totp.component.html

<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" />
		<form *ngIf="!isLoggedIn" name="form" (ngSubmit)="f.form.valid && onSubmit()" #f="ngForm" novalidate>
			<div class="form-group">
				<label for="code">Enter the 6-digit code from your authenticator app</label> <input type="text" class="form-control" name="code" [(ngModel)]="form.code" required minlength="6" #code="ngModel" />
				<div class="alert alert-danger" role="alert" *ngIf="f.submitted && code.invalid">
					<div *ngIf="code.errors.required">Code is required</div>
					<div *ngIf="code.errors.minlength">Code must be at least 6 characters</div>
				</div>
			</div>
			<div class="form-group">
				<button class="btn btn-primary btn-block">Login</button>
			</div>
			<div class="form-group">
				<div class="alert alert-danger" role="alert" *ngIf="isLoginFailed">Login failed: {{ errorMessage }}</div>
			</div>
		</form>
		<div class="alert alert-success" *ngIf="isLoggedIn">Welcome {{currentUser.displayName}} <br>Logged in as {{ currentUser.roles }}.</div>
	</div>
</div>

totp.component.css

.card-container.card {
  max-width: 400px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}

Modify Register component

register.component.ts

If 2FA enabled, then display QR Code image along with “Registration Successful” message. So that, user can scan the QR code using an Authenticator app and get the TOTP to login to the application.

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../_services/auth.service';

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

  form: any = {};
  isSuccessful = false;
  isSignUpFailed = false;
  isUsing2FA = false;
  errorMessage = '';
  qrCodeImage = '';

  constructor(private authService: AuthService) { }

  ngOnInit(): void {
  }

  onSubmit(): void {
    this.authService.register(this.form).subscribe(
      data => {
        if(data.using2FA){
        	this.isUsing2FA = true;
        	this.qrCodeImage = data.qrCodeImage;
        }
	    this.isSuccessful = true;
        this.isSignUpFailed = false;
      },
      err => {
        this.errorMessage = err.error.message;
        this.isSignUpFailed = true;
      }
    );
  }
}

register.component.html

Add a checkbox to enable 2FA

			<div class="form-group form-check">
				<input type="checkbox" class="form-check-input" name="using2FA" [(ngModel)]="form.using2FA" #using2FA="ngModel" /><label class="form-check-label" for="using2FA">Use
					Two Step Verification</label>
			</div>

Modify the following block to show the QR code image if the user enabled 2FA post successful registration.

		<div class="alert alert-success" *ngIf="isSuccessful">
			Your registration is successful!
			<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>

Modify Login Component

login.component.ts

Import Router

import { Router, ActivatedRoute } from '@angular/router';

Add the router in the constructor

On initialization, set isLoggedIn flag and currentUser if the user is available in the Browser Session.

Note: The REST API will return the user details only after successful verification of the TOTP if 2FA is enabled. That is why we have changed the logic to check for the user in the session instead of the token.

  constructor(private authService: AuthService, private tokenStorage: TokenStorageService, private route: ActivatedRoute, private userService: UserService, private router: Router) {}

  ngOnInit(): void {
	const token: string = this.route.snapshot.queryParamMap.get('token');
	const error: string = this.route.snapshot.queryParamMap.get('error');
  	if (this.tokenStorage.getUser()) {
      this.isLoggedIn = true;
      this.currentUser = this.tokenStorage.getUser();
    }

On Login Submit,

  • If the REST API returns authenticated = true in the response, then store the user details in the Browser Session Storage and reload the page.
  • Else, navigate to the TOTP page for the code verification.
  onSubmit(): void {
    this.authService.login(this.form).subscribe(
      data => {
        this.tokenStorage.saveToken(data.accessToken);
        if(data.authenticated){
	        this.login(data.user);
        } else {
        	this.router.navigate(['/totp']);
        }
      },
      err => {
        this.errorMessage = err.error.message;
        this.isLoginFailed = true;
      }
    );
  }

Modify App Component

app.component.ts

On initialization, set isLoggedIn flag based on the user availability in the Browser Session.

  ngOnInit(): void {
    this.isLoggedIn = !!this.tokenStorageService.getUser();

    if (this.isLoggedIn) {
      const user = this.tokenStorageService.getUser();
      this.roles = user.roles;

      this.showAdminBoard = this.roles.includes('ROLE_ADMIN');
      this.showModeratorBoard = this.roles.includes('ROLE_MODERATOR');

      this.username = user.displayName;
    }
  }

Modify Auth Service

auth.service.ts

Add a new field using2FA in the Sign Up request

Add a new method to call the /verify REST service for TOTP verification.

  register(user): Observable<any> {
    return this.http.post(AppConstants.AUTH_API + 'signup', {
      displayName: user.displayName,
      email: user.email,
      password: user.password,
      matchingPassword: user.matchingPassword,
      socialProvider: 'LOCAL',
      using2FA: user.using2FA
    }, httpOptions);
  }
  
  verify(credentials): Observable<any> {
    return this.http.post(AppConstants.AUTH_API + 'verify', credentials.code, {
    	  headers: new HttpHeaders({ 'Content-Type': 'text/plain' })
    });
  }

Define Module

app.module.ts

Import and add TotpComponent in the module declarations

import { TotpComponent } from './totp/totp.component';

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

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

Define Module Routing

app-routing.module.ts

Import and add TotpComponent in the route declarations

import { TotpComponent } from './totp/totp.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: '', redirectTo: 'home', pathMatch: 'full' }
];

Run Spring Boot App with Maven

You can run the application using 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/spring-boot-angular-2fa-demo

Conclusion

That’s all folks. In this article, we have implemented 2 Factor Authentication in our Spring Boot Angular application.

Thank you for reading.

This Post Has One Comment

  1. supal

    Thanks a lot… It’s a complete base code to start any project. God bless you.

Leave a Reply