This document outlines the security measures implemented in the FlightOps application to protect user accounts and data.
- Authentication & Authorization
- Password Security
- Account Protection
- Rate Limiting & Brute Force Prevention
- Data Security
- Admin Controls
- Security Best Practices
- Configuration
- Stateless authentication using JSON Web Tokens (JWT)
- Tokens signed with configurable secret key (
JWT_SECRET) - Default token expiration: 7 days (configurable via
JWT_EXPIRATION) - Tokens include user ID, email, and username in payload
- JwtAuthGuard - Validates JWT tokens on protected routes
- LocalAuthGuard - Validates email/password credentials during login
- AdminGuard - Restricts sensitive operations to admin users only
- Pre-authorization required for all new registrations
- Only emails in the
pre_authorized_emailstable can register - Each email can only be used once
- Generic error messages prevent email enumeration attacks
- bcrypt algorithm with 10 salt rounds
- One-way hashing - passwords cannot be reversed
- Password hashing occurs automatically on user creation/update via TypeORM hooks
- Minimum length: 9 characters
- Maximum length: 100 characters
- Must contain at least 1 uppercase letter (A-Z)
- Must contain at least 1 number (0-9)
- Validated using
class-validatordecorators with regex patterns
- Admins can flag accounts for mandatory password reset
- Users flagged with
needs_password_reset = truebypass password validation on login - Redirected immediately to password reset page
- Reset flag cleared only after successful password change
✅ SECURE: Passwords are never logged to database or console logs. Previous versions that logged passwords have been removed for security.
- Account automatically locked after 3 failed password attempts
- Locked accounts cannot login (returns
401 Unauthorized) - Error message: "Account is locked. Please contact an administrator."
- Automatic unlock: Failed attempts reset automatically after 1 hour of inactivity
- If the last failed login was more than 1 hour ago, the account automatically unlocks on next login attempt
- Prevents permanent lockouts for legitimate users
- Locked status tracked with:
is_lockedflaglocked_attimestampfailed_login_attemptscounterlast_failed_logintimestamp
- Maximum 10 failed login attempts per email per 24 hours
- Rate limit window: 24 hours (configurable via
RATE_LIMIT_FAILED_ATTEMPTS_WINDOW) - Max attempts: 10 (configurable via
RATE_LIMIT_FAILED_ATTEMPTS_MAX) - Error message: "Too many failed login attempts. Please try again later."
- Returns
429 Too Many Requestsstatus code
- Only admins can unlock locked accounts
- Endpoint:
PATCH /users/:id/unlock - Resets all lockout fields:
- Sets
is_locked = false - Resets
failed_login_attempts = 0 - Clears
last_failed_loginandlocked_attimestamps
- Sets
- Failed attempt counter resets to 0 on successful login
- Failed attempts automatically reset after 1 hour of inactivity
- If no failed login attempts occur for 1 hour, the counter resets and account unlocks automatically
- This prevents legitimate users from being permanently locked out due to forgotten passwords
- Rate limit counter persists in database for the full 24-hour window
- This prevents attackers from resetting the limit by occasionally guessing correctly
✅ Proxy-Safe: Uses email addresses instead of IP addresses for rate limiting, ensuring reliable protection when deployed behind proxies or load balancers.
- All failed login attempts are logged to the
login_attemptstable - Before validating password, system counts failed attempts for that email in the past 24 hours
- If count ≥ 10, login is blocked with
429 Too Many Requests - Rate limit check occurs after user lookup to ensure email exists
- Works behind proxies - doesn't rely on client IP addresses
- Per-email protection - prevents targeted attacks on specific accounts
- Configurable limits - adjust thresholds based on threat landscape
- Persistent tracking - database-backed, survives server restarts
Optimized query performance with composite index on (email, created_at) columns.
| Attack Type | Mitigation |
|---|---|
| Single account brute force | Account locks after 3 attempts + rate limit at 10 attempts |
| Distributed attack (multiple emails) | Each email has individual account lockout (3 attempts) |
| Slow brute force | 24-hour window tracks all attempts, not just recent |
| Password spraying | Account lockout prevents trying multiple passwords per account |
- Passwords: Never stored in plaintext; only bcrypt hashes in database
- Password attempts: No longer logged (security improvement from previous versions)
- IP addresses: No longer stored (removed for proxy compatibility)
- JWT tokens: Stored client-side only; validated on each request
- Password hash field excluded from default queries (
select: false) - Explicit selection required to retrieve password hashes (only during authentication)
- Foreign key constraints with cascade delete for data integrity
- Global
ValidationPipewith:whitelist: true- strips unknown propertiesforbidNonWhitelisted: true- rejects requests with unknown propertiestransform: true- automatic type conversion
- DTO-level validation using
class-validator - Email format validation
- Username format validation (alphanumeric + underscore, 3-20 characters)
Admins have access to a comprehensive user management interface with the following capabilities:
- List of all registered users with:
- Username, email, admin status
- Account status (Active/Locked)
- Last login timestamp
- Failed login attempt count (on hover)
- Unlock Account - Reset lockout and failed attempts
- Flag for Password Reset - Force password change on next login
- Delete User - Remove user account (with safeguards)
- Cannot delete own account
- Cannot delete the last admin user
- All admin operations require both
JwtAuthGuardandAdminGuard
All failed login attempts are logged with:
- Email address
- Success/failure status
- Detailed failure reason (e.g., "Invalid password", "Account locked", "Rate limit exceeded")
- Timestamp
- ✅ Password hashing with bcrypt (10 rounds)
- ✅ JWT token-based authentication
- ✅ Account lockout after failed attempts
- ✅ Email-based rate limiting (proxy-safe)
- ✅ Admin-only sensitive operations
- ✅ No password logging
- ✅ Pre-authorization for new registrations
- ✅ Input validation and sanitization
- ✅ CORS configuration for specific frontend URL
Configure your reverse proxy/load balancer to add:
X-Frame-Options: DENYX-Content-Type-Options: nosniffX-XSS-Protection: 1; mode=blockStrict-Transport-Security: max-age=31536000; includeSubDomains
IMPORTANT: Change default values in production:
JWT_SECRET- Use a strong, random secret (minimum 256 bits)DATABASE_PASSWORD- Use a strong, unique passwordJWT_EXPIRATION- Consider shorter expiration for high-security environments
Configure in .env file:
# Max failed login attempts per email before rate limiting kicks in
RATE_LIMIT_FAILED_ATTEMPTS_MAX=10
# Time window in hours for counting failed attempts
RATE_LIMIT_FAILED_ATTEMPTS_WINDOW=24Recommended values:
- Development: MAX=20, WINDOW=1 (lenient for testing)
- Staging: MAX=10, WINDOW=24 (match production)
- Production: MAX=10, WINDOW=24 (balanced security)
- High-security: MAX=5, WINDOW=24 (strict protection)
The 3-attempt account lockout is currently hardcoded in users.service.ts:208. To change this threshold, modify:
// Lock account after 3 failed attempts
if (user.failed_login_attempts >= 3) {
user.is_locked = true;
user.locked_at = new Date();
}✅ This application is designed to work behind proxies.
Email-based rate limiting ensures reliable brute force protection regardless of proxy configuration. IP addresses are not used for security decisions.
Ensure regular backups of the following tables:
users- Contains account lockout stateslogin_attempts- Audit trail for security investigationspre_authorized_emails- Controls who can register
Monitor these events for security incidents:
- High volume of failed login attempts from single email
- Multiple accounts being locked in short timeframe
- Pattern of "Rate limit exceeded" errors
- Unusual admin actions (bulk deletions, unlocks)
If you discover a security vulnerability, please:
- Do not create a public GitHub issue
- Email security details to your security team
- Include steps to reproduce the issue
- Allow reasonable time for patching before disclosure
- ✅ Removed IP-based rate limiting (proxy-incompatible)
- ✅ Implemented email-based rate limiting
- ✅ Removed password logging from database and console
- ✅ Added configurable rate limit thresholds
- ✅ Optimized rate limit queries with database index
- ✅ Account lockout after 3 failed attempts
- ✅ IP-based rate limiting (deprecated)
- ✅ Password reset functionality
- ✅ Admin user management interface
- ✅ Pre-authorization email system
- ✅ JWT authentication
Last Updated: January 7, 2026 Version: 2.0