-
-
Notifications
You must be signed in to change notification settings - Fork 11
Open
Labels
P2Important — depends on P1Important — depends on P1phase:2-orgsPhase 2: Multi-tenancy & OrganizationsPhase 2: Multi-tenancy & Organizations
Description
Context
The auth module has no protection against brute-force login attempts and no way to know when a user last logged in.
1. Account lockout
Current behavior
Failed login attempts have no consequence. An attacker can try unlimited passwords (only rate-limited by express-rate-limit at 10-20 req/15min, which is per-IP not per-account).
Expected behavior
Track failed attempts per account:
failedLoginAttempts: { type: Number, default: 0 }
lockUntil: { type: Date, default: null }
- Each failed signin → increment
failedLoginAttempts - After N failures (configurable, default: 5) → set
lockUntil = now + 30min - Locked account returns
423 Lockedwith message "Account temporarily locked, try again later" - Successful login → reset
failedLoginAttemptsto 0, clearlockUntil - Lock expires automatically (check
lockUntil < now)
Config
lockout: {
enabled: true,
maxAttempts: 5,
lockDuration: 1800, // 30 minutes in seconds
}2. lastLoginAt tracking
Expected behavior
On every successful authentication (local signin, OAuth callback, token refresh excluded):
lastLoginAt: { type: Date, default: null }
Update user.lastLoginAt = new Date() in the signin and OAuth callback controllers.
Why
- Simple activity indicator without external tooling
- Useful for admin dashboards ("last seen")
- Useful for cleanup (inactive accounts)
- No need for full audit logs here — detailed login history (IP, device, geo) belongs in PostHog/Datadog, not in the user model
Implementation notes
Both features touch the same signin flow, so they should be implemented together:
// In signin controller (pseudo-code)
const user = await findByEmail(email);
if (user.lockUntil && user.lockUntil > Date.now()) {
return res.status(423).json({ message: 'Account temporarily locked' });
}
if (!passwordMatch) {
user.failedLoginAttempts += 1;
if (user.failedLoginAttempts >= config.lockout.maxAttempts) {
user.lockUntil = new Date(Date.now() + config.lockout.lockDuration * 1000);
}
await user.save();
return res.status(401);
}
// Success
user.failedLoginAttempts = 0;
user.lockUntil = null;
user.lastLoginAt = new Date();
await user.save();User model additions
failedLoginAttempts: { type: Number, default: 0 }
lockUntil: { type: Date, default: null }
lastLoginAt: { type: Date, default: null }3 fields, low effort, high value.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important — depends on P1Important — depends on P1phase:2-orgsPhase 2: Multi-tenancy & OrganizationsPhase 2: Multi-tenancy & Organizations