feat: added support for configuring totp with secret key

This commit is contained in:
Sheen Capadngan
2024-11-20 23:40:36 +08:00
parent 660c09ded4
commit 0c3894496c
9 changed files with 446 additions and 62 deletions
@@ -17,6 +17,17 @@ export enum LdapCredentialType {
Static = "static" Static = "static"
} }
export enum TotpConfigType {
URL = "url",
MANUAL = "manual"
}
export enum TotpAlgorithm {
SHA1 = "sha1",
SHA256 = "sha256",
SHA512 = "sha512"
}
export const DynamicSecretRedisDBSchema = z.object({ export const DynamicSecretRedisDBSchema = z.object({
host: z.string().trim().toLowerCase(), host: z.string().trim().toLowerCase(),
port: z.number(), port: z.number(),
@@ -221,9 +232,29 @@ export const LdapSchema = z.union([
}) })
]); ]);
export const DynamicSecretTotpSchema = z.object({ export const DynamicSecretTotpSchema = z.discriminatedUnion("configType", [
url: z.string().url().trim().min(1) 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().min(1),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders { export enum DynamicSecretProviders {
SqlDatabase = "sql-database", SqlDatabase = "sql-database",
@@ -4,20 +4,12 @@ import { HashAlgorithms } from "otplib/core";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns } from "./models"; import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
export const TotpProvider = (): TDynamicProviderFns => { export const TotpProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs); const providerInputs = await DynamicSecretTotpSchema.parseAsync(inputs);
const urlObj = new URL(providerInputs.url);
const secret = urlObj.searchParams.get("secret");
if (!secret) {
throw new BadRequestError({
message: "TOTP secret is missing from URL"
});
}
return providerInputs; return providerInputs;
}; };
@@ -31,22 +23,46 @@ export const TotpProvider = (): TDynamicProviderFns => {
const entityId = alphaNumericNanoId(32); const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone(); const authenticatorInstance = authenticator.clone();
const urlObj = new URL(providerInputs.url); let secret: string;
const secret = urlObj.searchParams.get("secret") as string; let period: number | null | undefined;
const periodFromUrl = urlObj.searchParams.get("period"); let digits: number | null | undefined;
const digitsFromUrl = urlObj.searchParams.get("digits"); let algorithm: HashAlgorithms | null | undefined;
const algorithm = urlObj.searchParams.get("algorithm");
if (digitsFromUrl) { if (providerInputs.configType === TotpConfigType.URL) {
authenticatorInstance.options = { digits: +digitsFromUrl }; 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) { if (algorithm) {
authenticatorInstance.options = { algorithm: algorithm.toLowerCase() as HashAlgorithms }; authenticatorInstance.options = { algorithm };
} }
if (periodFromUrl) { if (period) {
authenticatorInstance.options = { step: +periodFromUrl }; authenticatorInstance.options = { step: period };
} }
return { entityId, data: { TOTP: authenticatorInstance.generate(secret) } }; return { entityId, data: { TOTP: authenticatorInstance.generate(secret) } };
@@ -22,23 +22,34 @@ The Infisical TOTP dynamic secret allows you to generate time-based one-time pas
![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png) ![Dynamic Secret Modal](/images/platform/dynamic-secrets/dynamic-secret-modal-totp.png)
</Step> </Step>
<Step title="Provide the inputs for dynamic secret parameters"> <Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required> <ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced Name by which you want the secret to be referenced
</ParamField> </ParamField>
<ParamField path="Configuration Type" type="string" required>
There are two supported configuration types - `url` and `manual`.
<ParamField path="Default TTL" type="string" required> When `url` is selected, you can configure the TOTP generator using the OTP URL.
Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated)
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" required>
Number of digits to generate in each TOTP code.
</ParamField>
<ParamField path="Algorithm" type="string" required>
Hash algorithm to use when generating TOTP codes. The supported algorithms are sha1, sha256, and sha512.
</ParamField> </ParamField>
<ParamField path="Max TTL" type="string" required> ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-url.png)
Maximum time-to-live for a generated secret ![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp-manual.png)
</ParamField>
<ParamField path="URL" type="string" required>
OTP url from the TOTP provider
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal-totp.png)
</Step> </Step>
<Step title="Click 'Submit'"> <Step title="Click 'Submit'">
Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 KiB

+12 -3
View File
@@ -234,9 +234,18 @@ export type TDynamicSecretProvider =
} }
| { | {
type: DynamicSecretProviders.Totp; type: DynamicSecretProviders.Totp;
inputs: { inputs:
url: string; | {
}; configType: "url";
url: string;
}
| {
configType: "manual";
secret: string;
period?: number;
algorithm?: string;
digits?: number;
};
}; };
export type TCreateDynamicSecretDTO = { export type TCreateDynamicSecretDTO = {
projectSlug: string; projectSlug: string;
@@ -6,20 +6,52 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useCreateDynamicSecret } from "@app/hooks/api"; import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; 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({ const formSchema = z.object({
provider: z.object({ provider: z.discriminatedUnion("configType", [
url: z.string().url().trim().min(1) 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().min(1),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]),
name: z name: z
.string() .string()
.trim() .trim()
.min(1) .min(1)
.refine((val) => val.toLowerCase() === val, "Must be lowercase") .refine((val) => val.toLowerCase() === val, "Must be lowercase")
}); });
type TForm = z.infer<typeof formSchema>; type TForm = z.infer<typeof formSchema>;
type Props = { type Props = {
@@ -39,12 +71,20 @@ export const TotpInputForm = ({
}: Props) => { }: Props) => {
const { const {
control, control,
watch,
formState: { isSubmitting }, formState: { isSubmitting },
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema),
defaultValues: {
provider: {
configType: ConfigType.URL
}
}
}); });
const selectedConfigType = watch("provider.configType");
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const handleCreateDynamicSecret = async ({ name, provider }: TForm) => { const handleCreateDynamicSecret = async ({ name, provider }: TForm) => {
@@ -113,19 +153,142 @@ export const TotpInputForm = ({
<div className="flex flex-col"> <div className="flex flex-col">
<Controller <Controller
control={control} control={control}
name="provider.url" name="provider.configType"
defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="OTP URL" label="Configuration Type"
className="flex-grow"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
className="w-full"
> >
<Input {...field} /> <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> </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">
<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>
</>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -6,16 +6,47 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useUpdateDynamicSecret } from "@app/hooks/api"; import { useUpdateDynamicSecret } from "@app/hooks/api";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; 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({ const formSchema = z.object({
inputs: z inputs: z
.object({ .discriminatedUnion("configType", [
url: z.string().url().trim().min(1) z.object({
}) configType: z.literal(ConfigType.URL),
.partial(), 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().min(1),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
])
.optional(),
newName: z newName: z
.string() .string()
.trim() .trim()
@@ -42,17 +73,17 @@ export const EditDynamicSecretTotpForm = ({
const { const {
control, control,
formState: { isSubmitting }, formState: { isSubmitting },
watch,
handleSubmit handleSubmit
} = useForm<TForm>({ } = useForm<TForm>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
values: { values: {
newName: dynamicSecret.name, newName: dynamicSecret.name,
inputs: { inputs: dynamicSecret.inputs as TForm["inputs"]
...(dynamicSecret.inputs as TForm["inputs"])
}
} }
}); });
const selectedConfigType = watch("inputs.configType");
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => { const handleUpdateDynamicSecret = async ({ inputs, newName }: TForm) => {
@@ -126,19 +157,142 @@ export const EditDynamicSecretTotpForm = ({
<div className="flex flex-col"> <div className="flex flex-col">
<Controller <Controller
control={control} control={control}
name="inputs.url" name="inputs.configType"
defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="OTP URL" label="Configuration Type"
className="flex-grow"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
className="w-full"
> >
<Input {...field} /> <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> </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">
<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>
</>
)}
</div> </div>
</div> </div>
</div> </div>