Generate and Verify TOTP in Spring Boot Applications.

In this blog post, we’ll delve into the world of Time-Based One-Time Passwords (TOTP). Specifically, we’ll learn how to generate and verify TOTP in Spring Boot applications.

What is TOTP

Before we dive into the coding part, let’s first understand what TOTP is. TOTP is a common mechanism for two-factor authentication (2FA), improving the security of a user’s account by requiring a second piece of evidence, in addition to the password. The TOTP is a unique, temporary passcode generated by an algorithm that uses the current time as a base.

TOTP combines a secret key with the current timestamp, applying a cryptographic algorithm to generate a one-time password. In the context of 2FA, this strengthens security by adding an additional layer of authentication, requiring the user to provide something they know (their password) and something they have (the generated TOTP).

In this blog post, instead of using TOTP library, we’ll explore how to implement a TOTP generator in Spring Boot applications.

Here are the steps to generate a TOTP:

1. **Initialization:** First, you need to set up the shared secret. The shared secret is usually a random set of bytes and is the same on the server and the client. It is often shared with the client as a QR code during initialization.

2. **Generate UNIX Timestamp:** The server and the client both generate a UNIX timestamp representing the current time. The UNIX timestamp is the number of seconds that have passed since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.

3. **Calculate Time Step:** The UNIX timestamp is then divided by a time-step value (X). The default time-step value recommended by the TOTP specification (RFC 6238) is 30 seconds. This is how the TOTP algorithm converts the current time into an interval index (T). “` T = (current Unix timestamp – T0) / X “` Here, T0 is the Unix time to start counting time steps (usually 0). X is the time step in seconds (default is 30).

4. **Truncate to Integer:** The calculated value (T) is then truncated to an integer.

5. **Generate HOTP Value:** Using the calculated integer (T) as the counter, generate an HMAC-SHA1 value, just like you would for the HOTP algorithm. The key for the HMAC is the shared secret, and the message is the counter.

6. **Truncate the HMAC:** Truncate the HMAC-SHA1 value to a user-friendly value. This is done by taking the last 4 bits of the HMAC to use as an offset, then take the 4-byte number from the HMAC at the offset (ignoring the most significant bit), to get a dynamic binary code.

7. **Reduce to Desired Digit Length:** Finally, the dynamic binary code is reduced to the desired number of digits by taking the code modulo 10^Digits. For example, if we want 6 digit codes, we would calculate `dynamic_binary_code % 1_000_000`

8. **Send the TOTP:** The generated TOTP is then sent to the user or checked against the user’s input.

Creating the TOTP Service

let’s create a service to generate the TOTP.

@Service
public class TotpService {

    private static final int OTP_LENGTH = 6;
    private static final int TIME_STEP = 30;

    public String generateSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        return new Base32().encodeToString(bytes);
    }


    public  int generateTotp(String secretKey, int timeOffset) throws NoSuchAlgorithmException,
            InvalidKeyException {
        Instant instant = Instant.now();
        long timeSlice =  (instant.getEpochSecond() / TIME_STEP) + timeOffset;
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putLong(timeSlice);
        byte[] timeBytes = buffer.array();

        byte[] hmacResult = hmacSha1(new Base32().decode(secretKey), timeBytes);

        int offset = hmacResult[hmacResult.length - 1] & 0x0F;
        byte[] codeBytes = Arrays.copyOfRange(hmacResult, offset, offset + 4);
        codeBytes[0] &= 0x7F;

        int code = ByteBuffer.wrap(codeBytes).getInt();
        return code % (int) Math.pow(10, OTP_LENGTH);
    }

    private  byte[] hmacSha1(byte[] key, byte[] timeBytes) throws InvalidKeyException,
            NoSuchAlgorithmException {
        Mac hmac = Mac.getInstance("HmacSHA1");
        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
        hmac.init(signKey);
        return hmac.doFinal(timeBytes);
    }

    public boolean verify(int inputOtp,String secret) throws NoSuchAlgorithmException, InvalidKeyException {
       return  this.verify(inputOtp,secret, 1, 1);
    }

    public boolean verify(int inputOtp,String secret, int lookAheadOffset,
                          int lookBehindOffset) throws NoSuchAlgorithmException, InvalidKeyException {

        for(int i = lookAheadOffset; i >=  -lookBehindOffset; --i) {
            int generatedOpt = this.generateTotp(secret, i);
            if (generatedOpt == inputOtp) {
                return true;
            }
        }

        return false;

    }
}
Code language: Java (java)

The generateSecretKey – method creates a random base32 encoded secret key.

The `verify` – method verifies a provided TOTP against a secret key.

Verifying the one-time password

Verify method needs to take the following into account:

verify method uses clock on the server to generate TOTP while authenticator app uses clock on your computer or mobile. The clock on the server and on your mobile may not match.

One more issue we will face is since each TOTP is only valid for 30 seconds, by the time it gets to the validator, it is no longer valid, even if the clock matches between the validator and the generator.

Due to these issues, the validator often accepts OTPs generated within a time range.

In verify method we are generating OTP for

current time

current time + 30 seconds (lookAheadOffset)

current time – 30 seconds (lookBehindOffset)

The `generateTotp` – method generates a TOTP using the provided secret key and offset

Now take a deep look at ‘generateTotp‘ method.

It performs following four steps as per the algorithm.

  • Calculate current time step as current time in 30 sec intervals
  • Generate HMAC SHA1 of time step based on key
  • Extract 4 bytes from hash as offset code
  • Truncate code to 6 digits to get final TOTP

While most of the code in generateOtp method is self explanatory, two lines of code need some explanation for understanding the algorithm implementation.

int offset = hmacResult[hmacResult.length - 1] & 0x0F;Code language: Java (java)

This line is calculating the offset into the HMAC result array where we will start extracting the OTP code bytes from.

  • We take the last byte of the hmacResult array by doing hmacResult.length - 1.
  • The last byte is ANDed with 0x0F to mask out all but the last 4 bits. This gives us a number between 0-15.
  • This masked value is used as the starting offset into the hmacResult array to read the 4 bytes for the OTP code.
 codeBytes[0] &= 0x7F;Code language: Java (java)

Once we have extracted the 4 bytes from hmacResult into codeBytes, this line masks the first code byte.

  • Per the TOTP RFC, the most significant bit of the first byte should be cleared to ensure the range of TOTP values.
  • 0x7F in binary is 0111 1111. By ANDing this with the first byte, it will clear the MSB while keeping other bits intact.
  • This makes sure the TOTP value can only range from 0 to 2^31 – 1.

So in summary, these operations are extracting the offset for the OTP bytes, and clearing the sign bit from the first byte to constrain value ranges as required by the TOTP standard.

Creating Employee Service

@Service
public class EmployeeService {

    @Autowired
    EmployeeRepository employeeRepository;

    @Autowired QRCodeService qrCodeService;

    @Autowired
    TotpService totpService;


    public Employee getEmployeeById(Integer id) {
        return employeeRepository.findById(id).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Employee not found with id :" + id));
    }

    public byte[] enableOtp(Integer id) throws WriterException, UnsupportedEncodingException {
      Employee emp =   employeeRepository.findById(id).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Employee not found with id :" + id));
      if(emp.getOtp_secret() != null) {
         throw  new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,"TOTP is already enabled for " +
                 "Employee with id :" + id);
      }
        String encodedSecret = totpService.generateSecretKey();
        emp.setOtp_secret(encodedSecret);
        employeeRepository.save(emp);
        String otpUri = String.format("otpauth://totp/%s?secret=%s", URLEncoder.encode(
                "FullStackCode", StandardCharsets.UTF_8),
                encodedSecret);
        return qrCodeService.generateQRCode(otpUri);

    }

    public boolean verifyOtp(Integer empId, String otp) {
        Employee emp =   employeeRepository.findById(empId).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Employee not found with id :" + empId));
        try {
           return  totpService.verify(Integer.parseInt(otp),emp.getOtp_secret());

        } catch (NoSuchAlgorithmException | InvalidKeyException e ) {
            throw new RuntimeException(e);
        }
    }

}Code language: Java (java)

The enableOtp – method enables TOTP for employee based and sends secret in the form of QR Code which can be scanned by Authenticator apps like Google Authenticator, MicroSoft Authenticator.

Creating QR Service

@Service
public class QRCodeService {

    @Autowired
    QRCodeWriter qrCodeWriter;

    public byte[] generateQRCode(String secret) throws WriterException {

        //     QRCodeWriter qrCodeWriter = new QRCodeWriter();
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);

        BitMatrix bitMatrix = qrCodeWriter.encode(secret,
                BarcodeFormat.QR_CODE, 200, 200,
                hints);


        BufferedImage qrCodeImage = MatrixToImageWriter.toBufferedImage(bitMatrix);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            ImageIO.write(qrCodeImage, "png", outputStream);
        } catch (IOException e) {
            throw new RuntimeException("Failed to write QR code image to output stream.", e);
        }

        return outputStream.toByteArray();
    }
}
Code language: Java (java)

Creating Employee Controller

@RestController
@RequestMapping("/employee")
public class EmployeeController {

   public  record OtpVerifyRequest(String otp){};
    @Autowired
    EmployeeService employeeService;

    @GetMapping("{id}")
    public Employee getEmployee(@PathVariable Integer id) {
        return employeeService.getEmployeeById(id);
    }

    @GetMapping(value = "{empId}/enableOtp",produces = MediaType.IMAGE_PNG_VALUE)
    public byte[] enableOtp(@PathVariable(value="empId") Integer id) throws WriterException, UnsupportedEncodingException {
        return employeeService.enableOtp(id);
    }

    @PostMapping(value = "{empId}/verifyOtp")
    public boolean verifyOtp(@PathVariable(value="empId") Integer empId,
                             @RequestBody OtpVerifyRequest otpRequest)  {
        return employeeService.verifyOtp(empId,otpRequest.otp);
    }
}
Code language: Java (java)

Using TOTP Library to verify OTP

Now let’s look at one example using TOTP library

There are many libraries available generating and verifying TOTP. For our example we are going to use following library.

Add Maven dependency in your pom.xml

		<dependency>
			<groupId>dev.uni-hamburg</groupId>
			<artifactId>timedrift-totp-java</artifactId>
			<version>2.0.1</version>
		</dependency>Code language: Java (java)

Creating TOTP service class

@Service
public class TotpService2 {

    public String generateSecretKey() {
        return Base32.random();
    }

    public String generateUriForQRCode(String secretKey,String name) {
        TimeDriftTotp totp = new TimeDriftTotp(secretKey);
        return totp.uri(name);
    }

    public String generateTotp(String secretKey) {
        TimeDriftTotp totp = new TimeDriftTotp(secretKey);
        totp.uri("suresh");
        return totp.now();
    }

    public boolean verifyTotp( String totp,String secretKey) {
        TimeDriftTotp validator = new TimeDriftTotp(secretKey,1,1);
        return validator.verify(totp);
    }
}
Code language: Java (java)

Using new TOTP service

@Service
public class EmployeeService {

    @Autowired
    EmployeeRepository employeeRepository;

    @Autowired QRCodeService qrCodeService;
  

    @Autowired
    TotpService2 totpService2;


    public Employee getEmployeeById(Integer id) {
        return employeeRepository.findById(id).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Employee not found with id :" + id));
    }
   

    public byte[] enableOtp2(Integer id) throws WriterException, UnsupportedEncodingException {
        Employee emp =   employeeRepository.findById(id).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Employee not found with id :" + id));
        if(emp.getOtp_secret() != null) {
            throw  new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,"TOTP is already enabled for " +
                    "Employee with id :" + id);
        }
        String encodedSecret = totpService2.generateSecretKey();   
        emp.setOtp_secret(encodedSecret);
        employeeRepository.save(emp);
        return qrCodeService.generateQRCode( totpService2.generateUriForQRCode(encodedSecret,emp.getFirst_name()));

    }

    public boolean verifyOtp2(Integer empId, String otp) {
        Employee emp =   employeeRepository.findById(empId).orElseThrow(
                () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Employee not found with id :" + empId));
        return  totpService2.verifyTotp(otp,emp.getOtp_secret());

    }

}
Code language: Java (java)

You can download source code for this blog post from GitHub

Similar Posts