fix: session validation fixes
This commit is contained in:
@@ -1261,7 +1261,7 @@ function ActiveSessions(props: { userId: string }) {
|
||||
</Show>
|
||||
<div>
|
||||
Last active:{" "}
|
||||
{formatDate(session.lastRotatedAt || session.createdAt)}
|
||||
{formatDate(session.lastActiveAt || session.createdAt)}
|
||||
</div>
|
||||
<Show when={session.expiresAt}>
|
||||
<div class="text-xs">
|
||||
|
||||
@@ -419,11 +419,11 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
const conn = ConnectionFactory();
|
||||
const res = await conn.execute({
|
||||
sql: `SELECT session_id, token_family, created_at, expires_at, last_rotated_at,
|
||||
rotation_count, client_ip, user_agent
|
||||
sql: `SELECT session_id, token_family, created_at, expires_at, last_active_at,
|
||||
rotation_count, ip_address, user_agent
|
||||
FROM Session
|
||||
WHERE user_id = ? AND revoked = 0 AND expires_at > datetime('now')
|
||||
ORDER BY last_rotated_at DESC`,
|
||||
ORDER BY last_active_at DESC`,
|
||||
args: [userId]
|
||||
});
|
||||
|
||||
@@ -435,9 +435,9 @@ export const userRouter = createTRPCRouter({
|
||||
tokenFamily: row.token_family,
|
||||
createdAt: row.created_at,
|
||||
expiresAt: row.expires_at,
|
||||
lastRotatedAt: row.last_rotated_at,
|
||||
lastActiveAt: row.last_active_at,
|
||||
rotationCount: row.rotation_count,
|
||||
clientIp: row.client_ip,
|
||||
clientIp: row.ip_address,
|
||||
userAgent: row.user_agent,
|
||||
isCurrent: currentSession?.sessionId === row.session_id
|
||||
}));
|
||||
|
||||
@@ -349,19 +349,33 @@ export const RATE_LIMITS = CONFIG_RATE_LIMITS;
|
||||
|
||||
/**
|
||||
* Rate limiting middleware for login operations
|
||||
* In development, skips IP rate limiting to avoid self-DoS
|
||||
* For unknown IPs in production, uses stricter shared limits
|
||||
*/
|
||||
export async function rateLimitLogin(
|
||||
email: string,
|
||||
clientIP: string,
|
||||
event?: H3Event
|
||||
): Promise<void> {
|
||||
// In development, skip IP rate limiting to avoid self-DoS
|
||||
if (env.NODE_ENV !== "development") {
|
||||
const isUnknownIP = clientIP === "unknown";
|
||||
const ipIdentifier = isUnknownIP
|
||||
? `login:unknown-ip`
|
||||
: `login:ip:${clientIP}`;
|
||||
const ipLimit = isUnknownIP
|
||||
? { maxAttempts: 3, windowMs: RATE_LIMITS.LOGIN_IP.windowMs } // Stricter for unknown IPs
|
||||
: RATE_LIMITS.LOGIN_IP;
|
||||
|
||||
await checkRateLimit(
|
||||
`login:ip:${clientIP}`,
|
||||
RATE_LIMITS.LOGIN_IP.maxAttempts,
|
||||
RATE_LIMITS.LOGIN_IP.windowMs,
|
||||
ipIdentifier,
|
||||
ipLimit.maxAttempts,
|
||||
ipLimit.windowMs,
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
// Always rate limit by email in all environments
|
||||
await checkRateLimit(
|
||||
`login:email:${email}`,
|
||||
RATE_LIMITS.LOGIN_EMAIL.maxAttempts,
|
||||
@@ -372,48 +386,87 @@ export async function rateLimitLogin(
|
||||
|
||||
/**
|
||||
* Rate limiting middleware for password reset
|
||||
* In development, skips IP rate limiting to avoid self-DoS
|
||||
* For unknown IPs in production, uses stricter shared limits
|
||||
*/
|
||||
export async function rateLimitPasswordReset(
|
||||
clientIP: string,
|
||||
event?: H3Event
|
||||
): Promise<void> {
|
||||
// In development, skip IP rate limiting to avoid self-DoS
|
||||
if (env.NODE_ENV !== "development") {
|
||||
const isUnknownIP = clientIP === "unknown";
|
||||
const ipIdentifier = isUnknownIP
|
||||
? `password-reset:unknown-ip`
|
||||
: `password-reset:ip:${clientIP}`;
|
||||
const ipLimit = isUnknownIP
|
||||
? { maxAttempts: 2, windowMs: RATE_LIMITS.PASSWORD_RESET_IP.windowMs } // Stricter for unknown IPs
|
||||
: RATE_LIMITS.PASSWORD_RESET_IP;
|
||||
|
||||
await checkRateLimit(
|
||||
`password-reset:ip:${clientIP}`,
|
||||
RATE_LIMITS.PASSWORD_RESET_IP.maxAttempts,
|
||||
RATE_LIMITS.PASSWORD_RESET_IP.windowMs,
|
||||
ipIdentifier,
|
||||
ipLimit.maxAttempts,
|
||||
ipLimit.windowMs,
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting middleware for registration
|
||||
* In development, skips IP rate limiting to avoid self-DoS
|
||||
* For unknown IPs in production, uses stricter shared limits
|
||||
*/
|
||||
export async function rateLimitRegistration(
|
||||
clientIP: string,
|
||||
event?: H3Event
|
||||
): Promise<void> {
|
||||
// In development, skip IP rate limiting to avoid self-DoS
|
||||
if (env.NODE_ENV !== "development") {
|
||||
const isUnknownIP = clientIP === "unknown";
|
||||
const ipIdentifier = isUnknownIP
|
||||
? `registration:unknown-ip`
|
||||
: `registration:ip:${clientIP}`;
|
||||
const ipLimit = isUnknownIP
|
||||
? { maxAttempts: 2, windowMs: RATE_LIMITS.REGISTRATION_IP.windowMs } // Stricter for unknown IPs
|
||||
: RATE_LIMITS.REGISTRATION_IP;
|
||||
|
||||
await checkRateLimit(
|
||||
`registration:ip:${clientIP}`,
|
||||
RATE_LIMITS.REGISTRATION_IP.maxAttempts,
|
||||
RATE_LIMITS.REGISTRATION_IP.windowMs,
|
||||
ipIdentifier,
|
||||
ipLimit.maxAttempts,
|
||||
ipLimit.windowMs,
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting middleware for email verification
|
||||
* In development, skips IP rate limiting to avoid self-DoS
|
||||
* For unknown IPs in production, uses stricter shared limits
|
||||
*/
|
||||
export async function rateLimitEmailVerification(
|
||||
clientIP: string,
|
||||
event?: H3Event
|
||||
): Promise<void> {
|
||||
// In development, skip IP rate limiting to avoid self-DoS
|
||||
if (env.NODE_ENV !== "development") {
|
||||
const isUnknownIP = clientIP === "unknown";
|
||||
const ipIdentifier = isUnknownIP
|
||||
? `email-verification:unknown-ip`
|
||||
: `email-verification:ip:${clientIP}`;
|
||||
const ipLimit = isUnknownIP
|
||||
? { maxAttempts: 3, windowMs: RATE_LIMITS.EMAIL_VERIFICATION_IP.windowMs } // Stricter for unknown IPs
|
||||
: RATE_LIMITS.EMAIL_VERIFICATION_IP;
|
||||
|
||||
await checkRateLimit(
|
||||
`email-verification:ip:${clientIP}`,
|
||||
RATE_LIMITS.EMAIL_VERIFICATION_IP.maxAttempts,
|
||||
RATE_LIMITS.EMAIL_VERIFICATION_IP.windowMs,
|
||||
ipIdentifier,
|
||||
ipLimit.maxAttempts,
|
||||
ipLimit.windowMs,
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ACCOUNT_LOCKOUT = CONFIG_ACCOUNT_LOCKOUT;
|
||||
export async function checkAccountLockout(userId: string): Promise<{
|
||||
|
||||
@@ -416,6 +416,79 @@ describe("Rate Limiting", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unknown IP Handling", () => {
|
||||
it("should handle 'unknown' IP in development without rate limiting", async () => {
|
||||
// In development, IP rate limits should be skipped
|
||||
// This test assumes NODE_ENV is 'development' or 'test'
|
||||
const unknownIP = "unknown";
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Should allow many login attempts in development with unknown IP
|
||||
// (only email rate limit applies)
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
|
||||
const testEmail = `test-${Date.now()}-${i}@example.com`;
|
||||
await rateLimitLogin(testEmail, unknownIP);
|
||||
}
|
||||
|
||||
// Should be able to continue with different emails (no IP limit in dev)
|
||||
await rateLimitLogin(`final-${Date.now()}@example.com`, unknownIP);
|
||||
});
|
||||
|
||||
it("should still enforce email rate limits with unknown IP", async () => {
|
||||
const unknownIP = "unknown";
|
||||
const email = `test-${Date.now()}@example.com`;
|
||||
|
||||
// Use up email rate limit
|
||||
for (let i = 0; i < RATE_LIMITS.LOGIN_EMAIL.maxAttempts; i++) {
|
||||
await rateLimitLogin(email, unknownIP);
|
||||
}
|
||||
|
||||
// Next attempt should fail due to email limit
|
||||
try {
|
||||
await rateLimitLogin(email, unknownIP);
|
||||
expect.unreachable("Should have thrown");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TRPCError);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle unknown IP in password reset", async () => {
|
||||
const unknownIP = "unknown";
|
||||
|
||||
// In development, should allow many attempts (no IP limit)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitPasswordReset(unknownIP);
|
||||
}
|
||||
|
||||
// Should not throw in development
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle unknown IP in registration", async () => {
|
||||
const unknownIP = "unknown";
|
||||
|
||||
// In development, should allow many attempts (no IP limit)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitRegistration(unknownIP);
|
||||
}
|
||||
|
||||
// Should not throw in development
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle unknown IP in email verification", async () => {
|
||||
const unknownIP = "unknown";
|
||||
|
||||
// In development, should allow many attempts (no IP limit)
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await rateLimitEmailVerification(unknownIP);
|
||||
}
|
||||
|
||||
// Should not throw in development
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rate Limit Configuration", () => {
|
||||
it("should have reasonable limits configured", () => {
|
||||
// Login should be more permissive than registration
|
||||
|
||||
@@ -185,19 +185,22 @@ export async function getAuthSession(
|
||||
// In SSR contexts where headers may already be sent, use unsealSession directly
|
||||
if (skipUpdate) {
|
||||
const { unsealSession } = await import("vinxi/http");
|
||||
const cookieValue = getCookie(event, sessionConfig.cookieName);
|
||||
const cookieName = sessionConfig.name || "session";
|
||||
const cookieValue = getCookie(event, cookieName);
|
||||
if (!cookieValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await unsealSession<SessionData>(
|
||||
event,
|
||||
sessionConfig,
|
||||
cookieValue
|
||||
);
|
||||
// unsealSession returns Partial<Session<T>>, not T directly
|
||||
const session = await unsealSession(event, sessionConfig, cookieValue);
|
||||
|
||||
if (!data || !data.userId || !data.sessionId) {
|
||||
if (!session?.data || typeof session.data !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = session.data as SessionData;
|
||||
if (!data.userId || !data.sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user