Skip to content

feat: Login anomaly detection & suspicious activity alerts#278

Closed
sirrodgepodge wants to merge 1 commit intorohitdash08:mainfrom
sirrodgepodge:feat/login-anomaly-detection
Closed

feat: Login anomaly detection & suspicious activity alerts#278
sirrodgepodge wants to merge 1 commit intorohitdash08:mainfrom
sirrodgepodge:feat/login-anomaly-detection

Conversation

@sirrodgepodge
Copy link

Summary

Implements login anomaly detection and suspicious activity alerts as described in #124.

What's Included

Login History Tracking

  • Every login attempt (success/failure) is recorded with IP address, user agent, and timestamp
  • New login_history database table with proper indexes

Anomaly Detection Engine (app/services/login_anomaly.py)

Detects four types of suspicious activity:

  • New IP address — login from an IP not previously seen for the user
  • New device — login from an unrecognized user agent
  • Unusual time — login during off-hours (UTC 01:00–05:00)
  • Brute force — 5+ failed attempts within a 30-minute window

Security Alerts

  • In-app alerts stored in security_alerts table
  • Email notifications via existing SMTP infrastructure (when configured)
  • Duplicate alert suppression (1-hour dedup window)
  • Login response includes security_warnings array when anomalies are detected

New API Endpoints

Method Endpoint Description
GET /auth/login-history Paginated login history
GET /auth/security-alerts User's security alerts
POST /auth/security-alerts/<id>/acknowledge Dismiss an alert

Tests

9 comprehensive tests covering:

  • Login history recording and pagination
  • Failed login tracking
  • New IP/device detection
  • Second login from same environment (no false positives)
  • Security alert creation and acknowledgment
  • Cross-user alert isolation (authorization)
  • Brute force detection

Documentation

  • Full API documentation in docs/login-anomaly-detection.md
  • Database schema, configuration, and threshold documentation

Files Changed

  • packages/backend/app/models.py — LoginHistory + SecurityAlert models
  • packages/backend/app/services/login_anomaly.py — Detection engine (new)
  • packages/backend/app/routes/auth.py — Integration + new endpoints
  • packages/backend/app/db/schema.sql — New tables and indexes
  • packages/backend/tests/test_login_anomaly.py — Test suite (new)
  • docs/login-anomaly-detection.md — Documentation (new)
  • Minor Python 3.9 compatibility fixes (__future__ annotations)

Closes #124

- Add LoginHistory and SecurityAlert models with DB migrations
- Track login metadata (IP, user agent, timestamp) on every attempt
- Detect anomalies: new IP, new device, unusual login hour, brute force
- Create in-app security alerts with email notification support
- Add GET /auth/login-history (paginated) endpoint
- Add GET /auth/security-alerts endpoint
- Add POST /auth/security-alerts/<id>/acknowledge endpoint
- Return security_warnings in login response when anomalies detected
- Add comprehensive test suite (9 tests covering all features)
- Add documentation in docs/login-anomaly-detection.md
- Fix Python 3.9 compatibility with __future__ annotations

Closes rohitdash08#124
Copilot AI review requested due to automatic review settings March 1, 2026 06:54
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds login anomaly detection, login history tracking, and security alerting to the backend auth flow, including new authenticated endpoints to view history/alerts and acknowledge alerts.

Changes:

  • Record successful/failed logins in a new login_history table and detect anomalies (new IP/device, unusual time, brute force).
  • Create in-app security alerts (and attempt email notifications) when anomalies are detected; expose warnings in the /auth/login response.
  • Add /auth/login-history, /auth/security-alerts, and /auth/security-alerts/<id>/acknowledge endpoints plus a new test suite and docs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
packages/backend/app/services/login_anomaly.py New anomaly detection + alert creation/email notification logic.
packages/backend/app/routes/auth.py Integrates login recording/anomaly warnings; adds login-history + security-alert endpoints.
packages/backend/app/models.py Adds LoginHistory and SecurityAlert ORM models.
packages/backend/app/db/schema.sql Adds new tables and initial indexes.
packages/backend/tests/test_login_anomaly.py Adds tests for history endpoints, anomaly flags, and alerts/ack flow.
packages/backend/tests/conftest.py Reworks test Redis handling (currently problematic).
docs/login-anomaly-detection.md Adds API + schema + thresholds documentation.
packages/backend/app/services/{expense_import,cache,ai}.py Adds __future__.annotations for Python 3.9 typing compatibility.
packages/backend/app/{observability,config,init.py} Adds __future__.annotations for Python 3.9 typing compatibility.
packages/backend/app/routes/expenses.py Adds __future__.annotations for Python 3.9 typing compatibility.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Detects suspicious login activity by analysing login history for:
- New / previously-unseen IP addresses
- New / previously-unseen user agents (device fingerprint proxy)
- Unusual login hour (outside user's typical window)
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module docstring says unusual login hour detection is based on the user’s "typical window", but the implementation uses a fixed UTC off-hours range. Update the docstring to reflect the actual behavior (or adjust the implementation).

Suggested change
- Unusual login hour (outside user's typical window)
- Unusual login hour (during off-hours in a fixed 01:0005:00 UTC window)

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +41
# Use a fake redis for tests so we don't need a running Redis server
fake_redis = MagicMock()
fake_redis.get.return_value = "1" # refresh tokens always "valid"
ext.redis_client = fake_redis
# Also patch the reference imported in auth routes
from app.routes import auth as auth_mod
auth_mod.redis_client = fake_redis
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test Redis stub makes refresh tokens effectively never revoked (get always returns "1"), so /auth/logout can’t invalidate a refresh token and test_auth_logout_revokes_refresh_token will fail. Use an in-memory Redis implementation (e.g., fakeredis) or a small dict-backed stub that correctly implements setex/get/delete and resets state per test.

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +140
CREATE INDEX IF NOT EXISTS idx_login_history_user ON login_history(user_id, created_at DESC);

CREATE TABLE IF NOT EXISTS security_alerts (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
alert_type VARCHAR(50) NOT NULL,
message VARCHAR(500) NOT NULL,
metadata_json TEXT,
acknowledged BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_security_alerts_user ON security_alerts(user_id, acknowledged, created_at DESC);
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The login_history index only covers (user_id, created_at), but anomaly checks query on (user_id, ip_address, success), (user_id, user_agent, success), and brute-force counts query on (user_id, success, created_at). Without supporting composite indexes these will degrade to scans as the tables grow. Consider adding composite indexes aligned to those query patterns.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
import json


Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import json will fail flake8 (F401). Remove it or use it in the tests.

Suggested change
import json

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +207
send_email(user.email, "FinMind Security Alert", body)
except Exception:
logger.debug("Email notification failed for user %s", user_id)
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send_email() returns a boolean and swallows exceptions internally (see app/services/reminders.py), so this try/except will never log failures and email delivery problems become silent. Check the return value from send_email and log (at least debug/info) when it returns False.

Suggested change
send_email(user.email, "FinMind Security Alert", body)
except Exception:
logger.debug("Email notification failed for user %s", user_id)
email_sent = send_email(user.email, "FinMind Security Alert", body)
except Exception:
logger.debug("Email notification failed for user %s due to exception", user_id)
else:
if not email_sent:
logger.info(
"Email notification not sent for user %s (send_email returned False)",
user_id,
)

Copilot uses AI. Check for mistakes.
@bp.get("/security-alerts")
@jwt_required()
def security_alerts():
"""Return unacknowledged security alerts for the authenticated user."""
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says this endpoint returns unacknowledged alerts, but the query returns all alerts for the user (no acknowledged=False filter), and tests appear to rely on acknowledged alerts remaining visible. Either filter to unacknowledged alerts or update the docstring/contract to match the actual behavior.

Suggested change
"""Return unacknowledged security alerts for the authenticated user."""
"""Return recent security alerts for the authenticated user."""

Copilot uses AI. Check for mistakes.
import json
import logging
from datetime import datetime, timedelta
from typing import List, Optional
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import Optional will fail flake8 (F401). Remove it or use it.

Suggested change
from typing import List, Optional
from typing import List

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +188
# Best-effort email notification
_notify_user(user_id, flags, messages)

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Email deduplication is bypassed because _notify_user() is called with the original flags list even when all alerts were suppressed as duplicates (existing causes continue). This can spam users with repeated emails (especially unusual_time or repeated brute-force attempts) even though no new alert rows are created. Track which alerts were actually created and only notify for those (or move notification inside the if not existing branch).

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +48
entry = LoginHistory(
user_id=user_id,
ip_address=ip_address,
user_agent=user_agent,
success=success,
anomaly_flags=json.dumps(anomaly_flags) if anomaly_flags else None,
)
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ip_address and user_agent are persisted directly from request headers without length limiting/normalization. Since the DB columns are VARCHAR(45) / VARCHAR(500), a long X-Forwarded-For or UA header can trigger a DB error and fail login. Normalize/truncate before constructing LoginHistory (and similarly for failed logins).

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +25
| Anomaly | Flag | Description |
|---------|------|-------------|
| New IP Address | `new_ip` | Login from an IP not previously seen for this user |
| New Device | `new_device` | Login from a user agent not previously seen |
| Unusual Time | `unusual_time` | Login during UTC 01:00–05:00 |
| Brute Force | `multiple_failed_attempts` | 5+ failed login attempts within 30 minutes |
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anomaly table in this markdown uses leading double pipes (||), which renders as an extra empty column in GitHub-flavored markdown. Use single leading pipes (e.g., | Anomaly | Flag | Description |) for correct table formatting.

Copilot uses AI. Check for mistakes.
@sirrodgepodge
Copy link
Author

Closing — no longer pursuing this bounty.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Login anomaly detection & suspicious activity alerts

2 participants