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.