DEV Community

Cover image for Securing Your App: TOTP Authentication with Spring Boot and Angular — Part Two— Implementation & Demo
Anbumani
Anbumani

Posted on • Updated on • Originally published at Medium

Securing Your App: TOTP Authentication with Spring Boot and Angular — Part Two— Implementation & Demo

This is a continuation of Part 1 — Overview and Project Setup. Please go through it if you have not.

Register Endpoint Implementation

As the first step let’s implement the register endpoint. Below operations involved in this process,
The controller delegates the service request.

@PostMapping("/register")
    public ResponseEntity<?> register(@Validated @RequestBody User user) {
        // Register User // Generate QR code using the Secret KEY
        try {
            return ResponseEntity.ok(userService.registerUser(user));
        } catch (QrGenerationException e) {
            return ResponseEntity.internalServerError().body("Something went wrong. Try again.");
        }
    }
Enter fullscreen mode Exit fullscreen mode

Service layer for Registration.

  1. Check the user already exists in DB with the same username.
  2. Hash the password
  3. Create a Secret key that is used for TOTP generation
  4. Save these data into DB
  5. Generate QR code and respond.
@Override
    public MfaTokenData registerUser(User user) throws UserAlreadyExistException, QrGenerationException {
        try{
            if (userRepository.findByUsername(user.getUsername()).isPresent()) {
                throw new UserAlreadyExistException("Username already exists");
            }
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            //some additional work
            user.setSecretKey(totpManager.generateSecretKey()); //generating the secret and store with profile
            User savedUser = userRepository.save(user);

            //Generate the QR Code
            String qrCode = totpManager.getQRCode(savedUser.getSecretKey());
            return MfaTokenData.builder()
                    .mfaCode(savedUser.getSecretKey())
                    .qrCode(qrCode)
                    .build();
        } catch (Exception e){
            throw new MFAServerAppException("Exception while registering the user", e);
        }
    }
Enter fullscreen mode Exit fullscreen mode

The TOTP Manager is responsible for generating the secret key and QR code. Let’s create the beans for this. By default, the secret generator will generate 32 32-byte secret keys. it can be overridden by passing a constructor argument.

@Bean
    public SecretGenerator secretGenerator(){
        return new DefaultSecretGenerator();
    }

    @Bean
    public QrGenerator qrGenerator(){
        return new ZxingPngQrGenerator();
    }
Enter fullscreen mode Exit fullscreen mode

TOTP Manager Implementation. We create a QR code with all the required details for the TOTP generation. Below QR code below contains the secret, number of digits, period, and algorithm. These details help the Authenticator app generate a TOTP that matches the server.

@Override
public String generateSecretKey() {
    return secretGenerator.generate(); // 32 Byte Secret Key
}

@Override
public String getQRCode(String secret) throws QrGenerationException {
    QrData qrData = new QrData.Builder().label("2FA Server")
            .issuer("Youtube 2FA Demo")
            .secret(secret)
            .digits(6)
            .period(30)
            .algorithm(HashingAlgorithm.SHA512)
            .build();

    return Utils.getDataUriForImage(qrGenerator.generate(qrData), qrGenerator.getImageMimeType());
}
Enter fullscreen mode Exit fullscreen mode

Now Registration endpoint is ready. Let’s try with UI.

Registration UI with details keyed

The below screenshot shows the QR code returned after successful registration.

After successful registration of the user

Mongo DB with User data persisted.

user persistance

we have completed the registration successfully. Now user scans the QR code with the authenticator app to generate the one-time password. Let's create the login and verifyTotp endpoints.

Login endpoint Implementation

The controller delegates the request to service and responds with a response.

Here we respond with all the required parameters for UI to determine the user needs to be validated again on 2FA, user is validated, JWT, and message.

@PostMapping(value = "/login", produces = "application/json")
    public ResponseEntity<?> login(@Validated @RequestBody LoginRequest loginRequest) {
        // Validate the user credentials and return the JWT / send redirect to MFA page
        try {//Get the user and Compare the password
            Authentication authentication = authenticationProvider.authenticate(
                    new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
            );
            SecurityContextHolder.getContext().setAuthentication(authentication);
            User user = (User) authentication.getPrincipal();
            return ResponseEntity.ok(MfaVerificationResponse.builder()
                    .username(loginRequest.getUsername())
                    .tokenValid(Boolean.FALSE)
                    .authValid(Boolean.TRUE)
                    .mfaRequired(Boolean.TRUE)
                    .message("User Authenticated using username and Password")
                    .jwt("")
                    .build());

        } catch (Exception e){
            return ResponseEntity.ok(MfaVerificationResponse.builder()
                    .username(loginRequest.getUsername())
                    .tokenValid(Boolean.FALSE)
                    .authValid(Boolean.FALSE)
                    .mfaRequired(Boolean.FALSE)
                    .message("Invalid Credentials. Please try again.")
                    .jwt("")
                    .build());
        }
    }
Enter fullscreen mode Exit fullscreen mode

We are using an authentication provider from Spring Security that validates the user. Alternatively, you can directly validate the password and respond as we are going to implement JWT in the upcoming blog.

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        Optional<User> userDetails = userRepository.findByUsername(username);
        if(userDetails.isPresent()){
            if (userDetails != null && passwordEncoder.matches(password, userDetails.get().getPassword())) {
                return new UsernamePasswordAuthenticationToken(userDetails.get(), password);
            } else {
                throw new BadCredentialsException("Invalid password");
            }
        } else {
            throw new UsernameNotFoundException("Username Not Found");
        }
    }
Enter fullscreen mode Exit fullscreen mode

Let’s validate the login page using /login API.

Login Screen with message for invalid credentials

Let’s enter valid credentials and it navigates us to the 2FA page.

User prompted for 2FA upon successful login

This shows the login is working as expected. Let's move on to create the verifyTotp endpoint.

VerifyTotp Implementation

The controller delegates the request to the service layer and responds back with the response from the service

@PostMapping("/verifyTotp")
public ResponseEntity<?> verifyTotp(@Validated @RequestBody MfaVerificationRequest request) {
    MfaVerificationResponse mfaVerificationResponse = MfaVerificationResponse.builder()
            .username(request.getUsername())
            .tokenValid(Boolean.FALSE)
            .message("Token is not Valid. Please try again.")
            .build();
    // Validate the OTP
    if(userService.verifyTotp(request.getTotp(), request.getUsername())){
        mfaVerificationResponse = MfaVerificationResponse.builder()
                .username(request.getUsername())
                .tokenValid(Boolean.TRUE)
                .message("Token is valid")
                .jwt("DUMMYTOKEN")
                .build();
    }
    return ResponseEntity.ok(mfaVerificationResponse);
}
Enter fullscreen mode Exit fullscreen mode

The service layer makes use of a code verifier from samssteven.totp dependency to validate. User secret will be queried from DB using the username from the payload

@Override
public boolean verifyTotp(String code, String username) {
    User user = userRepository.findByUsername(username).get();
    return totpManager.verifyTotp(code, user.getSecretKey());
}
Enter fullscreen mode Exit fullscreen mode
 @Override
    public boolean verifyTotp(String code, String secret) {
        return myCodeVerifier.isValidCode(secret, code);
    }
Enter fullscreen mode Exit fullscreen mode

We will create a bean of it with the same values which was used for QR code generation.

Code (TOTP) can be verified with time and the code generation algorithm. We will create the object of Time Provider and Code Generator as below. All the parameters must be the same as the QR code data.

@Bean
    public CodeVerifier myCodeVerifier(){
        // Time
        TimeProvider timeProvider = new SystemTimeProvider();
        // Code Generator
        CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA512, 6);
        DefaultCodeVerifier codeVerifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
        codeVerifier.setTimePeriod(30);
        codeVerifier.setAllowedTimePeriodDiscrepancy(2);
        return  codeVerifier;
    }
Enter fullscreen mode Exit fullscreen mode

Let’s check this out from UI. I have entered the OTP generated from the authenticator app.

key in TOTP from authenticator app

Upon submission it successfully allowed me into the application.

Home page after successful TOTP

Kudos!!! We have successfully implemented the TOTP as 2FA into our application. Please feel free to refer to the GitHub repo.

Mfaapplication

Application developed using Angular 14 and Bootstrap.

Components involved.

  • Login
  • Register
  • TOTP
  • Home Module

GitHub logo amaialth / mfaserver

Spring boot backend for MFA

MFA Server

Application for 2FA demo.

APIs Involved

  • login
  • register
  • verifyTotp
  • confrim-email

Dependencies

  • Spring Security
  • Spring Web
  • dev.samstevens.totp



Top comments (0)