Skip to content

Latest commit

 

History

History
788 lines (607 loc) · 31.9 KB

File metadata and controls

788 lines (607 loc) · 31.9 KB

Project Review Notes

⚠️ CRITICAL Security Issues (Fix Immediately)

1. Refresh Token Authentication Bypass

Note

Done: Access tokens now include typ="access" and get_jwt_user enforces it; refresh or typ-less tokens are rejected.

Location: app/managers/auth.py:478-544 (get_jwt_user)

  • Issue: Refresh tokens can be used as access tokens because access tokens don't carry a typ claim and get_jwt_user doesn't enforce token type. This completely undermines the token separation model and allows 30-day tokens to be used where 120-minute tokens should be required.
  • Impact: Attackers with a stolen refresh token get extended unauthorized access to protected endpoints. The entire point of short-lived access tokens is defeated.
  • Fix: Add typ="access" in AuthManager.encode_token and validate typ="access" in get_jwt_user. Reject any token without the correct type.
  • Priority: Fix immediately before addressing other issues.

2. CORS Wildcard with Credentials - SEVERE

Note

Done: CORS credentials are disabled for the API and startup now warns when CORS_ORIGINS=* is used.

Location: app/main.py:169, app/config/settings.py:56

  • Issue: Default CORS configuration allows ALL origins (cors_origins="*") while allow_credentials=True is set. This allows any malicious website to make authenticated requests to your API using user credentials.
  • Attack Scenario:
    • User logs into your API at api.example.com
    • User visits malicious site evil.com
    • evil.com makes fetch requests to api.example.com with user's cookies/credentials
    • API accepts these because CORS allows all origins with credentials
  • Impact: Complete authentication bypass, any website can impersonate logged-in users.
  • Fix:
    • Remove allow_credentials=True from main.py:169 OR restrict origins to explicit list
    • Add validation in Settings to reject "*" when credentials enabled
    • Update .env.example with proper origin examples (not *)
    • Add validator to trim/normalize cors_origins entries (split+strip) to avoid accidental leading spaces causing mismatches

3. No Rate Limiting on Authentication Endpoints

Note

Done: Rate limiting implemented using slowapi with conservative limits on all 7 authentication endpoints. Supports both in-memory (single-instance) and Redis (multi-instance) backends. Disabled by default, opt-in via RATE_LIMIT_ENABLED=true. Returns HTTP 429 with Retry-After header when limits exceeded. Violations tracked via Prometheus metrics.

Location: All routes in app/resources/auth.py, app/rate_limit/

  • Issue: No rate limiting implementation found anywhere. Vulnerable to:
    • Brute force login attacks (/login/)
    • Email flooding via registration/password reset
    • API key enumeration
    • Resource exhaustion via expensive queries
  • Impact: Attackers can perform unlimited login attempts, spam registration emails, and exhaust server resources.
  • Fix: Implement rate limiting middleware (e.g., slowapi, fastapi-limiter):
    • Login: 5 attempts per 15 minutes per IP ✅
    • Registration: 3 per hour per IP ✅
    • Password reset: 3 per hour per email ✅
    • Email verification: 10 per minute per IP ✅
    • Token refresh: 20 per minute per IP ✅
    • Reset password GET: 10 per minute per IP ✅
    • Reset password POST: 5 per hour per IP ✅

4. API Key Scopes Stored But Never Enforced

Note

This is a known issue, the scope field was added but enforcing it was going to be left until we have a more granualar user access model.

Location: app/managers/api_key.py:67-81 (stores), app/models/api_key.py:28 (field), app/managers/api_key.py:155-248 (validation doesn't check)

  • Issue: Scopes are stored in database but never validated anywhere. API keys with "read-only" scope can perform destructive operations. The ApiKeyAuth.__call__() validates key but never checks scopes. No middleware or dependency checks scopes against requested routes.
  • Impact: HIGH SEVERITY - API keys cannot be limited to specific permissions. A compromised read-only API key could be used for destructive operations.
  • Fix:
    • Add scope validation in ApiKeyAuth.__call__()
    • Create scope decorators for routes (e.g., @require_scope("users:write"))
    • Document scope format and enforcement

5. Sensitive Data in Request Logs

Note

Done: Request logging now redacts sensitive query parameters by name.

Location: app/middleware/logging_middleware.py:44

  • Issue: Logs full query strings including:
    • Password reset tokens: /reset-password/?code=SECRET_TOKEN
    • Verification codes: /verify/?code=SECRET_CODE
    • Potentially API keys if passed in query params
  • Impact: Token/code leakage in log files. If logs exposed or accessed by attackers, they gain password reset capabilities and account access.
  • Fix: Redact query parameters for sensitive routes or filter specific params like code, token, key before logging.

High Priority Security Issues

6. Timing Attack in Login - User Enumeration

Note

Done: Login now uses a precomputed dummy hash to ensure password verification always occurs, maintaining consistent response times regardless of user existence.

Location: app/managers/user.py:172-199

  • Issue: Different execution paths leak information through timing:

    • User not found → immediate failure (lines 175-184)
    • Invalid password → bcrypt verification + failure (lines 187-199)
    • User banned → check after password verification (lines 206-216)
  • Impact: An attacker can distinguish between "user doesn't exist" and "wrong password" by measuring response times. This enables user enumeration.

  • Fix: Always perform password hash verification, even for non-existent users:

    dummy_hash = hash_password("dummy_password_for_timing")
    user_do = await get_user_by_email_(...)
    hash_to_verify = user_do.password if user_do else dummy_hash
    verify_password(user_data["password"], hash_to_verify)
    # Then check user existence and other conditions

7. Timing Attack in Token Type Checking

Note

Done: Token type comparisons now use secrets.compare_digest() for constant-time comparison. Note: The timing difference (nanoseconds) is negligible compared to network jitter (milliseconds), making this purely defense-in-depth rather than addressing a realistic threat vector.

Location: app/managers/auth.py:191, 265, 380

  • Issue: String comparison if payload["typ"] != "refresh" may be vulnerable to timing attacks. An attacker could potentially distinguish between token types by measuring response time differences.
  • Fix: Use secrets.compare_digest() for constant-time comparison of token types.

8. Missing JWT ID (jti) Claims - No Token Revocation

Location: app/managers/auth.py:50-166 (all token encoding functions)

  • Issue: Tokens do not include a unique identifier (jti claim), making token revocation impossible. If a user's account is compromised, there's no way to invalidate specific outstanding tokens before expiry.
  • Impact: 120-minute window of vulnerability after password reset. Cannot implement:
    • Token blacklist
    • Token revocation on password change
    • Logout functionality (already in TODOs)
  • Fix: Add jti claim with UUID and implement Redis-based token blacklist.

9. Password Reset Tokens Reusable

Location: app/managers/auth.py:361-420 (reset_password method)

  • Issue: Password reset tokens are not invalidated after use. An attacker who intercepts a reset email could use the token multiple times within the 30-minute window.
  • Impact: Replay attacks on password reset flow.
  • Fix: Implement one-time-use tokens by storing token hash in database and deleting after first use.

10. Email Enumeration via Registration

Location: app/managers/user.py:120-128

  • Issue: Returns "A User with this email already exists" allowing attacker to enumerate valid emails via registration endpoint. While password reset correctly prevents enumeration (auth.py:321-327), registration doesn't.
  • Fix: Return success message even if email exists, or require email verification first before confirming registration.

11. Race Condition in Last Admin Deletion

Location: app/managers/user.py:248-262

  • Issue: Check for last admin and deletion are not atomic. In a concurrent scenario, two requests could simultaneously delete the last two admins:

    if check_user.role == RoleType.admin:
        admin_count = await session.execute(count_query)  # Line 255
        # Race condition window here
        if admin_count <= 1:
            raise HTTPException(...)
        await session.execute(delete(User).where(...))  # Line 264
  • Fix: Use database-level constraints or SELECT FOR UPDATE lock:

    count_query = (
        select(func.count())
        .select_from(User)
        .where(User.role == RoleType.admin)
        .with_for_update()
    )

12. KeyError Throws 500 Instead of 401

Note

Done: All JWT claim accesses now use payload.get() with explicit None checks in all token validation flows (refresh, verify, reset, get_jwt_user). Malformed tokens missing 'sub' or 'typ' claims now properly return 401 Unauthorized instead of 500 Internal Server Error.

Location: app/managers/auth.py:191, 265, 380

  • Issue: payload["typ"] / payload["sub"] are accessed directly in refresh/verify/reset flows; a token missing those keys will throw KeyError and return a 500 instead of a clean 401.
  • Impact: Creates information leakage (500 vs 401 tells attackers something about token structure) and poor UX.
  • Fix: Use payload.get(...) with fallback and treat missing keys as invalid token.

13. Missing JWT Format Guards in get_jwt_user

Note

Done: JWT format and length guards now applied in get_jwt_user before decoding. Tokens exceeding MAX_JWT_TOKEN_LENGTH or failing is_valid_jwt_format checks are rejected with 401 Unauthorized.

Location: app/managers/auth.py:478-544 (get_jwt_user)

  • Issue: get_jwt_user doesn't apply the JWT format/length guard you already built; very large or malformed tokens go straight to jwt.decode. Format validation exists (helpers.py:13-52) and is used in other flows (auth.py:174-185), but not in main authentication dependency.
  • Impact: DoS vulnerability via huge tokens, bypasses protection you already built.
  • Fix: Add the MAX_JWT_TOKEN_LENGTH and is_valid_jwt_format checks before jwt.decode.

High Priority Code Quality Issues

15. Massive Code Duplication - Token Encoding

Location: app/managers/auth.py:50-166

  • Issue: Four nearly identical methods with 95% duplicate code:
    • encode_token() (lines 50-76)
    • encode_refresh_token() (lines 79-105)
    • encode_verify_token() (lines 108-136)
    • encode_reset_token() (lines 139-166)
  • Impact: Violates DRY principle, bug fixes require updating 4 places, increases maintenance burden.
  • Fix: Extract common logic into _encode_jwt_token() helper method with parameters for token_type and expire_minutes.

16. Missing Database Index on Foreign Key

Note

Done: The index already existed in the database (created by the original migration) but wasn't marked in the model. Added index=True to match the actual database schema. See PR #816.

Location: app/models/api_key.py:22

  • Issue: user_id foreign key lacks index in the model definition. The index was created by the migration but not reflected in the model, causing a mismatch that could confuse alembic autogenerate.

  • Impact: Model/database schema mismatch; the index exists in the database but wasn't explicit in the code.

  • Fix: Add index=True to user_id field to match the database schema.

    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)

17. HTTPException Used for Success (200 OK)

Location: app/managers/auth.py:294

  • Issue: Verification success uses raise HTTPException(status.HTTP_200_OK, ...) for control flow and returns 200 with a message; docs already note this should be 204.
  • Impact: Semantically incorrect - exceptions should signal errors, not success. Confusing for developers and error tracking systems. Adds exception noise to logs/metrics.
  • Fix: Return normal responses instead of raising HTTP 200 exceptions. Refactor verify endpoint to handle success responses properly (204 No Content).

Medium Priority Security Issues

18. Redis Without Authentication in Production

Note

Real-world practice: Many production systems run Redis without AUTH, relying on network isolation (private VPC/subnet, firewalls, Kubernetes NetworkPolicies). This is widely accepted when Redis is in a private network with proper firewall rules. Defense-in-depth suggests using both network isolation AND authentication, but network isolation alone is often sufficient.

Location: app/main.py:82-91

  • Issue: Redis connection without password gets a warning but app continues. While unauthenticated Redis can be a security risk if exposed, many production deployments rely on network-level security controls (firewalls, ACLs, private networks) rather than Redis AUTH.
  • Impact: Potential risk if:
    • Network misconfiguration accidentally exposes Redis
    • SSRF vulnerability in the application allows internal network access
    • Container/Kubernetes networking issues expose Redis unexpectedly
  • Fix: Make this opt-in enforcement rather than mandatory. Add ENFORCE_REDIS_PASSWORD setting (default: false). When enabled, fail startup if REDIS_ENABLED=true and REDIS_PASSWORD is empty. This allows operators to choose based on their infrastructure (e.g., enable for cloud deployments, disable for Kubernetes with NetworkPolicies).
  • Current warning is sufficient for most use cases - it documents the decision and reminds operators to verify network isolation.

19. Weak Password Requirements

Location: app/schemas/request/auth.py:23, app/resources/auth.py:47

  • Issue: Password validation only checks minimum length of 8 characters. No complexity requirements (uppercase, lowercase, numbers, special chars).
  • Fix: Implement password strength validation:
    • Minimum 12 characters (NIST recommendation)
    • At least one uppercase, lowercase, number, and special character
    • Check against common password lists (e.g., pwned passwords API)
    • Add max_length=128 to prevent bcrypt issues

20. Missing Email Validation on Base Schema

Note

Done: Changed email field from str to EmailStr in the base schema. This provides proper email validation at the Pydantic level for all schemas that inherit from UserBase. See PR #817.

Location: app/schemas/base.py:13

  • Issue: Base email field was just str, not EmailStr. While registration validated email server-side, the schema accepted any string, potentially allowing malformed emails through validation.
  • Fix: Changed to email: EmailStr for proper Pydantic validation.

21. Search Term LIKE Wildcard Issues

Note

Partially done: Added max_length=100 to search_term in UserSearchParams to prevent DoS via long patterns. The func.concat and wildcard escaping improvements will be addressed in a future PR as they require more substantial changes to the query logic.

Location: app/managers/user.py:446-448, 460

  • Issue: User input in LIKE queries with f-strings:

    User.email.ilike(f"%{search_term}%")

    While SQLAlchemy parameterizes queries (safe from SQL injection), LIKE wildcards % and _ in user input can cause:

    • Unintended pattern matching
    • Performance degradation (full table scans)
    • DoS via expensive patterns (e.g., "%%%%%%%%%%")
  • Fix: Escape LIKE wildcards or add max_length validation (currently only min_length=1). Use SQLAlchemy's explicit concatenation:

    from sqlalchemy import func
    User.email.ilike(func.concat('%', search_term, '%'))

22. Missing Max Length on Input Fields

Note

Done: Added max_length constraints to all user request schemas (UserRegisterRequest, UserLoginRequest, UserEditRequest, UserChangePasswordRequest). Values now match database constraints: password=128, first_name=30, last_name=50.

Location: app/schemas/request/user.py:38-39, 57-59

  • Issue: User registration/edit fields lack maximum length validation:
    • password: str (no max_length)
    • first_name: str (no max_length)
    • last_name: str (no max_length) Database constraints exist (user.py:18-19: 30/50 chars), but schema doesn't enforce them, allowing oversized payloads.
  • Fix: Add max_length constraints matching database schema (first_name: 30, last_name: 50, password: 128).

23. API Key last_used_at Never Updated

Note

Done: Added last_used_at field to ApiKey model and update it in ApiKeyAuth.__call__() after successful validation. Field is exposed in ApiKeyResponse schema for security auditing.

Location: app/models/api_key.py:27 (field defined), app/managers/api_key.py:155-254 (updated on successful auth)

  • Issue: The last_used_at field did not exist in the model, making it impossible to identify stale/unused API keys for security audits.
  • Fix: Added last_used_at field and update it in ApiKeyAuth.__call__() after successful validation.

24. No Password Required for Self-Service Password Change

Location: app/resources/user.py:128-142

  • Issue: Uses can_edit_user dependency which allows users to change their own password OR admins to change any password. However, there's no requirement for the current password when a user changes their own password.
  • Impact: An attacker with access to a user's valid session could change the password without knowing the current password, locking out the legitimate user.
  • Fix: For self-service password changes, require current password validation. Only allow admin password changes without current password.

25. Magic Numbers Without Constants

Note

Done: Extracted hardcoded token expiry values to module-level constants (REFRESH_TOKEN_EXPIRE_MINUTES, VERIFY_TOKEN_EXPIRE_MINUTES, RESET_TOKEN_EXPIRE_MINUTES) for better maintainability.

Location: app/managers/auth.py:85, 114, 144

  • Issue: Hardcoded expiry times scattered throughout:

    • 60 * 24 * 30 (30 days for refresh tokens)
    • 10 (verification token expiry)
    • 30 (reset token expiry)
  • Fix: Define module-level or class-level constants:

    REFRESH_TOKEN_EXPIRE_MINUTES = 60 * 24 * 30  # 30 days
    VERIFY_TOKEN_EXPIRE_MINUTES = 10
    RESET_TOKEN_EXPIRE_MINUTES = 30

26. String Duplication in Cache Namespaces

Note

Done: Created app/cache/constants.py with CacheNamespaces class containing all namespace strings and format templates. Updated all cache decorators and invalidation functions to use the centralized constants.

Location: app/cache/invalidation.py:39, 44, app/resources/user.py:75, app/resources/api_key.py:58

  • Issue: Hardcoded namespace strings like "users", "users:list", "apikeys". Typos could cause cache misses, hard to refactor.

  • Fix: Define cache namespace constants in central location:

    # app/cache/constants.py
    class CacheNamespaces:
        USER = "user"
        USERS_LIST = "users:list"
        USERS_SINGLE = "users"
        API_KEYS = "apikeys"
        API_KEY_SINGLE = "apikey"

27. Dead Code - get_optional_user

Note

Not fixing: This is a template project and get_optional_user may be useful for users who customize the template for their own use. Keeping it for backwards compatibility and as a convenience for template users.

Location: app/managers/security.py:32-36

  • Issue: Function documented to return None if not authenticated, but dependency get_current_user always raises HTTPException if no auth. Function never works as intended.
  • Fix: Remove this function or fix the dependency to truly be optional.

28. Commented-Out Code

Note

Not fixing: The commented code (resend verification feature) is preserved as a reference for template users who may want to implement this functionality. It serves as documentation of a potential feature implementation.

Location: app/resources/auth.py:337-346

  • Issue: Large block of commented code (resend verification feature).
  • Fix: Remove commented code - it's in git history if needed.

Low Priority Issues

29. Information Leakage in Verify Endpoint

Location: app/managers/auth.py:276-278

  • Issue: Returns "INVALID_TOKEN" for already-verified users, allowing enumeration of verified vs unverified accounts.
  • Fix: Return same generic error message regardless of verification status.

30. Timing Attack in API Key Validation

Note

Not fixing: The prefix ("fta_") is public, documented information that provides no security value. Timing differences (nanoseconds) are completely buried by network jitter. The actual secret (32-byte random key) is compared via HMAC hash lookup in the database. No security benefit from constant-time prefix comparison.

Location: app/managers/api_key.py:136-137

  • Issue: String prefix comparison if not raw_key.startswith(cls.KEY_PREFIX) may leak information about valid key prefixes through timing analysis.
  • Fix: Use constant-time comparison or validate length first.

31. Missing Max Length on Token Refresh Schema

Note

Done: Added max_length=1024 to refresh token field, matching MAX_JWT_TOKEN_LENGTH. This provides defense-in-depth validation with Pydantic catching oversized tokens at schema level. See PR #818.

Location: app/schemas/request/auth.py:6-9

  • Issue: Refresh token field had no length validation in schema. While the endpoint validated format (auth.py:174-181), the schema itself had no constraints.
  • Fix: Add max_length to schema for defense in depth.

32. Bcrypt Work Factor Not Configurable

Location: app/database/helpers.py:15

  • Issue: Uses bcrypt default work factor (12 rounds). Not configurable for future-proofing against hardware improvements.
  • Fix: Make rounds configurable via settings (consider 13-14 rounds for 2026).

33. Cache Invalidation Overhead

Note

Done: Created invalidate_user_related_caches() helper in app/cache/invalidation.py that uses asyncio.gather() to invalidate user-specific and users-list caches in parallel. Updated all 5 user mutation endpoints to use the new helper.

Location: app/resources/user.py (lines 123-124, 162-163, 184-185, 205-206, 225-226)

  • Issue: Every user mutation invalidates TWO cache namespaces separately:

    await invalidate_user_cache(user_id)
    await invalidate_users_list_cache()
  • Fix: Create a combined invalidation helper using asyncio.gather() for better performance:

    async def invalidate_user_related_caches(user_id: int) -> None:
        await asyncio.gather(
            invalidate_user_cache(user_id),
            invalidate_users_list_cache()
        )

Existing TODOs Worth Prioritizing

  • Logout + token invalidation (security win) - ties into #8 above (jti claims)
  • Fix docs status codes/examples (quick win) - includes #17 above (204 for verify)
  • Test TODOs for better assertions
  • Refactor ApiKeyAuth.__call__() complexity - currently has high cyclomatic complexity (lines 155-248) but justified for detailed metrics tracking

Additional Enhancements From Code Review

  • Add created_at/updated_at on User model for auditability and to support features like "recently active users."
  • Account lockout policy - Lock account after 10 failed attempts, require admin unlock or time-based unlock
  • Security headers - Add CSP, X-Frame-Options, X-Content-Type-Options headers for HTML responses (password reset form)
  • JWT algorithm options - Add options={"verify_signature": True, "require": ["exp", "sub"]} to enforce required claims

Feature Ideas (Future Enhancements)

  • User-based rate limiting for authenticated endpoints: Current IP-based limiting (#3) protects auth endpoints. Next enhancement: add user/API-key based limiting for authenticated API operations. Benefits:

    • Fair usage across shared IPs (corporate NAT, proxies)
    • Per-user quotas and tiered limits (free vs premium)
    • Better tracking of individual user API consumption
    • Prevent individual account abuse

    Implementation approach:

    • Create middleware to extract user ID from JWT/API key before rate limiting
    • Add user_limiter alongside existing IP-based limiter
    • Use custom key function: user:{user_id} if authenticated, fallback to ip:{address} for unauthenticated
    • Apply to protected endpoints (keep IP-based for auth endpoints)
    • Support role-based dynamic limits (admins get higher quotas)

    Example usage:

    @router.get("/api/data")
    @user_rate_limited("500/hour")  # Per user, not per IP
    async def get_data(user: User = Depends(AuthManager())):
        pass

    Technical considerations:

    • Middleware runs before dependencies, so need early auth extraction
    • Reuse existing Redis/in-memory backend
    • No impact on current auth endpoint protection
    • Enables per-user analytics and quota enforcement
  • Session management: list/revoke active refresh tokens per user (ties into logout/invalidations and #8 jti claims)

  • Account security notifications: email user when password/email changes or when banned/unbanned

  • API key scope enforcement + per-route required scopes: pairs well with existing API key model (see #4 above - move from idea to implementation)

  • Optional 2FA (TOTP) for admin and/or all users - significant security enhancement

  • Webhook events for user lifecycle changes (register/verify/ban/unban/delete) to integrate with external systems

  • Token rotation on refresh to improve security

  • Audit logging for sensitive operations (login, password change, admin actions)


Summary

Total Issues Identified: 33

Priority Count Must Fix Before Production?
CRITICAL 5 (4 closed) ✅ YES - Security vulnerabilities
High 9 (3 closed) ✅ YES - Important security/quality
Medium 14 (0 closed) ⚠️ Recommended - Hardening needed
Low 5 (0 closed) 💡 Optional - Nice to have

Overall Assessment: The codebase demonstrates solid security foundations with proper JWT authentication, password hashing (bcrypt), and SQL injection protection via SQLAlchemy ORM. However, there are critical security issues that must be addressed before production deployment, particularly around CORS configuration, rate limiting, token validation, and API key scope enforcement.

Code Quality Score: 7.5/10

  • Architecture: 8/10 (well-organized, good patterns)
  • Security: 7/10 (strong foundations, missing critical protections)
  • Performance: 6/10 (missing indexes, cache overhead)
  • Maintainability: 7/10 (code duplication, magic numbers)
  • Error Handling: 8/10 (comprehensive, some inconsistencies)

Recommended Implementation Order

Sprint 1 - CRITICAL (This Week)

  1. Fix CORS configuration (#2) - Remove credentials or restrict origins
  2. Implement rate limiting (#3) - All auth endpoints
  3. Add token type validation (#1) - get_jwt_user() enforcement
  4. Redact sensitive data from logs (#5) - Query parameter filtering
  5. Fix API key scope enforcement (#4) - Add validation logic

Sprint 2 - High Priority (Next Week)

  1. Fix timing attacks (#6, #7) - Login and token validation
  2. Implement token revocation (#8) - Add jti claims + blacklist
  3. Add database index (#16) - api_key.user_id
  4. Refactor token encoding (#15) - Remove code duplication
  5. Fix password reset reuse (#9) - One-time tokens
  6. Add KeyError protection (#12) - Use payload.get()
  7. Add JWT format guards (#13) - to get_jwt_user()

Sprint 3 - Hardening (Next 2 Weeks)

  1. Strengthen password requirements (#19) - 12 chars + complexity
  2. Fix race condition (#11) - Admin deletion with SELECT FOR UPDATE
  3. Implement proper email validation (#20) - EmailStr in base schema
  4. Fix search term wildcards (#21) - Escape or use func.concat
  5. Add max_length constraints (#22) - Schema validation
  6. Fix email enumeration (#10) - Registration endpoint
  7. Redis password enforcement (#18) - Add opt-in ENFORCE_REDIS_PASSWORD setting
  8. Fix HTTPException 200 (#17) - Return proper responses

Sprint 4 - Quality & Polish (Next Month)

  1. Update API key last_used_at (#23)
  2. Require current password for self-change (#24)
  3. Extract magic numbers (#25) - Token expiry constants
  4. Centralize cache namespaces (#26) - Prevent typos
  5. Remove dead code (#27, #28) - Clean up codebase

Future Enhancements

  1. Account lockout policy (after N failed attempts)
  2. Multi-factor authentication (2FA/TOTP)
  3. Security headers (CSP, X-Frame-Options)
  4. Configurable bcrypt work factor (#32)
  5. Combined cache invalidation helpers (#33)

Files Requiring Changes

Critical Priority:

  • app/main.py - CORS configuration (#2), Redis password check (#18)
  • app/config/settings.py - CORS validation (#2), defaults, ENFORCE_REDIS_PASSWORD (#18)
  • app/middleware/logging_middleware.py - Sensitive data redaction (#5)
  • app/managers/auth.py - Token validation (#1, #7, #12, #13), timing attacks (#7), code duplication (#15), jti claims (#8)
  • app/managers/api_key.py - Scope enforcement (#4), last_used_at (#23)
  • app/managers/user.py - Timing attacks (#6), email enumeration (#10), race condition (#11)
  • app/rate_limit/ - Rate limiting implementation (#3)

High Priority:

  • app/schemas/request/auth.py - Password validation (#19), refresh schema (#31)
  • app/schemas/request/user.py - Field validation (#22)
  • app/schemas/base.py - Email type (#20)
  • app/models/api_key.py - Database index (#16)
  • app/resources/user.py - Password change validation (#24)
  • app/database/helpers.py - Configurable bcrypt (#32)

Medium Priority:

  • app/cache/ - Namespace constants (#26), combined invalidation (#33)
  • app/resources/auth.py - Remove dead code (#28), fix 200 exception (#17)
  • app/managers/security.py - Fix/remove get_optional_user (#27)

Positive Security Observations

The codebase demonstrates several excellent security practices:

  • Strong cryptography: bcrypt for passwords, HMAC-SHA256 for API keys, proper JWT handling
  • SQL injection protection: SQLAlchemy ORM throughout, no raw SQL
  • Token validation: Format checking before expensive crypto operations (DoS prevention)
  • Secret key validation: Strong validation at startup, prevents weak keys
  • Email enumeration protection: Password reset correctly prevents enumeration
  • Self-ban prevention: User can't ban themselves
  • Last admin protection: Check in place (though has race condition)
  • Category-based logging: Excellent separation of concerns for security monitoring
  • Proper password hashing: Automatic salting, modern algorithms
  • Database password validation: Prevents weak defaults in production

Notes

  • This review was conducted from the perspective of a senior Python security engineer
  • All line numbers and file paths are accurate as of the review date
  • Issues are prioritized based on security impact and ease of exploitation
  • Many "High Priority" issues are quick wins that should be addressed together with Critical issues
  • The codebase is production-ready AFTER addressing Critical and High priority issues