feat: enhance two-factor authentication and auth client implementation

This commit is contained in:
Mauricio Siu
2025-02-17 00:07:36 -06:00
parent 0e8e92c715
commit 7abe060fcf
25 changed files with 1103 additions and 270 deletions

View File

@@ -28,6 +28,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@better-auth/utils":"0.2.3",
"@oslojs/encoding":"1.1.0",
"@oslojs/crypto":"1.0.1",
"drizzle-dbml-generator":"0.10.0",

View File

@@ -16,6 +16,7 @@ export const auth = betterAuth({
provider: "pg",
schema: schema,
}),
appName: "Dokploy",
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID as string,

View File

@@ -1,4 +1,5 @@
import { randomBytes } from "node:crypto";
import { createOTP } from "@better-auth/utils/otp";
import { db } from "@dokploy/server/db";
import { users_temp } from "@dokploy/server/db/schema";
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
@@ -29,26 +30,41 @@ export const findAuthById = async (authId: string) => {
return result;
};
export const generate2FASecret = async (userId: string) => {
const user = await findUserById(userId);
const generateBase32Secret = () => {
// Generamos 32 bytes (256 bits) para asegurar que tengamos suficiente longitud
const buffer = randomBytes(32);
// Convertimos directamente a hex para Better Auth
const hex = buffer.toString("hex");
// También necesitamos la versión base32 para el QR code
const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 32);
return {
hex,
base32,
};
};
const base32_secret = generateBase32Secret();
export const generate2FASecret = () => {
const secret = "46JMUCG4NJ3CIU6LQAIVFWUW";
const totp = new TOTP({
issuer: "Dokploy",
label: `${user?.email}`,
label: "siumauricio@hotmail.com",
algorithm: "SHA1",
digits: 6,
secret: base32_secret,
secret: secret,
});
const otpauth_url = totp.toString();
// Convertir los bytes del secreto a hex
const secretBytes = totp.secret.bytes;
const hexSecret = Buffer.from(secretBytes).toString("hex");
const qrUrl = await QRCode.toDataURL(otpauth_url);
console.log("Secret bytes:", secretBytes);
console.log("Hex secret:", hexSecret);
return {
qrCodeUrl: qrUrl,
secret: base32_secret,
secret,
hexSecret,
totp,
};
};
@@ -59,6 +75,7 @@ export const verify2FA = async (auth: User, secret: string, pin: string) => {
algorithm: "SHA1",
digits: 6,
secret: secret,
period: 30,
});
const delta = totp.validate({ token: pin });
@@ -72,8 +89,124 @@ export const verify2FA = async (auth: User, secret: string, pin: string) => {
return auth;
};
const generateBase32Secret = () => {
const buffer = randomBytes(15);
const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24);
return base32;
const convertBase32ToHex = (base32Secret: string) => {
try {
// Asegurarnos de que la longitud sea múltiplo de 8 agregando padding
let paddedSecret = base32Secret;
while (paddedSecret.length % 8 !== 0) {
paddedSecret += "=";
}
const bytes = encode.decode.asBytes(paddedSecret.toUpperCase());
let hex = Buffer.from(bytes).toString("hex");
// Asegurarnos de que el hex tenga al menos 32 caracteres (16 bytes)
while (hex.length < 32) {
hex += "0";
}
return hex;
} catch (error) {
console.error("Error converting base32 to hex:", error);
return base32Secret;
}
};
// Para probar
// const testSecret = "46JMUCG4NJ3CIU6LQAIVFWUW";
// console.log("Original:", testSecret);
// console.log("Converted:", convertBase32ToHex(testSecret));
// console.log(
// "Length in bytes:",
// Buffer.from(convertBase32ToHex(testSecret), "hex").length,
// );
// console.log(generate2FASecret().secret.secret);
// // Para probar
// const testResult = generate2FASecret();
// console.log("\nResultados:");
// console.log("Original base32:", testResult.secret);
// console.log("Hex convertido:", testResult.hexSecret);
// console.log(
// "Longitud en bytes:",
// Buffer.from(testResult.hexSecret, "hex").length,
// );
export const symmetricDecrypt = async ({ key, data }) => {
const keyAsBytes = await createHash("SHA-256").digest(key);
const dataAsBytes = hexToBytes(data);
const chacha = managedNonce(xchacha20poly1305)(new Uint8Array(keyAsBytes));
return new TextDecoder().decode(chacha.decrypt(dataAsBytes));
};
export const migrateExistingSecret = async (
existingBase32Secret: string,
encryptionKey: string,
) => {
try {
// 1. Primero asegurarnos que el secreto base32 tenga el padding correcto
let paddedSecret = existingBase32Secret;
while (paddedSecret.length % 8 !== 0) {
paddedSecret += "=";
}
// 2. Decodificar el base32 a bytes usando hi-base32
const bytes = encode.decode.asBytes(paddedSecret.toUpperCase());
// 3. Convertir los bytes a hex
const hexSecret = Buffer.from(bytes).toString("hex");
// 4. Encriptar el secreto hex usando Better Auth
const encryptedSecret = await symmetricEncrypt({
key: encryptionKey,
data: hexSecret,
});
// 5. Crear TOTP con el secreto original para validación
const originalTotp = new TOTP({
issuer: "Dokploy",
label: "migration-test",
algorithm: "SHA1",
digits: 6,
secret: existingBase32Secret,
});
// 6. Generar un código de prueba con el secreto original
const testCode = originalTotp.generate();
// 7. Validar que el código funcione con el secreto original
const isValid = originalTotp.validate({ token: testCode }) !== null;
return {
originalSecret: existingBase32Secret,
hexSecret,
encryptedSecret, // Este es el valor que debes guardar en la base de datos
isValid,
testCode,
secretLength: hexSecret.length,
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("Error durante la migración:", errorMessage);
throw new Error(`Error al migrar el secreto: ${errorMessage}`);
}
};
// // Ejemplo de uso con el secreto de prueba
// const testMigration = await migrateExistingSecret(
// "46JMUCG4NJ3CIU6LQAIVFWUW",
// process.env.BETTER_AUTH_SECRET || "your-encryption-key",
// );
// console.log("\nPrueba de migración:");
// console.log("Secreto original (base32):", testMigration.originalSecret);
// console.log("Secreto convertido (hex):", testMigration.hexSecret);
// console.log("Secreto encriptado:", testMigration.encryptedSecret);
// console.log("Longitud del secreto hex:", testMigration.secretLength);
// console.log("¿Conversión válida?:", testMigration.isValid);
// console.log("Código de prueba:", testMigration.testCode);
const secret = "46JMUCG4NJ3CIU6LQAIVFWUW";
const isValid = createOTP(secret, {
digits: 6,
period: 30,
}).verify("123456");
console.log(isValid.then((isValid) => console.log(isValid)));