Merge pull request #2742 from Infisical/feat/totp-dynamic-secret

feat: TOTP dynamic secret provider
This commit is contained in:
Sheen
2024-11-21 12:00:12 +08:00
committed by GitHub
32 changed files with 1086 additions and 58 deletions

View File

@ -13,6 +13,7 @@ import { RabbitMqProvider } from "./rabbit-mq";
import { RedisDatabaseProvider } from "./redis";
import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
@ -27,5 +28,6 @@ export const buildDynamicSecretProviders = () => ({
[DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(),
[DynamicSecretProviders.Ldap]: LdapProvider(),
[DynamicSecretProviders.SapHana]: SapHanaProvider(),
[DynamicSecretProviders.Snowflake]: SnowflakeProvider()
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
[DynamicSecretProviders.Totp]: TotpProvider()
});

View File

@ -17,6 +17,17 @@ export enum LdapCredentialType {
Static = "static"
}
export enum TotpConfigType {
URL = "url",
MANUAL = "manual"
}
export enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(),
port: z.number(),
@ -221,6 +232,34 @@ export const LdapSchema = z.union([
})
]);
export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
z.object({
configType: z.literal(TotpConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(TotpConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
Cassandra = "cassandra",
@ -234,7 +273,8 @@ export enum DynamicSecretProviders {
AzureEntraID = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
@ -250,7 +290,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }),
z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema })
z.object({ type: z.literal(DynamicSecretProviders.Snowflake), inputs: DynamicSecretSnowflakeSchema }),
z.object({ type: z.literal(DynamicSecretProviders.Totp), inputs: DynamicSecretTotpSchema })
]);
export type TDynamicProviderFns = {

View File

@ -0,0 +1,92 @@
import { authenticator } from "otplib";
import { HashAlgorithms } from "otplib/core";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
export const TotpProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs);
return providerInputs;
};
const validateConnection = async () => {
return true;
};
const create = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
if (providerInputs.configType === TotpConfigType.URL) {
const urlObj = new URL(providerInputs.url);
secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithmFromUrl = urlObj.searchParams.get("algorithm");
if (periodFromUrl) {
period = +periodFromUrl;
}
if (digitsFromUrl) {
digits = +digitsFromUrl;
}
if (algorithmFromUrl) {
algorithm = algorithmFromUrl.toLowerCase() as HashAlgorithms;
}
} else {
secret = providerInputs.secret;
period = providerInputs.period;
digits = providerInputs.digits;
algorithm = providerInputs.algorithm as unknown as HashAlgorithms;
}
if (digits) {
authenticatorInstance.options = { digits };
}
if (algorithm) {
authenticatorInstance.options = { algorithm };
}
if (period) {
authenticatorInstance.options = { step: period };
}
return {
entityId,
data: { TOTP: authenticatorInstance.generate(secret), TIME_REMAINING: authenticatorInstance.timeRemaining() }
};
};
const revoke = async (_inputs: unknown, entityId: string) => {
return { entityId };
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renew = async (_inputs: unknown, _entityId: string) => {
throw new BadRequestError({
message: "Lease renewal is not supported for TOTPs"
});
};
return {
validateProviderInputs,
validateConnection,
create,
revoke,
renew
};
};

View File

@ -69,7 +69,7 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -131,12 +131,12 @@ The Infisical AWS ElastiCache dynamic secret allows you to generate AWS ElastiCa
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -66,7 +66,7 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -138,12 +138,12 @@ Replace **\<account id\>** with your AWS account id and **\<aws-scope-path\>** w
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -98,7 +98,7 @@ Click on Add assignments. Search for the application name you created and select
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -151,12 +151,12 @@ Click on Add assignments. Search for the application name you created and select
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -39,7 +39,7 @@ The above configuration allows user creation and granting permissions.
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -116,12 +116,12 @@ The above configuration allows user creation and granting permissions.
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -34,7 +34,7 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -114,12 +114,12 @@ The Infisical Elasticsearch dynamic secret allows you to generate Elasticsearch
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -31,7 +31,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -171,7 +171,7 @@ The Infisical LDAP dynamic secret allows you to generate user credentials on dem
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>

View File

@ -30,7 +30,7 @@ Create a project scopped API Key with the required permission in your Mongo Atla
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -101,12 +101,12 @@ Create a project scopped API Key with the required permission in your Mongo Atla
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -31,7 +31,7 @@ Create a user with the required permission in your MongoDB instance. This user w
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -103,12 +103,12 @@ Create a user with the required permission in your MongoDB instance. This user w
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -102,12 +102,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ Create a user with the required permission in your SQL instance. This user will
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -105,12 +105,12 @@ Create a user with the required permission in your SQL instance. This user will
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete the lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete the lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -28,7 +28,7 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -103,12 +103,12 @@ The Infisical RabbitMQ dynamic secret allows you to generate RabbitMQ credential
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -27,7 +27,7 @@ Create a user with the required permission in your Redis instance. This user wil
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -93,12 +93,12 @@ Create a user with the required permission in your Redis instance. This user wil
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the expiration time of the lease or delete a lease before it's set time to live.
This will allow you to see the expiration time of the lease or delete a lease before it's set time to live.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret leases past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -30,7 +30,7 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
</ParamField>
<ParamField path="Max TTL" type="string" required>
@ -106,13 +106,13 @@ The Infisical SAP HANA dynamic secret allows you to generate SAP HANA database c
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)
## Renew Leases
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** button as illustrated below.
![Provision Lease](/images/platform/dynamic-secrets/dynamic-secret-lease-renew.png)
<Warning>

View File

@ -109,7 +109,7 @@ Infisical's Snowflake dynamic secrets allow you to generate Snowflake user crede
## Audit or Revoke Leases
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
This will allow you see the lease details and delete the lease ahead of its expiration time.
This will allow you to see the lease details and delete the lease ahead of its expiration time.
![Provision Lease](/images/platform/dynamic-secrets/lease-data.png)

View File

@ -0,0 +1,70 @@
---
title: "TOTP"
description: "Learn how to dynamically generate time-based one-time passwords."
---
The Infisical TOTP dynamic secret allows you to generate time-based one-time passwords on demand.
## Prerequisite
- Infisical requires either an OTP url or a secret key from a TOTP provider.
## Set up Dynamic Secrets with TOTP
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the 'Add Dynamic Secret' button">
![Add Dynamic Secret Button](/images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select TOTP">
![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Configuration Type" type="string" required>
There are two supported configuration types - `url` and `manual`.
When `url` is selected, you can configure the TOTP generator using the OTP URL.
When `manual` is selected, you can configure the TOTP generator using the secret key along with other configurations like period, number of digits, and algorithm.
</ParamField>
<ParamField path="URL" type="string">
OTP URL in `otpauth://` format used to generate TOTP codes.
</ParamField>
<ParamField path="Secret Key" type="string">
Base32 encoded secret used to generate TOTP codes.
</ParamField>
<ParamField path="Period" type="number">
Time interval in seconds between generating new TOTP codes.
</ParamField>
<ParamField path="Digits" type="number">
Number of digits to generate in each TOTP code.
</ParamField>
<ParamField path="Algorithm" type="string">
Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512.
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png)
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png)
</Step>
<Step title="Click 'Submit'">
After submitting the form, you will see a dynamic secret created in the dashboard.
</Step>
<Step title="Generate dynamic secrets">
Once you've successfully configured the dynamic secret, you're ready to generate on-demand TOTPs.
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png)
Once you click the `Generate` button, a new secret lease will be generated and the TOTP will be shown to you.
![Provision Lease](/images/platform/dynamic-secrets/totp-lease-value.png)
</Step>
</Steps>

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

View File

@ -189,7 +189,8 @@
"documentation/platform/dynamic-secrets/azure-entra-id",
"documentation/platform/dynamic-secrets/ldap",
"documentation/platform/dynamic-secrets/sap-hana",
"documentation/platform/dynamic-secrets/snowflake"
"documentation/platform/dynamic-secrets/snowflake",
"documentation/platform/dynamic-secrets/totp"
]
},
"documentation/platform/project-templates",

View File

@ -28,7 +28,8 @@ export enum DynamicSecretProviders {
AzureEntraId = "azure-entra-id",
Ldap = "ldap",
SapHana = "sap-hana",
Snowflake = "snowflake"
Snowflake = "snowflake",
Totp = "totp"
}
export enum SqlProviders {
@ -230,6 +231,21 @@ export type TDynamicSecretProvider =
revocationStatement: string;
renewStatement?: string;
};
}
| {
type: DynamicSecretProviders.Totp;
inputs:
| {
configType: "url";
url: string;
}
| {
configType: "manual";
secret: string;
period?: number;
algorithm?: string;
digits?: number;
};
};
export type TCreateDynamicSecretDTO = {
projectSlug: string;

View File

@ -11,7 +11,7 @@ import {
SiSnowflake
} from "react-icons/si";
import { faAws } from "@fortawesome/free-brands-svg-icons";
import { faDatabase } from "@fortawesome/free-solid-svg-icons";
import { faClock, faDatabase } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AnimatePresence, motion } from "framer-motion";
@ -31,6 +31,7 @@ import { RabbitMqInputForm } from "./RabbitMqInputForm";
import { RedisInputForm } from "./RedisInputForm";
import { SapHanaInputForm } from "./SapHanaInputForm";
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
import { TotpInputForm } from "./TotpInputForm";
type Props = {
isOpen?: boolean;
@ -110,6 +111,11 @@ const DYNAMIC_SECRET_LIST = [
icon: <SiSnowflake size="1.5rem" />,
provider: DynamicSecretProviders.Snowflake,
title: "Snowflake"
},
{
icon: <FontAwesomeIcon icon={faClock} size="lg" />,
provider: DynamicSecretProviders.Totp,
title: "TOTP"
}
];
@ -405,6 +411,24 @@ export const CreateDynamicSecretForm = ({
/>
</motion.div>
)}
{wizardStep === WizardSteps.ProviderInputs &&
selectedProvider === DynamicSecretProviders.Totp && (
<motion.div
key="dynamic-totp-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<TotpInputForm
onCompleted={handleFormReset}
onCancel={handleFormReset}
projectSlug={projectSlug}
secretPath={secretPath}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>

View File

@ -0,0 +1,314 @@
import { Controller, useForm } from "react-hook-form";
import Link from "next/link";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
enum ConfigType {
URL = "url",
MANUAL = "manual"
}
enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
const formSchema = z.object({
provider: z.discriminatedUnion("configType", [
z.object({
configType: z.literal(ConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(ConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]),
name: z
.string()
.trim()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
});
type TForm = z.infer<typeof formSchema>;
type Props = {
onCompleted: () => void;
onCancel: () => void;
secretPath: string;
projectSlug: string;
environment: string;
};
export const TotpInputForm = ({
onCompleted,
onCancel,
environment,
secretPath,
projectSlug
}: Props) => {
const {
control,
watch,
formState: { isSubmitting },
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
defaultValues: {
provider: {
configType: ConfigType.URL
}
}
});
const selectedConfigType = watch("provider.configType");
const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
// wait till previous request is finished
if (createDynamicSecret.isLoading) return;
try {
await createDynamicSecret.mutateAsync({
provider: { type: DynamicSecretProviders.Totp, inputs: provider },
maxTTL: "24h",
name,
path: secretPath,
defaultTTL: "1m",
projectSlug,
environmentSlug: environment
});
onCompleted();
} catch (err) {
createNotification({
type: "error",
text: err instanceof Error ? err.message : "Failed to create dynamic secret"
});
}
};
return (
<div>
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<Link
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
passHref
>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
<div className="flex flex-col">
<Controller
control={control}
name="provider.configType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
onValueChange={(val) => {
field.onChange(val);
}}
dropdownContainerClassName="max-w-full"
>
<SelectItem value={ConfigType.URL} key="config-type-url">
URL
</SelectItem>
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
Manual
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigType === ConfigType.URL && (
<Controller
control={control}
name="provider.url"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="OTP URL"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} placeholder="otpauth://" />
</FormControl>
)}
/>
)}
{selectedConfigType === ConfigType.MANUAL && (
<>
<Controller
control={control}
name="provider.secret"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex flex-row gap-2">
<Controller
control={control}
name="provider.period"
defaultValue={30}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Period"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.digits"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digits"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="provider.algorithm"
defaultValue={TotpAlgorithm.SHA1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Algorithm"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
dropdownContainerClassName="max-w-full"
onValueChange={(val) => {
field.onChange(val);
}}
>
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
SHA1
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
SHA256
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
SHA512
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<p className="mb-8 text-sm font-normal text-gray-400">
The period, digits, and algorithm values can remain at their defaults unless
your TOTP provider specifies otherwise.
</p>
</>
)}
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onCancel}>
Cancel
</Button>
</div>
</form>
</div>
);
};

View File

@ -1,6 +1,6 @@
import { ReactNode } from "react";
import { ReactNode, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { faCheck, faClock, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
@ -9,8 +9,16 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, SecretInput, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import {
Button,
FormControl,
IconButton,
Input,
SecretInput,
Spinner,
Tooltip
} from "@app/components/v2";
import { useTimedReset, useToggle } from "@app/hooks";
import { useCreateDynamicSecretLease } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
@ -54,7 +62,76 @@ const OutputDisplay = ({
);
};
const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
const TotpOutputDisplay = ({
totp,
remainingSeconds,
triggerLeaseRegeneration
}: {
totp: string;
remainingSeconds: number;
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>;
}) => {
const [remainingTime, setRemainingTime] = useState(remainingSeconds);
const [shouldShowRegenerate, setShouldShowRegenerate] = useToggle(false);
useEffect(() => {
setRemainingTime(remainingSeconds);
setShouldShowRegenerate.off();
// Set up countdown interval
const intervalId = setInterval(() => {
setRemainingTime((prevTime) => {
if (prevTime <= 1) {
clearInterval(intervalId);
setShouldShowRegenerate.on();
return 0;
}
return prevTime - 1;
});
}, 1000);
// Cleanup interval on unmount or when totp changes
return () => clearInterval(intervalId);
}, [totp, remainingSeconds]);
return (
<div className="h-36">
<OutputDisplay label="Time-based one-time password" value={totp} />
{remainingTime > 0 ? (
<div
className={`ml-2 flex items-center text-sm ${
remainingTime < 10 ? "text-red-500" : "text-yellow-500"
} transition-colors duration-500`}
>
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
<span>
Expires in {remainingTime} {remainingTime > 1 ? "seconds" : "second"}
</span>
</div>
) : (
<div className="ml-2 flex items-center text-sm text-red-500">
<FontAwesomeIcon className="mr-1" icon={faClock} size="sm" />
Expired
</div>
)}
{shouldShowRegenerate && (
<Button
colorSchema="secondary"
className="mt-2"
onClick={() => triggerLeaseRegeneration({})}
>
Regenerate
</Button>
)}
</div>
);
};
const renderOutputForm = (
provider: DynamicSecretProviders,
data: unknown,
triggerLeaseRegeneration: (details: { ttl?: string }) => Promise<void>
) => {
if (
provider === DynamicSecretProviders.SqlDatabase ||
provider === DynamicSecretProviders.Cassandra ||
@ -242,11 +319,29 @@ const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
);
}
if (provider === DynamicSecretProviders.Totp) {
const { TOTP, TIME_REMAINING } = data as {
TOTP: string;
TIME_REMAINING: number;
};
return (
<TotpOutputDisplay
totp={TOTP}
remainingSeconds={TIME_REMAINING}
triggerLeaseRegeneration={triggerLeaseRegeneration}
/>
);
}
return null;
};
const formSchema = z.object({
ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number")
ttl: z
.string()
.refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional()
});
type TForm = z.infer<typeof formSchema>;
@ -259,6 +354,8 @@ type Props = {
secretPath: string;
};
const PROVIDERS_WITH_AUTOGENERATE_SUPPORT = [DynamicSecretProviders.Totp];
export const CreateDynamicSecretLease = ({
onClose,
projectSlug,
@ -277,6 +374,9 @@ export const CreateDynamicSecretLease = ({
ttl: "1h"
}
});
const [isPreloading, setIsPreloading] = useToggle(
PROVIDERS_WITH_AUTOGENERATE_SUPPORT.includes(provider)
);
const createDynamicSecretLease = useCreateDynamicSecretLease();
@ -290,10 +390,13 @@ export const CreateDynamicSecretLease = ({
ttl,
dynamicSecretName
});
createNotification({
type: "success",
text: "Successfully leased dynamic secret"
});
setIsPreloading.off();
} catch (error) {
console.log(error);
createNotification({
@ -303,8 +406,23 @@ export const CreateDynamicSecretLease = ({
}
};
const handleLeaseRegeneration = async (data: { ttl?: string }) => {
setIsPreloading.on();
handleDynamicSecretLeaseCreate(data);
};
useEffect(() => {
if (provider === DynamicSecretProviders.Totp) {
handleDynamicSecretLeaseCreate({});
}
}, [provider]);
const isOutputMode = Boolean(createDynamicSecretLease?.data);
if (isPreloading) {
return <Spinner className="mx-auto h-40 text-mineshaft-700" />;
}
return (
<div>
<AnimatePresence>
@ -350,7 +468,11 @@ export const CreateDynamicSecretLease = ({
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
{renderOutputForm(provider, createDynamicSecretLease.data?.data)}
{renderOutputForm(
provider,
createDynamicSecretLease.data?.data,
handleLeaseRegeneration
)}
</motion.div>
)}
</AnimatePresence>

View File

@ -101,10 +101,20 @@ export const DynamicSecretListView = ({
role="button"
tabIndex={0}
onKeyDown={(evt) => {
// no lease view for TOTP because it's irrelevant
if (secret.type === DynamicSecretProviders.Totp) {
return;
}
if (evt.key === "Enter" && !isRevoking)
handlePopUpOpen("dynamicSecretLeases", secret.id);
}}
onClick={() => {
// no lease view for TOTP because it's irrelevant
if (secret.type === DynamicSecretProviders.Totp) {
return;
}
if (!isRevoking) {
handlePopUpOpen("dynamicSecretLeases", secret.id);
}

View File

@ -17,6 +17,7 @@ import { EditDynamicSecretRedisProviderForm } from "./EditDynamicSecretRedisProv
import { EditDynamicSecretSapHanaForm } from "./EditDynamicSecretSapHanaForm";
import { EditDynamicSecretSnowflakeForm } from "./EditDynamicSecretSnowflakeForm";
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
import { EditDynamicSecretTotpForm } from "./EditDynamicSecretTotpForm";
type Props = {
onClose: () => void;
@ -276,6 +277,23 @@ export const EditDynamicSecretForm = ({
/>
</motion.div>
)}
{dynamicSecretDetails?.type === DynamicSecretProviders.Totp && (
<motion.div
key="totp-edit"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<EditDynamicSecretTotpForm
onClose={onClose}
projectSlug={projectSlug}
secretPath={secretPath}
dynamicSecret={dynamicSecretDetails}
environment={environment}
/>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@ -0,0 +1,318 @@
import { Controller, useForm } from "react-hook-form";
import Link from "next/link";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
enum ConfigType {
URL = "url",
MANUAL = "manual"
}
enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
const formSchema = z.object({
inputs: z
.discriminatedUnion("configType", [
z.object({
configType: z.literal(ConfigType.URL),
url: z
.string()
.url()
.trim()
.min(1)
.refine((val) => {
const urlObj = new URL(val);
const secret = urlObj.searchParams.get("secret");
return Boolean(secret);
}, "OTP URL must contain secret field")
}),
z.object({
configType: z.literal(ConfigType.MANUAL),
secret: z
.string()
.trim()
.min(1)
.transform((val) => val.replace(/\s+/g, "")),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
])
.optional(),
newName: z
.string()
.trim()
.min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
});
type TForm = z.infer<typeof formSchema>;
type Props = {
onClose: () => void;
dynamicSecret: TDynamicSecret & { inputs: unknown };
secretPath: string;
projectSlug: string;
environment: string;
};
export const EditDynamicSecretTotpForm = ({
onClose,
dynamicSecret,
environment,
secretPath,
projectSlug
}: Props) => {
const {
control,
formState: { isSubmitting },
watch,
handleSubmit
} = useForm<TForm>({
resolver: zodResolver(formSchema),
values: {
newName: dynamicSecret.name,
inputs: dynamicSecret.inputs as TForm["inputs"]
}
});
const selectedConfigType = watch("inputs.configType");
const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => {
// wait till previous request is finished
if (updateDynamicSecret.isLoading) return;
try {
await updateDynamicSecret.mutateAsync({
name: dynamicSecret.name,
path: secretPath,
projectSlug,
environmentSlug: environment,
data: {
inputs,
newName: newName === dynamicSecret.name ? undefined : newName
}
});
onClose();
createNotification({
type: "success",
text: "Successfully updated dynamic secret"
});
} catch (err) {
createNotification({
type: "error",
text: err instanceof Error ? err.message : "Failed to update dynamic secret"
});
}
};
return (
<div>
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
<div>
<div className="flex items-center space-x-2">
<div className="flex-grow">
<Controller
control={control}
defaultValue=""
name="newName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
</div>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration
<Link
href="https://infisical.com/docs/documentation/platform/dynamic-secrets/totp"
passHref
>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
<div className="flex flex-col">
<Controller
control={control}
name="inputs.configType"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
onValueChange={(val) => {
field.onChange(val);
}}
dropdownContainerClassName="max-w-full"
>
<SelectItem value={ConfigType.URL} key="config-type-url">
URL
</SelectItem>
<SelectItem value={ConfigType.MANUAL} key="config-type-manual">
Manual
</SelectItem>
</Select>
</FormControl>
)}
/>
{selectedConfigType === ConfigType.URL && (
<Controller
control={control}
name="inputs.url"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="OTP URL"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
)}
{selectedConfigType === ConfigType.MANUAL && (
<>
<Controller
control={control}
name="inputs.secret"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Key"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<div className="flex flex-row gap-2">
<Controller
control={control}
name="inputs.period"
defaultValue={30}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Period"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.digits"
defaultValue={6}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digits"
className="flex-grow"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input
{...field}
type="number"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="inputs.algorithm"
defaultValue={TotpAlgorithm.SHA1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Algorithm"
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-full"
>
<Select
defaultValue={field.value}
{...field}
className="w-full"
dropdownContainerClassName="max-w-full"
onValueChange={(val) => {
field.onChange(val);
}}
>
<SelectItem value={TotpAlgorithm.SHA1} key="algorithm-sha-1">
SHA1
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA256} key="algorithm-sha-256">
SHA256
</SelectItem>
<SelectItem value={TotpAlgorithm.SHA512} key="algorithm-sha-512">
SHA512
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<p className="mb-8 text-sm font-normal text-gray-400">
The period, digits, and algorithm values can remain at their defaults unless
your TOTP provider specifies otherwise.
</p>
</>
)}
</div>
</div>
</div>
<div className="mt-4 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
<Button variant="outline_bg" onClick={onClose}>
Cancel
</Button>
</div>
</form>
</div>
);
};