Add TOTP Authentication to Your Java API with Quarkus and Vault Dev Services
Learn how to secure REST endpoints using Vault’s TOTP engine, Quarkus Dev Services, and QR codes for Google Authenticator with minimal setup.
What if you could add multi-factor authentication (MFA) to your Quarkus API in under 15 minutes. Without installing Vault, without writing raw HTTP requests, and without pulling your hair out over QR code generation? You can. And you're going to.
In this hands-on guide, we’ll build a REST API in Quarkus protected by a time-based one-time password (TOTP) generated by HashiCorp Vault and compatible with Google Authenticator. We’ll use Dev Services so Vault spins up automatically, and we’ll display a QR code in the browser to onboard users easily.
Let’s go full secure mode. MFA for developers, by developers.
What You’ll Build
POST /totp/register/{username}
— Generates a Vault-backed TOTP key and returns a QR code to scan.GET /protected
— Requires a valid TOTP token and username header to grant access.Vault Dev Services spins up Vault automagically. No manual setup. No CLI voodoo.
Google Authenticator (or any other 2FAS app) becomes your second factor.
Prerequisites
All you need:
Java 17+
Podman (for Quarkus DevServices)
Google Authenticator (or Authy, or anything that scans
otpauth://
)
Step 1: Bootstrap the Project
We start with a clean Quarkus app:
quarkus create app org.acme:totp-vault \
--no-code \
--extension="rest,vault"
cd totp-vault
This gives you:
Quarkus REST for HTTP endpoints.
Quarkus Vault for secret management.
Step 2: Configure Vault Dev Services
Let’s tell Quarkus to spin up Vault for us.
Create src/main/resources/application.properties
:
quarkus.vault.devservices.enabled=true
That’s it. When you run the app, Quarkus will fire up the vault server
for you.
Step 3: Create the Vault Service
This service will use Quarkus' VaultTOTPSecretEngine
to:
Register a TOTP key for a user.
Return the
otpauth://
URI.Validate incoming TOTP codes.
Create src/main/java/org/acme/VaultService.java
:
package org.acme;
import io.quarkus.vault.VaultTOTPSecretEngine;
import io.quarkus.vault.secrets.totp.CreateKeyParameters;
import io.quarkus.vault.secrets.totp.KeyDefinition;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Optional;
@ApplicationScoped
public class VaultService {
@Inject
VaultTOTPSecretEngine totpEngine;
public Optional<KeyDefinition> createTotpKey(String username) {
CreateKeyParameters params = new CreateKeyParameters("quarkus-demo", username);
params.setPeriod("1m");
params.setExported(true); // Ensures the otpauth URL is returned
return totpEngine.createKey(username, params);
}
public boolean validateCode(String username, String code) {
return totpEngine.validateCode(username, code);
}
}
This is your TOTP brain. Next, let’s use it.
Step 4: Show the QR Code
We’ll add an endpoint that:
Registers a TOTP key for a given username.
Converts the
otpauth://
URI into a QR code.Returns a little HTML page with the code in it.
Create src/main/java/org/acme/TotpResource.java
:
package org.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/totp")
public class TotpResource {
@Inject
VaultService vaultService;
@GET
@Path("/register/{username}")
@Produces(MediaType.TEXT_HTML)
public String register(@PathParam("username") String username) {
return vaultService.createTotpKey(username)
.map(key -> """
<!DOCTYPE html>
<html>
<head><title>Scan QR Code</title></head>
<body>
<h2>Scan this QR code with Google Authenticator</h2>
<img src="data:image/png;base64,%s" alt="TOTP QR Code"/>
</body>
</html>
""".formatted(key.getBarcode()))
.orElse("Failed to generate TOTP key");
}
}
We’re almost ready to get this application started now. But first, let’s configure the Vault dev-service a little more. We need to add the totp secrets engine.
Start the application in dev mode:
quarkus dev
You can find the Vault URL in the quarkus console log or via the dev-service by looking at the quarkus.vault.url configuration. It looks like: http://localhost:36733/.
Use your browser and log in with method “Token” and type “root” as user. Navigate to the Secrets Engines tap and “Enable New Engine” => TOTP.
Point your browser to:
http://localhost:8080/totp/register/alice
Open the returned HTML in a browser.
Scan it with your favorite TOTP app. Boom. You have your TOTP token ready to use:
Step 5: Protect Your Endpoint
Here’s the simplest protected resource ever. It just checks your username and TOTP code.
Create src/main/java/org/acme/ProtectedResource.java
:
package org.acme;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.Response;
@Path("/protected")
public class ProtectedResource {
@Inject
VaultService vaultService;
@GET
public Response access(@HeaderParam("X-User") String username,
@HeaderParam("X-TOTP-Code") String code) {
if (vaultService.validateCode(username, code)) {
return Response.ok("Access granted").build();
} else {
return Response.status(Response.Status.UNAUTHORIZED).entity("Invalid TOTP code").build();
}
}
}
Try it:
curl -H "X-User: alice" \
-H "X-TOTP-Code: 123456" \
http://localhost:8080/protected
(Substitute 123456
with the real code from Google Authenticator.)
If the code is correct and matches what Vault expects, you're in. If not: You’re out. Like MFA should be.
Summary
You just added real MFA to a Java REST API using:
Vault Dev Services to run Vault without hassle.
VaultTOTPSecretEngine to securely manage TOTP keys.
Minimal code. Maximum impact.
This is enterprise-grade auth, boiled down for developers who value clarity over complexity.
Here’s what you could add next
Here’s how you could level this up:
Add real user management and persistence.
Integrate TOTP checks into JWT or session-based flows.
Hook into OpenID Connect or OAuth flows for 2FA.
Want the source code in a ready-to-run repository? Already made available!