Generate TOTP Codes with Spring Boot and Google Authenticator

Posted on Feb 12, 2021 in
Reading time: 4 minutes

Intro

Securing applications with a username and a password is often not enough. This is where a second factor comes into play. While a password is the first factor, and something that you know, a second factor is something that you have.

The thing that you have is often a mobile phone, which can be used to generate a one-time password. Other options include hardware tokens, such as the Yubikey. But in this tutorial we will focus on generating time-based one-time passwords (TOTP) using Google Authenticator.

Passwords are derived from a secret key, which is shared between the server and the client. Each applies a hashing function that considers the current time, and the secret key. The result is a six-digit number that changes every 30 seconds.

🍿 Watch on YouTube or get the code from GitHub

Setup

We will be using Spring Boot to create a simple web application that is serving QR codes that can be read using Google Authenticator. The QR code contains the secret key and is encoded using a URI with the following format:

otpauth://TYPE/LABEL?PARAMETERS

In our example this will look something like this:

otpauth://totp/Spring:hello@axgr.dev?secret=JBSWY3DPEHPK3PXP&issuer=Spring

Let’s Code

Our app generates a shared secret upon start, and we are dumping it to the logs, so we can verify it. Based on the secret we are generating a one-time password every second, though it will only change every 30 seconds. This is what we are using the scheduled task for.

@EnableScheduling
@SpringBootApplication
class App {
 
companion object {
private val log = LoggerFactory.getLogger(App::class.java)
}
 
private val secret = GoogleAuthenticator.createRandomSecret()
 
init {
log.info("Secret: $secret")
}
 
@Scheduled(fixedRate = 1_000L)
fun ping() {
val timestamp = Date(System.currentTimeMillis())
val code = GoogleAuthenticator(secret).generate(timestamp)
log.info("Code: $code")
}
 
}

Next up is the CodeGenerator that will encode the secret into a QR code. It accepts the issuer, email, and secret as parameters, puts them into the correct URI format and encodes it using the QRCodeWriter. The QRCodeWriter is provided by the ZXing library, and we have to add it to the Spring context, so we can then inject it into the CodeGenerator.

@EnableScheduling
@SpringBootApplication
class App {
 
companion object {
private val log = LoggerFactory.getLogger(App::class.java)
}
 
private val secret = GoogleAuthenticator.createRandomSecret()
 
init {
log.info("Secret: $secret")
}
 
@Scheduled(fixedRate = 1_000L)
fun ping() {
val timestamp = Date(System.currentTimeMillis())
val code = GoogleAuthenticator(secret).generate(timestamp)
log.info("Code: $code")
}
 
@Bean
fun qrCodeWriter() = QRCodeWriter()
 
}
 
@Component
class CodeGenerator(private val writer: QRCodeWriter) {
 
fun generate(issuer: String, email: String, secret: String): BufferedImage {
val uri = "otpauth://totp/$issuer:$email?secret=$secret&issuer=$issuer"
val matrix = writer.encode(uri, BarcodeFormat.QR_CODE, 200, 200)
 
return MatrixToImageWriter.toBufferedImage(matrix)
}
 
}
 

Now that we got the image ready, we need to expose it via a controller. This is what the CodeController will be doing and we annotate it with the @RestController annotation.

@EnableScheduling
@SpringBootApplication
class App {
 
companion object {
private val log = LoggerFactory.getLogger(App::class.java)
}
 
private val secret = GoogleAuthenticator.createRandomSecret()
 
init {
log.info("Secret: $secret")
}
 
@Scheduled(fixedRate = 1_000L)
fun ping() {
val timestamp = Date(System.currentTimeMillis())
val code = GoogleAuthenticator(secret).generate(timestamp)
log.info("Code: $code")
}
 
@Bean
fun qrCodeWriter() = QRCodeWriter()
 
}
 
@Component
class CodeGenerator(private val writer: QRCodeWriter) {
 
fun generate(issuer: String, email: String, secret: String): BufferedImage {
val uri = "otpauth://totp/$issuer:$email?secret=$secret&issuer=$issuer"
val matrix = writer.encode(uri, BarcodeFormat.QR_CODE, 200, 200)
 
return MatrixToImageWriter.toBufferedImage(matrix)
}
 
}
 
@RestController
class CodeController(private val generator: CodeGenerator) {
 
@GetMapping("/code/{secret}", produces = [MediaType.IMAGE_PNG_VALUE])
fun code(@PathVariable secret: String): BufferedImage {
return generator.generate("Spring", "hello@axgr.dev", secret)
}
 
}
 

Almost there! There is just one piece missing. Spring uses message converters to translate objects into HTTP messages. While the framework boots a handful of default converters, this is not the case for the BufferedImage conversion. But there is a component available, that we can activate by simply adding it to the Spring context as well, the BufferedImageHttpMessageConverter.

@EnableScheduling
@SpringBootApplication
class App {
 
companion object {
private val log = LoggerFactory.getLogger(App::class.java)
}
 
private val secret = GoogleAuthenticator.createRandomSecret()
 
init {
log.info("Secret: $secret")
}
 
@Scheduled(fixedRate = 1_000L)
fun ping() {
val timestamp = Date(System.currentTimeMillis())
val code = GoogleAuthenticator(secret).generate(timestamp)
log.info("Code: $code")
}
 
@Bean
fun qrCodeWriter() = QRCodeWriter()
 
@Bean
fun imageConverter(): HttpMessageConverter<BufferedImage> {
return BufferedImageHttpMessageConverter()
}
 
}
 
@Component
class CodeGenerator(private val writer: QRCodeWriter) {
 
fun generate(issuer: String, email: String, secret: String): BufferedImage {
val uri = "otpauth://totp/$issuer:$email?secret=$secret&issuer=$issuer"
val matrix = writer.encode(uri, BarcodeFormat.QR_CODE, 200, 200)
 
return MatrixToImageWriter.toBufferedImage(matrix)
}
 
}
 
@RestController
class CodeController(private val generator: CodeGenerator) {
 
@GetMapping("/code/{secret}", produces = [MediaType.IMAGE_PNG_VALUE])
fun code(@PathVariable secret: String): BufferedImage {
return generator.generate("Spring", "hello@axgr.dev", secret)
}
 
}

If you start the application and navigate to http://localhost:8080/code/UZVD3BPY7BORLMM3 in your browser, you should see a QR code. This code can be scanned using Google Authenticator, and you should see a new entry in the app. If you provide the same secret that the app has generated, you should see the same code in the app and the logs.

The Dead Letter Queue

Love what you're seeing? By subscribing to my newsletter, not only will you be the first to know about fresh tutorials and videos, but you'll also unlock:

Subscribe now and become a part of our growing tech tribe!