Generate TOTP Codes with Spring Boot and Google Authenticator
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.
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@SpringBootApplicationclass 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@SpringBootApplicationclass 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@SpringBootApplicationclass 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) } } @RestControllerclass 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@SpringBootApplicationclass 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) } } @RestControllerclass 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.