import { Ratelimit } from "@upstash/ratelimit"; import { env } from "~/env.server"; import { createRedisRateLimitClient, RateLimiter } from "~/services/rateLimiter.server"; import { singleton } from "~/utils/singleton"; export class SsoRateLimitError extends Error { public readonly retryAfter: number; constructor(retryAfter: number) { super("SSO sign-in rate limit exceeded."); this.retryAfter = retryAfter; } } function getRedisClient() { return createRedisRateLimitClient({ 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", }); } const ssoEmailRateLimiter = singleton("ssoEmailRateLimiter", initializeEmailLimiter); const ssoIpRateLimiter = singleton("ssoIpRateLimiter", initializeIpLimiter); function initializeEmailLimiter() { return new RateLimiter({ redisClient: getRedisClient(), keyPrefix: "auth:sso:email", limiter: Ratelimit.slidingWindow(5, "1 m"), logSuccess: false, logFailure: true, }); } function initializeIpLimiter() { return new RateLimiter({ redisClient: getRedisClient(), keyPrefix: "auth:sso:ip", limiter: Ratelimit.slidingWindow(20, "1 m"), logSuccess: false, logFailure: true, }); } export async function checkSsoEmailRateLimit(identifier: string): Promise { const result = await ssoEmailRateLimiter.limit(identifier); if (!result.success) { // Clamp: `reset` can already be in the past by the time we read it, // which would otherwise yield a negative Retry-After. const retryAfter = Math.max(0, new Date(result.reset).getTime() - Date.now()); throw new SsoRateLimitError(retryAfter); } } export async function checkSsoIpRateLimit(ip: string): Promise { const result = await ssoIpRateLimiter.limit(ip); if (!result.success) { // Clamp: `reset` can already be in the past by the time we read it, // which would otherwise yield a negative Retry-After. const retryAfter = Math.max(0, new Date(result.reset).getTime() - Date.now()); throw new SsoRateLimitError(retryAfter); } }