chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,79 @@
import { Ratelimit } from "@upstash/ratelimit";
import { type RedisWithClusterOptions } from "~/redis.server";
import { createRedisRateLimitClient, RateLimiter } from "~/services/rateLimiterCore.server";
// MFA rate limiting: two sliding windows in series, both of which must pass.
// A per-minute window covers interactive retries; a cumulative daily window
// caps total attempts per pending-MFA session.
//
// Free of `env.server` so it can be tested directly against a container Redis;
// the env-derived production singletons live in `mfaRateLimiterGlobal.server.ts`.
// Production policy. Exported so tests assert against the real numbers.
export const MFA_PER_MINUTE_ATTEMPTS = 5;
export const MFA_DAILY_ATTEMPTS = 30;
export type MfaRateLimiters = {
perMinute: Pick<RateLimiter, "limit">;
daily: Pick<RateLimiter, "limit">;
};
/**
* Build the pair of MFA rate limiters. Production passes the env-derived
* Redis connection and the default policy; tests inject a container
* Redis (and may override the attempt caps to isolate one window).
*/
export function createMfaRateLimiters(options: {
redisOptions: RedisWithClusterOptions;
perMinuteAttempts?: number;
dailyAttempts?: number;
}): { perMinute: RateLimiter; daily: RateLimiter } {
const redisClient = createRedisRateLimitClient(options.redisOptions);
return {
perMinute: new RateLimiter({
redisClient,
keyPrefix: "mfa:validation",
limiter: Ratelimit.slidingWindow(options.perMinuteAttempts ?? MFA_PER_MINUTE_ATTEMPTS, "1 m"),
logSuccess: false,
logFailure: true,
}),
daily: new RateLimiter({
redisClient,
keyPrefix: "mfa:validation:daily",
limiter: Ratelimit.slidingWindow(options.dailyAttempts ?? MFA_DAILY_ATTEMPTS, "24 h"),
logSuccess: false,
logFailure: true,
}),
};
}
export class MfaRateLimitError extends Error {
public readonly retryAfter: number;
constructor(retryAfter: number) {
super(`MFA validation rate limit exceeded.`);
this.retryAfter = retryAfter;
}
}
/**
* Check whether the user can attempt MFA validation, enforcing both the
* per-minute and the daily cap. The daily cap is checked first.
* @param userId - The user ID to rate limit
* @param limiters - The limiter pair (production singletons or test-injected)
* @throws {MfaRateLimitError} If either rate limit is exceeded
*/
export async function checkMfaRateLimit(userId: string, limiters: MfaRateLimiters): Promise<void> {
const dailyResult = await limiters.daily.limit(userId);
if (!dailyResult.success) {
const retryAfter = new Date(dailyResult.reset).getTime() - Date.now();
throw new MfaRateLimitError(retryAfter);
}
const result = await limiters.perMinute.limit(userId);
if (!result.success) {
const retryAfter = new Date(result.reset).getTime() - Date.now();
throw new MfaRateLimitError(retryAfter);
}
}
@@ -0,0 +1,37 @@
import { env } from "~/env.server";
import { singleton } from "~/utils/singleton";
import {
checkMfaRateLimit as checkMfaRateLimitWith,
createMfaRateLimiters,
type MfaRateLimiters,
} from "./mfaRateLimiter.server";
// Production singletons, wired to the env-derived rate-limit Redis.
// Kept out of `mfaRateLimiter.server.ts` so that module stays free of
// `env.server` and remains testable in isolation (see that file).
const mfaRateLimiters = singleton("mfaRateLimiters", () =>
createMfaRateLimiters({
redisOptions: {
port: env.RATE_LIMIT_REDIS_PORT,
host: env.RATE_LIMIT_REDIS_HOST,
username: env.RATE_LIMIT_REDIS_USERNAME,
password: env.RATE_LIMIT_REDIS_PASSWORD,
tlsDisabled: env.RATE_LIMIT_REDIS_TLS_DISABLED === "true",
clusterMode: env.RATE_LIMIT_REDIS_CLUSTER_MODE_ENABLED === "1",
},
})
);
export const mfaRateLimiter = mfaRateLimiters.perMinute;
export const mfaDailyRateLimiter = mfaRateLimiters.daily;
/**
* Production entrypoint: rate-limit an MFA validation attempt for `userId`
* against the env-configured limiter pair. Throws `MfaRateLimitError` when
* either the per-minute or the cumulative daily cap is exceeded.
*/
export function checkMfaRateLimit(userId: string, limiters: MfaRateLimiters = mfaRateLimiters) {
return checkMfaRateLimitWith(userId, limiters);
}
export { MfaRateLimitError } from "./mfaRateLimiter.server";
@@ -0,0 +1,378 @@
import { type SecretReference, type User, type PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { createRandomStringGenerator } from "@better-auth/utils/random";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { createHash } from "@better-auth/utils/hash";
import { createOTP } from "@better-auth/utils/otp";
import { base32 } from "@better-auth/utils/base32";
import { z } from "zod";
import { scheduleEmail } from "../scheduleEmail.server";
const generateRandomString = createRandomStringGenerator("A-Z", "0-9");
const SecretSchema = z.object({
secret: z.string(),
});
export class MultiFactorAuthenticationService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async disableTotp(userId: string, params: { totpCode?: string; recoveryCode?: string }) {
const user = await this.#prismaClient.user.findFirst({
where: { id: userId },
include: {
mfaSecretReference: true,
},
});
if (!user) {
return {
success: false,
};
}
if (!user.mfaEnabledAt) {
return {
success: false,
};
}
if (!user.mfaSecretReference) {
return {
success: false,
};
}
// validate the TOTP code
const secretStore = getSecretStore(user.mfaSecretReference.provider);
const secretResult = await secretStore.getSecret(SecretSchema, user.mfaSecretReference.key);
if (!secretResult) {
return {
success: false,
};
}
const isValid = await this.#verifyTotpCodeOrRecoveryCode(
user,
user.mfaSecretReference,
params.totpCode,
params.recoveryCode
);
if (!isValid) {
return {
success: false,
};
}
// Delete the MFA secret
await secretStore.deleteSecret(user.mfaSecretReference.key);
// Delete the MFA backup codes
await this.#prismaClient.mfaBackupCode.deleteMany({
where: {
userId,
},
});
await this.#prismaClient.user.update({
where: { id: userId },
data: {
mfaEnabledAt: null,
mfaSecretReference: {
delete: true,
},
},
});
await scheduleEmail({
email: "mfa-disabled",
to: user.email,
userEmail: user.email,
});
return {
success: true,
};
}
public async enableTotp(userId: string) {
const user = await this.#prismaClient.user.findFirst({
where: { id: userId },
});
if (!user) {
throw new ServiceValidationError("User not found");
}
const secretStore = getSecretStore("DATABASE");
// Generate a new secret
const secret = generateRandomString(24);
const secretKey = `mfa:${userId}:${generateRandomString(8)}`;
// Store the secret in the SecretStore
await secretStore.setSecret(secretKey, {
secret,
});
// Update the user's secret reference to the secret store
await this.#prismaClient.user.update({
where: { id: userId },
data: {
mfaSecretReference: {
create: {
provider: "DATABASE",
key: secretKey,
},
},
},
});
// Return the secret and the recovery codes
const otpAuthUrl = createOTP(secret).url("trigger.dev", user.email);
const displaySecret = base32.encode(secret, {
padding: false,
});
return {
secret: displaySecret,
otpAuthUrl,
};
}
public async validateTotpSetup(userId: string, totpCode: string) {
const user = await this.#prismaClient.user.findFirst({
where: { id: userId },
include: {
mfaSecretReference: true,
},
});
if (!user) {
throw new ServiceValidationError("User not found");
}
if (!user.mfaSecretReference) {
throw new ServiceValidationError("User has not enabled MFA");
}
const secretStore = getSecretStore(user.mfaSecretReference.provider);
const secretResult = await secretStore.getSecret(SecretSchema, user.mfaSecretReference.key);
if (!secretResult) {
throw new ServiceValidationError("User has not enabled MFA");
}
const secret = secretResult.secret;
const otp = createOTP(secret, {
digits: 6,
period: 30,
});
const isValid = await otp.verify(totpCode);
if (!isValid) {
// Return the secret and the recovery codes
const otpAuthUrl = createOTP(secret).url("trigger.dev", user.email);
const displaySecret = base32.encode(secret, {
padding: false,
});
return {
success: false,
otpAuthUrl,
secret: displaySecret,
};
}
// Now that we've validated the TOTP code, we can enable MFA for the user
await this.#prismaClient.user.update({
where: { id: userId },
data: {
mfaEnabledAt: new Date(),
},
});
// Generate a new set of recovery codes
const recoveryCodes = Array.from({ length: 9 }, () => generateRandomString(16, "a-z", "0-9"));
// Delete any existing recovery codes
await this.#prismaClient.mfaBackupCode.deleteMany({
where: {
userId,
},
});
// Hash and store the recovery codes
for (const code of recoveryCodes) {
const hashedCode = await createHash("SHA-512", "hex").digest(code);
await this.#prismaClient.mfaBackupCode.create({
data: {
userId,
code: hashedCode,
},
});
}
await scheduleEmail({
email: "mfa-enabled",
to: user.email,
userEmail: user.email,
});
return {
success: true,
recoveryCodes,
};
}
async #verifyTotpCodeOrRecoveryCode(
user: User,
secretReference: SecretReference,
totpCode?: string,
recoveryCode?: string
) {
if (!totpCode && !recoveryCode) {
return false;
}
if (typeof totpCode === "string" && totpCode.length === 6) {
return this.#verifyTotpCode(user, secretReference, totpCode);
}
if (typeof recoveryCode === "string") {
return this.#verifyRecoveryCode(user, recoveryCode);
}
return false;
}
async #verifyTotpCode(user: User, secretReference: SecretReference, totpCode: string) {
const secretStore = getSecretStore(secretReference.provider);
const secretResult = await secretStore.getSecret(SecretSchema, secretReference.key);
if (!secretResult) {
return false;
}
const secret = secretResult.secret;
const isValid = await createOTP(secret, {
digits: 6,
period: 30,
}).verify(totpCode);
return isValid;
}
async #verifyRecoveryCode(user: User, recoveryCode: string) {
const hashedCode = await createHash("SHA-512", "hex").digest(recoveryCode);
const backupCode = await this.#prismaClient.mfaBackupCode.findFirst({
where: { userId: user.id, code: hashedCode, usedAt: null },
});
return !!backupCode;
}
// Public methods for login flow with security measures
public async verifyTotpForLogin(userId: string, totpCode: string) {
const user = await this.#prismaClient.user.findFirst({
where: { id: userId },
include: {
mfaSecretReference: true,
},
});
if (!user || !user.mfaEnabledAt || !user.mfaSecretReference) {
return {
success: false,
error: "Invalid authentication code",
};
}
// Check for replay attack - if this code was already used
const hashedCode = await createHash("SHA-512", "hex").digest(totpCode);
if (user.mfaLastUsedCode === hashedCode) {
return {
success: false,
error: "Invalid authentication code",
};
}
// Verify the TOTP code
const isValid = await this.#verifyTotpCode(user, user.mfaSecretReference, totpCode);
if (!isValid) {
return {
success: false,
error: "Invalid authentication code",
};
}
// Mark this code as used to prevent replay
await this.#prismaClient.user.update({
where: { id: userId },
data: {
mfaLastUsedCode: hashedCode,
},
});
return {
success: true,
};
}
public async verifyRecoveryCodeForLogin(userId: string, recoveryCode: string) {
const user = await this.#prismaClient.user.findFirst({
where: { id: userId },
});
if (!user || !user.mfaEnabledAt) {
return {
success: false,
error: "Invalid authentication code",
};
}
const hashedCode = await createHash("SHA-512", "hex").digest(recoveryCode);
// Find an unused recovery code
const backupCode = await this.#prismaClient.mfaBackupCode.findFirst({
where: {
userId: user.id,
code: hashedCode,
usedAt: null,
},
});
if (!backupCode) {
return {
success: false,
error: "Invalid authentication code",
};
}
// Mark this recovery code as used
await this.#prismaClient.mfaBackupCode.update({
where: { id: backupCode.id },
data: {
usedAt: new Date(),
},
});
return {
success: true,
};
}
}