diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java
new file mode 100644
index 0000000000000..d592bded66222
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java
@@ -0,0 +1,64 @@
+package io.quarkus.vault;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.vault.secrets.totp.CreateKeyParameters;
+import io.quarkus.vault.secrets.totp.KeyConfiguration;
+import io.quarkus.vault.secrets.totp.KeyDefinition;
+
+/**
+ * This service provides access to the TOTP secret engine.
+ *
+ * @see TOTP Secrets Engine
+ */
+public interface VaultTOTPSecretEngine {
+
+ /**
+ * Creates or updates a key definition.
+ *
+ * @param name of the key.
+ * @param createKeyParameters required to create or update a key.
+ * @return Barcode and/or URL of the created OTP key.
+ */
+ Optional createKey(String name, CreateKeyParameters createKeyParameters);
+
+ /**
+ * Queries the key definition.
+ *
+ * @param name of the key.
+ * @return The key configuration.
+ */
+ KeyConfiguration readKey(String name);
+
+ /**
+ * Returns a list of available keys. Only the key names are returned, not any values.
+ *
+ * @return List of available keys.
+ */
+ List listKeys();
+
+ /**
+ * Deletes the key definition.
+ *
+ * @param name of the key.
+ */
+ void deleteKey(String name);
+
+ /**
+ * Generates a new time-based one-time use password based on the named key.
+ *
+ * @param name of the key.
+ * @return The Code.
+ */
+ String generateCode(String name);
+
+ /**
+ * Validates a time-based one-time use password generated from the named key.
+ *
+ * @param name of the key.
+ * @param code to validate.
+ * @return True if valid, false otherwise.
+ */
+ boolean validateCode(String name, String code);
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java
index 9157e05a2ddab..4bef6bf541b62 100644
--- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java
@@ -15,6 +15,7 @@ public class VaultManager {
private VaultDbManager vaultDbManager;
private VaultTransitManager vaultTransitManager;
private VaultCredentialsProvider vaultCredentialsProvider;
+ private VaultTOTPManager vaultTOTPManager;
public static VaultManager getInstance() {
return instance;
@@ -42,6 +43,7 @@ public VaultManager(VaultRuntimeConfig serverConfig, VaultClient vaultClient) {
this.vaultDbManager = new VaultDbManager(this.vaultAuthManager, this.vaultClient, serverConfig);
this.vaultTransitManager = new VaultTransitManager(this.vaultAuthManager, this.vaultClient, serverConfig);
this.vaultCredentialsProvider = new VaultCredentialsProvider(serverConfig, this.vaultKvManager, this.vaultDbManager);
+ this.vaultTOTPManager = new VaultTOTPManager(this.vaultAuthManager, this.vaultClient);
}
public VaultClient getVaultClient() {
@@ -60,6 +62,10 @@ public VaultKvManager getVaultKvManager() {
return vaultKvManager;
}
+ public VaultTOTPManager getVaultTOTPManager() {
+ return vaultTOTPManager;
+ }
+
public VaultTransitManager getVaultTransitManager() {
return vaultTransitManager;
}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java
index d714bf2e62f6b..f2bea2fe3749f 100644
--- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java
@@ -7,6 +7,7 @@
import io.quarkus.vault.CredentialsProvider;
import io.quarkus.vault.VaultKVSecretEngine;
+import io.quarkus.vault.VaultTOTPSecretEngine;
import io.quarkus.vault.VaultTransitSecretEngine;
import io.quarkus.vault.runtime.config.VaultRuntimeConfig;
@@ -25,6 +26,12 @@ public VaultTransitSecretEngine createTransitSecretEngine() {
return VaultManager.getInstance().getVaultTransitManager();
}
+ @Produces
+ @ApplicationScoped
+ public VaultTOTPSecretEngine createVaultTOTPSecretEngine() {
+ return VaultManager.getInstance().getVaultTOTPManager();
+ }
+
@Produces
@ApplicationScoped
@Named("vault-credentials-provider")
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java
new file mode 100644
index 0000000000000..a1f933879c91b
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java
@@ -0,0 +1,88 @@
+package io.quarkus.vault.runtime;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.vault.VaultTOTPSecretEngine;
+import io.quarkus.vault.runtime.client.VaultClient;
+import io.quarkus.vault.runtime.client.VaultClientException;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyBody;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPReadKeyResult;
+import io.quarkus.vault.secrets.totp.CreateKeyParameters;
+import io.quarkus.vault.secrets.totp.KeyConfiguration;
+import io.quarkus.vault.secrets.totp.KeyDefinition;
+
+public class VaultTOTPManager implements VaultTOTPSecretEngine {
+
+ private VaultAuthManager vaultAuthManager;
+ private VaultClient vaultClient;
+
+ public VaultTOTPManager(VaultAuthManager vaultAuthManager, VaultClient vaultClient) {
+ this.vaultAuthManager = vaultAuthManager;
+ this.vaultClient = vaultClient;
+ }
+
+ @Override
+ public Optional createKey(String name, CreateKeyParameters createKeyParameters) {
+ VaultTOTPCreateKeyBody body = new VaultTOTPCreateKeyBody();
+
+ body.accountName = createKeyParameters.getAccountName();
+ body.algorithm = createKeyParameters.getAlgorithm();
+ body.digits = createKeyParameters.getDigits();
+ body.exported = createKeyParameters.getExported();
+ body.generate = createKeyParameters.getGenerate();
+ body.issuer = createKeyParameters.getIssuer();
+ body.key = createKeyParameters.getKey();
+ body.keySize = createKeyParameters.getKeySize();
+ body.period = createKeyParameters.getPeriod();
+ body.qrSize = createKeyParameters.getQrSize();
+ body.skew = createKeyParameters.getSkew();
+ body.url = createKeyParameters.getUrl();
+
+ final VaultTOTPCreateKeyResult result = this.vaultClient
+ .createTOTPKey(getToken(), name, body);
+
+ return result == null ? Optional.empty() : Optional.of(new KeyDefinition(result.data.barcode, result.data.url));
+ }
+
+ @Override
+ public KeyConfiguration readKey(String name) {
+ final VaultTOTPReadKeyResult result = this.vaultClient.readTOTPKey(getToken(), name);
+ return new KeyConfiguration(result.data.accountName,
+ result.data.algorithm, result.data.digits,
+ result.data.issuer, result.data.period);
+ }
+
+ @Override
+ public List listKeys() {
+ try {
+ return this.vaultClient.listTOTPKeys(getToken()).data.keys;
+ } catch (VaultClientException e) {
+ if (e.getStatus() == 404) {
+ return Collections.emptyList();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public void deleteKey(String name) {
+ this.vaultClient.deleteTOTPKey(getToken(), name);
+ }
+
+ @Override
+ public String generateCode(String name) {
+ return this.vaultClient.generateTOTPCode(getToken(), name).data.code;
+ }
+
+ @Override
+ public boolean validateCode(String name, String code) {
+ return this.vaultClient.validateTOTPCode(getToken(), name, code).data.valid;
+ }
+
+ private String getToken() {
+ return vaultAuthManager.getClientToken();
+ }
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java
index b10cb2937a946..a5ea06f68fad2 100644
--- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java
@@ -10,6 +10,7 @@
import org.jboss.logging.Logger;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -31,6 +32,13 @@
import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesBody;
import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup;
import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyBody;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPGenerateCodeResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPListKeysResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPReadKeyResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPValidateCodeBody;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPValidateCodeResult;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecryptBody;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitEncrypt;
@@ -61,6 +69,7 @@ public OkHttpVaultClient(VaultRuntimeConfig serverConfig) {
this.client = createHttpClient(serverConfig);
this.url = serverConfig.url.get();
this.mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
+ this.mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
@Override
@@ -103,12 +112,12 @@ public void writeSecretV2(String token, String secretEnginePath, String path, Va
@Override
public void deleteSecretV1(String token, String secretEnginePath, String path) {
- delete(secretEnginePath + "/" + path, token, null, null, 204);
+ delete(secretEnginePath + "/" + path, token, 204);
}
@Override
public void deleteSecretV2(String token, String secretEnginePath, String path) {
- delete(secretEnginePath + "/data/" + path, token, null, null, 204);
+ delete(secretEnginePath + "/data/" + path, token, 204);
}
@Override
@@ -167,11 +176,61 @@ public VaultTransitEncrypt rewrap(String token, String keyName, VaultTransitRewr
return post("transit/rewrap/" + keyName, token, body, VaultTransitEncrypt.class);
}
+ @Override
+ public VaultTOTPCreateKeyResult createTOTPKey(String token, String keyName,
+ VaultTOTPCreateKeyBody body) {
+ String path = "totp/keys/" + keyName;
+
+ // Depending on parameters it might produce an output or not
+ if (body.isProducingOutput()) {
+ return post(path, token, body, VaultTOTPCreateKeyResult.class, 200);
+ } else {
+ post(path, token, body, 204);
+ return null;
+ }
+ }
+
+ @Override
+ public VaultTOTPReadKeyResult readTOTPKey(String token, String keyName) {
+ String path = "totp/keys/" + keyName;
+ return get(path, token, VaultTOTPReadKeyResult.class);
+ }
+
+ @Override
+ public VaultTOTPListKeysResult listTOTPKeys(String token) {
+ return list("totp/keys", token, VaultTOTPListKeysResult.class);
+ }
+
+ @Override
+ public void deleteTOTPKey(String token, String keyName) {
+ String path = "totp/keys/" + keyName;
+ delete(path, token, 204);
+ }
+
+ @Override
+ public VaultTOTPGenerateCodeResult generateTOTPCode(String token, String keyName) {
+ String path = "totp/code/" + keyName;
+ return get(path, token, VaultTOTPGenerateCodeResult.class);
+ }
+
+ @Override
+ public VaultTOTPValidateCodeResult validateTOTPCode(String token, String keyName,
+ String code) {
+ String path = "totp/code/" + keyName;
+ VaultTOTPValidateCodeBody body = new VaultTOTPValidateCodeBody(code);
+ return post(path, token, body, VaultTOTPValidateCodeResult.class);
+ }
+
// ---
- protected T delete(String path, String token, Object body, Class resultClass, int expectedCode) {
- Request request = builder(path, token).delete(requestBody(body)).build();
- return exec(request, resultClass, expectedCode);
+ protected T list(String path, String token, Class resultClass) {
+ Request request = builder(path, token).method("LIST", null).build();
+ return exec(request, resultClass);
+ }
+
+ protected T delete(String path, String token, int expectedCode) {
+ Request request = builder(path, token).delete().build();
+ return exec(request, expectedCode);
}
protected T post(String path, String token, Object body, Class resultClass, int expectedCode) {
@@ -184,6 +243,11 @@ protected T post(String path, String token, Object body, Class resultClas
return exec(request, resultClass);
}
+ protected T post(String path, String token, Object body, int expectedCode) {
+ Request request = builder(path, token).post(requestBody(body)).build();
+ return exec(request, expectedCode);
+ }
+
protected T put(String path, String token, Object body, Class resultClass) {
Request request = builder(path, token).put(requestBody(body)).build();
return exec(request, resultClass);
@@ -198,6 +262,10 @@ private T exec(Request request, Class resultClass) {
return exec(request, resultClass, 200);
}
+ private T exec(Request request, int expectedCode) {
+ return exec(request, null, expectedCode);
+ }
+
private T exec(Request request, Class resultClass, int expectedCode) {
try (Response response = client.newCall(request).execute()) {
if (response.code() != expectedCode) {
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java
index f7b20c363d927..6d1c51f694b1c 100644
--- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java
@@ -13,6 +13,12 @@
import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2WriteBody;
import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup;
import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyBody;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPGenerateCodeResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPListKeysResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPReadKeyResult;
+import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPValidateCodeResult;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecryptBody;
import io.quarkus.vault.runtime.client.dto.transit.VaultTransitEncrypt;
@@ -68,4 +74,15 @@ VaultTransitVerify verify(String token, String keyName, String hashAlgorithm,
VaultTransitEncrypt rewrap(String token, String keyName, VaultTransitRewrapBody body);
+ VaultTOTPCreateKeyResult createTOTPKey(String token, String keyName, VaultTOTPCreateKeyBody vaultTOTPCreateKeyBody);
+
+ VaultTOTPReadKeyResult readTOTPKey(String token, String keyName);
+
+ VaultTOTPListKeysResult listTOTPKeys(String token);
+
+ void deleteTOTPKey(String token, String keyName);
+
+ VaultTOTPGenerateCodeResult generateTOTPCode(String token, String keyName);
+
+ VaultTOTPValidateCodeResult validateTOTPCode(String token, String keyName, String code);
}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java
new file mode 100644
index 0000000000000..6ac0739a573c0
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java
@@ -0,0 +1,38 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPCreateKeyBody implements VaultModel {
+
+ public Boolean generate;
+ public Boolean exported;
+
+ @JsonProperty("key_size")
+ public Integer keySize;
+
+ public String url;
+ public String key;
+ public String issuer;
+
+ @JsonProperty("account_name")
+ public String accountName;
+
+ public String period;
+ public String algorithm;
+ public Integer digits;
+ public Integer skew;
+
+ @JsonProperty("qr_size")
+ public Integer qrSize;
+
+ public boolean isProducingOutput() {
+ // When exported is not set, by default is true
+ return is(exported, true) && is(generate, false);
+ }
+
+ private boolean is(Boolean v, boolean defaultValue) {
+ return v == null ? defaultValue : v;
+ }
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java
new file mode 100644
index 0000000000000..d2df951dbd2d7
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java
@@ -0,0 +1,10 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPCreateKeyData implements VaultModel {
+
+ public String barcode;
+ public String url;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java
new file mode 100644
index 0000000000000..5c052e9a44a24
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java
@@ -0,0 +1,9 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPCreateKeyResult implements VaultModel {
+
+ public VaultTOTPCreateKeyData data;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java
new file mode 100644
index 0000000000000..0530d6cad5d91
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java
@@ -0,0 +1,9 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPGenerateCodeData implements VaultModel {
+
+ public String code;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java
new file mode 100644
index 0000000000000..d2f80a957216c
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java
@@ -0,0 +1,9 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPGenerateCodeResult implements VaultModel {
+
+ public VaultTOTPGenerateCodeData data;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java
new file mode 100644
index 0000000000000..3d0efb51c5bb9
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java
@@ -0,0 +1,11 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import java.util.List;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPListKeysData implements VaultModel {
+
+ public List keys;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java
new file mode 100644
index 0000000000000..eeef9c4279f2b
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java
@@ -0,0 +1,6 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.AbstractVaultDTO;
+
+public class VaultTOTPListKeysResult extends AbstractVaultDTO {
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java
new file mode 100644
index 0000000000000..3b34802de9812
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java
@@ -0,0 +1,17 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPReadKeyData implements VaultModel {
+
+ @JsonProperty("account_name")
+ public String accountName;
+
+ public String algorithm;
+ public int digits;
+ public String issuer;
+ public int period;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java
new file mode 100644
index 0000000000000..6c7e11a577fcb
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java
@@ -0,0 +1,9 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPReadKeyResult implements VaultModel {
+
+ public VaultTOTPReadKeyData data;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java
new file mode 100644
index 0000000000000..f72ab71676c98
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java
@@ -0,0 +1,13 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPValidateCodeBody implements VaultModel {
+
+ public VaultTOTPValidateCodeBody(String code) {
+ this.code = code;
+ }
+
+ public String code;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java
new file mode 100644
index 0000000000000..f8d7014d7ae3f
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java
@@ -0,0 +1,9 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPValidateCodeData implements VaultModel {
+
+ public boolean valid;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java
new file mode 100644
index 0000000000000..91dd0e1c5ded1
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java
@@ -0,0 +1,9 @@
+package io.quarkus.vault.runtime.client.dto.totp;
+
+import io.quarkus.vault.runtime.client.dto.VaultModel;
+
+public class VaultTOTPValidateCodeResult implements VaultModel {
+
+ public VaultTOTPValidateCodeData data;
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java
new file mode 100644
index 0000000000000..39140d55cf2bd
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java
@@ -0,0 +1,129 @@
+package io.quarkus.vault.secrets.totp;
+
+public class CreateKeyParameters {
+
+ private Boolean generate;
+ private Boolean exported;
+
+ private Integer keySize;
+
+ private String url;
+ private String key;
+ private String issuer;
+
+ private String accountName;
+
+ private String period;
+ private String algorithm;
+ private Integer digits;
+ private Integer skew;
+
+ private Integer qrSize;
+
+ public CreateKeyParameters(String url) {
+ this.url = url;
+ }
+
+ public CreateKeyParameters(String key, String issuer, String accountName) {
+ this.key = key;
+ this.issuer = issuer;
+ this.accountName = accountName;
+ }
+
+ /**
+ * Constructs an object with generate to true as no key is provided.
+ *
+ * @param issuer to set.
+ * @param accountName to set.
+ */
+ public CreateKeyParameters(String issuer, String accountName) {
+ this.issuer = issuer;
+ this.accountName = accountName;
+ this.generate = true;
+ }
+
+ public void setGenerate(Boolean generate) {
+ this.generate = generate;
+ }
+
+ public void setExported(Boolean exported) {
+ this.exported = exported;
+ }
+
+ public void setKeySize(int keySize) {
+ this.keySize = keySize;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public void setPeriod(String period) {
+ this.period = period;
+ }
+
+ public void setAlgorithm(String algorithm) {
+ this.algorithm = algorithm;
+ }
+
+ public void setDigits(Integer digits) {
+ this.digits = digits;
+ }
+
+ public void setSkew(Integer skew) {
+ this.skew = skew;
+ }
+
+ public void setQrSize(Integer qrSize) {
+ this.qrSize = qrSize;
+ }
+
+ public Boolean getGenerate() {
+ return generate;
+ }
+
+ public Boolean getExported() {
+ return exported;
+ }
+
+ public Integer getKeySize() {
+ return keySize;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getIssuer() {
+ return issuer;
+ }
+
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public String getPeriod() {
+ return period;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public Integer getDigits() {
+ return digits;
+ }
+
+ public Integer getSkew() {
+ return skew;
+ }
+
+ public Integer getQrSize() {
+ return qrSize;
+ }
+
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java
new file mode 100644
index 0000000000000..2d2ba69aea08f
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java
@@ -0,0 +1,71 @@
+package io.quarkus.vault.secrets.totp;
+
+import java.util.Objects;
+
+public class KeyConfiguration {
+
+ private String accountName;
+ private String algorithm;
+ private int digits;
+ private String issuer;
+ private int period;
+
+ public KeyConfiguration(String accountName, String algorithm, int digits, String issuer, int period) {
+ this.accountName = accountName;
+ this.algorithm = algorithm;
+ this.digits = digits;
+ this.issuer = issuer;
+ this.period = period;
+ }
+
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public int getDigits() {
+ return digits;
+ }
+
+ public String getIssuer() {
+ return issuer;
+ }
+
+ public int getPeriod() {
+ return period;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ final KeyConfiguration that = (KeyConfiguration) o;
+ return digits == that.digits &&
+ period == that.period &&
+ accountName.equals(that.accountName) &&
+ Objects.equals(algorithm, that.algorithm) &&
+ issuer.equals(that.issuer);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(accountName, issuer);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("KeyConfiguration{");
+ sb.append("accountName='").append(accountName).append('\'');
+ sb.append(", algorithm='").append(algorithm).append('\'');
+ sb.append(", digits=").append(digits);
+ sb.append(", issuer='").append(issuer).append('\'');
+ sb.append(", period=").append(period);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java
new file mode 100644
index 0000000000000..12e3f60bb0d05
--- /dev/null
+++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java
@@ -0,0 +1,61 @@
+package io.quarkus.vault.secrets.totp;
+
+import java.util.Objects;
+
+public class KeyDefinition {
+
+ private String barcode;
+ private String url;
+
+ public KeyDefinition() {
+ }
+
+ public KeyDefinition(String barcode, String url) {
+ this.barcode = barcode;
+ this.url = url;
+ }
+
+ /**
+ * QR code in base64-formatteed PNG bytes.
+ *
+ * @return Barcode.
+ */
+ public String getBarcode() {
+ return barcode;
+ }
+
+ /**
+ * URL in otpauth format (ie
+ * otpauth://totp/Google:test@gmail.com?algorithm=SHA1&digits=6&issuer=Google&period=30&secret=HTXT7KJFVNAJUPYWQRWMNVQE5AF5YZI2)
+ *
+ * @return URL in otpauth format.
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ final KeyDefinition that = (KeyDefinition) o;
+ return Objects.equals(barcode, that.barcode) &&
+ Objects.equals(url, that.url);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(barcode, url);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("KeyDefinition{");
+ sb.append("barcode='").append(barcode).append('\'');
+ sb.append(", url='").append(url).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/integration-tests/vault/pom.xml b/integration-tests/vault/pom.xml
index 1e7ae406edbd8..e1d057a53c192 100644
--- a/integration-tests/vault/pom.xml
+++ b/integration-tests/vault/pom.xml
@@ -30,6 +30,11 @@
quarkus-vault-deployment
test
+
+ org.assertj
+ assertj-core
+ test
+
diff --git a/integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java
new file mode 100644
index 0000000000000..59d8e675a9ca7
--- /dev/null
+++ b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java
@@ -0,0 +1,111 @@
+package io.quarkus.vault;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.vault.secrets.totp.CreateKeyParameters;
+import io.quarkus.vault.secrets.totp.KeyConfiguration;
+import io.quarkus.vault.secrets.totp.KeyDefinition;
+import io.quarkus.vault.test.VaultTestLifecycleManager;
+
+@DisabledOnOs(OS.WINDOWS) // https://github.com/quarkusio/quarkus/issues/3796
+@QuarkusTestResource(VaultTestLifecycleManager.class)
+public class VaultTOTPITCase {
+
+ private static final String TEST_OTP_URL = "otpauth://totp/Vault:test@google.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Vault";
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addAsResource("application-vault-totp.properties", "application.properties"));
+
+ @Inject
+ VaultTOTPSecretEngine vaultTOTPSecretEngine;
+
+ @Test
+ public void createKey() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters(
+ "otpauth://totp/Vault:test@test.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Vault");
+ final Optional myKey = vaultTOTPSecretEngine.createKey("my_key", createKeyParameters);
+
+ assertThat(myKey).isNotPresent();
+ }
+
+ @Test
+ public void createGenerateKey() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters("Google", "test@gmail.com");
+ final Optional myKey = vaultTOTPSecretEngine.createKey("my_key_2", createKeyParameters);
+
+ assertThat(myKey)
+ .isPresent()
+ .get().hasNoNullFieldsOrProperties();
+ }
+
+ @Test
+ public void readKey() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters(
+ TEST_OTP_URL);
+ vaultTOTPSecretEngine.createKey("my_key_3", createKeyParameters);
+
+ final KeyConfiguration myKey3 = vaultTOTPSecretEngine.readKey("my_key_3");
+ assertThat(myKey3).returns("test@google.com", KeyConfiguration::getAccountName);
+ assertThat(myKey3).returns("Vault", KeyConfiguration::getIssuer);
+ }
+
+ @Test
+ public void listKeys() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters(
+ TEST_OTP_URL);
+ vaultTOTPSecretEngine.createKey("my_key_4", createKeyParameters);
+
+ final List listKeys = vaultTOTPSecretEngine.listKeys();
+ assertThat(listKeys).contains("my_key_4");
+ }
+
+ @Test
+ public void deleteKey() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters(
+ TEST_OTP_URL);
+ vaultTOTPSecretEngine.createKey("my_key_5", createKeyParameters);
+
+ vaultTOTPSecretEngine.deleteKey("my_key_5");
+ final List listKeys = vaultTOTPSecretEngine.listKeys();
+ assertThat(listKeys).doesNotContain("my_key_5");
+ }
+
+ @Test
+ public void generateCode() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters(
+ TEST_OTP_URL);
+ vaultTOTPSecretEngine.createKey("my_key_6", createKeyParameters);
+
+ final String myKey6Code = vaultTOTPSecretEngine.generateCode("my_key_6");
+ assertThat(myKey6Code).isNotEmpty();
+ }
+
+ @Test
+ public void validateCode() {
+ CreateKeyParameters createKeyParameters = new CreateKeyParameters(
+ TEST_OTP_URL);
+ createKeyParameters.setPeriod("30m");
+
+ vaultTOTPSecretEngine.createKey("my_key_7", createKeyParameters);
+ final String myKey7Code = vaultTOTPSecretEngine.generateCode("my_key_7");
+
+ boolean valid = vaultTOTPSecretEngine.validateCode("my_key_7", myKey7Code);
+ assertThat(valid).isTrue();
+ }
+}
diff --git a/integration-tests/vault/src/test/resources/application-vault-totp.properties b/integration-tests/vault/src/test/resources/application-vault-totp.properties
new file mode 100644
index 0000000000000..e071c39ff76c8
--- /dev/null
+++ b/integration-tests/vault/src/test/resources/application-vault-totp.properties
@@ -0,0 +1,5 @@
+quarkus.vault.url=https://localhost:8200
+quarkus.vault.authentication.userpass.username=bob
+quarkus.vault.authentication.userpass.password=sinclair
+
+quarkus.vault.tls.skip-verify=true
\ No newline at end of file
diff --git a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java
index a4d56803c8dc6..c8d1daa5ee7d0 100644
--- a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java
+++ b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java
@@ -297,6 +297,10 @@ private void initVault() throws InterruptedException, IOException {
execVault(format("vault write transit/keys/%s type=ed25519 derived=true", SIGN_DERIVATION_KEY_NAME));
execVault("vault write transit/keys/jws type=ecdsa-p256");
+
+ // TOTP
+
+ execVault("vault secrets enable totp");
}
public static boolean useTls() {
diff --git a/test-framework/vault/src/main/resources/vault.policy b/test-framework/vault/src/main/resources/vault.policy
index 9a3d750b293cd..4199f361ce2a8 100644
--- a/test-framework/vault/src/main/resources/vault.policy
+++ b/test-framework/vault/src/main/resources/vault.policy
@@ -48,4 +48,8 @@ path "secret/crud" {
path "secret-v2/data/crud" {
capabilities = ["read", "create", "update", "delete"]
+}
+
+path "totp/*" {
+ capabilities = ["read", "create", "update", "delete", "list"]
}
\ No newline at end of file