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"
}
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,9 +232,29 @@ export const LdapSchema = z.union([
})
]);
export const DynamicSecretTotpSchema = z.object({
url: z.string().url().trim().min(1)
});
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().min(1),
period: z.number().optional(),
algorithm: z.nativeEnum(TotpAlgorithm).optional(),
digits: z.number().optional()
})
]);
export enum DynamicSecretProviders {
SqlDatabase = "sql-database",
@@ -4,20 +4,12 @@ import { HashAlgorithms } from "otplib/core";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { DynamicSecretTotpSchema, TDynamicProviderFns } from "./models";
import { DynamicSecretTotpSchema, TDynamicProviderFns, TotpConfigType } from "./models";
export const TotpProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
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;
};
@@ -31,22 +23,46 @@ export const TotpProvider = (): TDynamicProviderFns => {
const entityId = alphaNumericNanoId(32);
const authenticatorInstance = authenticator.clone();
const urlObj = new URL(providerInputs.url);
const secret = urlObj.searchParams.get("secret") as string;
const periodFromUrl = urlObj.searchParams.get("period");
const digitsFromUrl = urlObj.searchParams.get("digits");
const algorithm = urlObj.searchParams.get("algorithm");
let secret: string;
let period: number | null | undefined;
let digits: number | null | undefined;
let algorithm: HashAlgorithms | null | undefined;
if (digitsFromUrl) {
authenticatorInstance.options = { digits: +digitsFromUrl };
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: algorithm.toLowerCase() as HashAlgorithms };
authenticatorInstance.options = { algorithm };
}
if (periodFromUrl) {
authenticatorInstance.options = { step: +periodFromUrl };
if (period) {
authenticatorInstance.options = { step: period };
}
return { entityId, data: { TOTP: authenticatorInstance.generate(secret) } };