Piyush1225 commited on
Commit
5dc261b
·
1 Parent(s): 2b96220

ADD : Docer

Browse files
adaptiveauth/__init__.py CHANGED
@@ -56,7 +56,7 @@ from .risk import (
56
  from .auth import AuthService, OTPService, EmailService
57
  from .routers import (
58
  auth_router, user_router, admin_router,
59
- risk_router, adaptive_router
60
  )
61
 
62
 
@@ -140,7 +140,9 @@ class AdaptiveAuth:
140
  self._router.include_router(auth_router)
141
  self._router.include_router(user_router)
142
  self._router.include_router(admin_router)
143
-
 
 
144
  if self.enable_risk_assessment:
145
  self._router.include_router(risk_router)
146
  self._router.include_router(adaptive_router)
@@ -353,4 +355,5 @@ __all__ = [
353
  "admin_router",
354
  "risk_router",
355
  "adaptive_router",
 
356
  ]
 
56
  from .auth import AuthService, OTPService, EmailService
57
  from .routers import (
58
  auth_router, user_router, admin_router,
59
+ risk_router, adaptive_router, demo_router, session_intel_router
60
  )
61
 
62
 
 
140
  self._router.include_router(auth_router)
141
  self._router.include_router(user_router)
142
  self._router.include_router(admin_router)
143
+ self._router.include_router(demo_router)
144
+ self._router.include_router(session_intel_router)
145
+
146
  if self.enable_risk_assessment:
147
  self._router.include_router(risk_router)
148
  self._router.include_router(adaptive_router)
 
355
  "admin_router",
356
  "risk_router",
357
  "adaptive_router",
358
+ "demo_router",
359
  ]
adaptiveauth/auth/service.py CHANGED
@@ -250,7 +250,8 @@ class AuthService:
250
  if challenge.challenge_type == 'otp':
251
  verified = self.otp_service.verify_otp(user.tfa_secret, code)
252
  elif challenge.challenge_type == 'email':
253
- verified = challenge.challenge_code == code
 
254
 
255
  challenge.attempts += 1
256
 
 
250
  if challenge.challenge_type == 'otp':
251
  verified = self.otp_service.verify_otp(user.tfa_secret, code)
252
  elif challenge.challenge_type == 'email':
253
+ from ..core.security import constant_time_compare
254
+ verified = constant_time_compare(challenge.challenge_code or '', code)
255
 
256
  challenge.attempts += 1
257
 
adaptiveauth/models.py CHANGED
@@ -311,6 +311,42 @@ class AnomalyPattern(Base):
311
  resolved_at = Column(DateTime, nullable=True)
312
 
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  class StepUpChallenge(Base):
315
  """Step-up authentication challenges."""
316
  __tablename__ = "adaptiveauth_stepup_challenges"
 
311
  resolved_at = Column(DateTime, nullable=True)
312
 
313
 
314
+ class SessionTrustEvent(Base):
315
+ """Trust score change events throughout a session's lifecycle. (Feature 1 & 3)"""
316
+ __tablename__ = "adaptiveauth_trust_events"
317
+
318
+ id = Column(Integer, primary_key=True, index=True)
319
+ session_id = Column(Integer, ForeignKey("adaptiveauth_sessions.id", ondelete="CASCADE"), index=True)
320
+ user_id = Column(Integer, ForeignKey("adaptiveauth_users.id", ondelete="CASCADE"), index=True)
321
+
322
+ trust_score = Column(Float, nullable=False) # score AFTER this event
323
+ delta = Column(Float, default=0.0) # how much it changed
324
+ event_type = Column(String(100), nullable=False) # decay | behavior | context_change | micro_challenge | impossible_travel
325
+ reason = Column(String(255), nullable=True)
326
+ signals = Column(JSON, default=dict) # contributing signal values
327
+
328
+ created_at = Column(DateTime, default=datetime.utcnow, index=True)
329
+
330
+
331
+ class BehaviorSignalRecord(Base):
332
+ """Privacy-first behavior signal records. Only aggregated scores are stored. (Feature 2 & 8)"""
333
+ __tablename__ = "adaptiveauth_behavior_signals"
334
+
335
+ id = Column(Integer, primary_key=True, index=True)
336
+ session_id = Column(Integer, ForeignKey("adaptiveauth_sessions.id", ondelete="CASCADE"), index=True)
337
+ user_id = Column(Integer, ForeignKey("adaptiveauth_users.id", ondelete="CASCADE"), index=True)
338
+
339
+ # Client-computed signals (0.0–1.0 each). Raw keystrokes/mouse coords are NEVER sent.
340
+ typing_entropy = Column(Float, nullable=True) # 1.0 = very human-like keystroke rhythm
341
+ mouse_linearity = Column(Float, nullable=True) # higher = more curved/natural paths
342
+ scroll_variance = Column(Float, nullable=True) # moderate variance = normal human
343
+ local_risk_score = Column(Float, nullable=True) # client-side composite (privacy-first)
344
+
345
+ anomaly_score = Column(Float, default=0.0) # server-computed 0–100
346
+
347
+ recorded_at = Column(DateTime, default=datetime.utcnow, index=True)
348
+
349
+
350
  class StepUpChallenge(Base):
351
  """Step-up authentication challenges."""
352
  __tablename__ = "adaptiveauth_stepup_challenges"
adaptiveauth/risk/__init__.py CHANGED
@@ -12,6 +12,14 @@ from .factors import (
12
  )
13
  from .analyzer import BehaviorAnalyzer
14
  from .monitor import SessionMonitor, AnomalyDetector
 
 
 
 
 
 
 
 
15
 
16
  __all__ = [
17
  "RiskEngine",
@@ -25,4 +33,10 @@ __all__ = [
25
  "BehaviorAnalyzer",
26
  "SessionMonitor",
27
  "AnomalyDetector",
 
 
 
 
 
 
28
  ]
 
12
  )
13
  from .analyzer import BehaviorAnalyzer
14
  from .monitor import SessionMonitor, AnomalyDetector
15
+ from .session_intelligence import (
16
+ TrustScoreManager,
17
+ BehaviorSignalProcessor,
18
+ ImpossibleTravelDetector,
19
+ MicroChallengeEngine,
20
+ RiskExplainer,
21
+ StatisticalAnomalyDetector,
22
+ )
23
 
24
  __all__ = [
25
  "RiskEngine",
 
33
  "BehaviorAnalyzer",
34
  "SessionMonitor",
35
  "AnomalyDetector",
36
+ "TrustScoreManager",
37
+ "BehaviorSignalProcessor",
38
+ "ImpossibleTravelDetector",
39
+ "MicroChallengeEngine",
40
+ "RiskExplainer",
41
+ "StatisticalAnomalyDetector",
42
  ]
adaptiveauth/risk/session_intelligence.py ADDED
@@ -0,0 +1,671 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session Intelligence Engine
3
+ ===========================
4
+ Implements 8 advanced security capabilities beyond standard login-time auth:
5
+
6
+ 1. Continuous Verification – verify context throughout session lifecycle
7
+ 2. Behavioral Intelligence – typing rhythm, mouse paths, scroll patterns
8
+ 3. Dynamic Trust Score – evolving 0-100 score; decays + recovers
9
+ 4. Low-Friction Micro-Challenges – triggered only when trust drops
10
+ 5. Explainable Risk – factor contributions, confidence, audit trail
11
+ 6. AI-Powered Anomaly Scoring – isolation-forest-inspired statistical model
12
+ 7. Impossible Travel – haversine geo-velocity detection
13
+ 8. Privacy-First Design – client computes signals; server receives only score
14
+ """
15
+
16
+ import math
17
+ import hashlib
18
+ import random
19
+ from datetime import datetime, timedelta
20
+ from typing import Optional, List, Dict, Any, Tuple
21
+ from sqlalchemy.orm import Session
22
+
23
+ from ..models import (
24
+ User, UserSession, LoginAttempt, AnomalyPattern, RiskLevel,
25
+ SessionTrustEvent, BehaviorSignalRecord,
26
+ )
27
+
28
+ # ── City Geo-Coordinates ────────────────────────────────────────────────────
29
+ CITY_COORDS: Dict[str, Tuple[float, float]] = {
30
+ "new york": (40.7128, -74.0060),
31
+ "moscow": (55.7558, 37.6173),
32
+ "beijing": (39.9042, 116.4074),
33
+ "london": (51.5074, -0.1278),
34
+ "tokyo": (35.6762, 139.6503),
35
+ "sydney": (-33.8688, 151.2093),
36
+ "paris": (48.8566, 2.3522),
37
+ "dubai": (25.2048, 55.2708),
38
+ "singapore": ( 1.3521, 103.8198),
39
+ "toronto": (43.6532, -79.3832),
40
+ "chicago": (41.8781, -87.6298),
41
+ "miami": (25.7617, -80.1918),
42
+ "berlin": (52.5200, 13.4050),
43
+ "mumbai": (19.0760, 72.8777),
44
+ "bangkok": (13.7563, 100.5018),
45
+ "cairo": (30.0444, 31.2357),
46
+ "johannesburg": (-26.2041, 28.0473),
47
+ "sao paulo": (-23.5505, -46.6333),
48
+ "buenos aires": (-34.6037, -58.3816),
49
+ "seattle": (47.6062, -122.3321),
50
+ "los angeles": (34.0522, -118.2437),
51
+ "mexico city": (19.4326, -99.1332),
52
+ }
53
+
54
+ # ── Behavior Baseline (mean, std) for each client-collected metric ──────────
55
+ BEHAVIOR_BASELINE: Dict[str, Tuple[float, float]] = {
56
+ "typing_entropy": (0.70, 0.15), # 1.0 = perfectly human-like rhythm
57
+ "mouse_linearity": (0.62, 0.18), # 1.0 = natural curved paths
58
+ "scroll_variance": (0.48, 0.22), # moderate = organic human scrolling
59
+ }
60
+
61
+ # ── Trust score baselines by security level ─────────────────────────────────
62
+ TRUST_BASELINE: Dict[int, float] = {0: 95.0, 1: 80.0, 2: 60.0, 3: 35.0, 4: 10.0}
63
+
64
+ # ── In-memory caches ─────────────────────────────────────────────────────────
65
+ # {session_id: {"score": float, "last_update": datetime}}
66
+ _trust_cache: Dict[int, Dict[str, Any]] = {}
67
+
68
+ # {challenge_id: {"answer_hash": str, "expires": datetime, "attempts": int}}
69
+ _micro_challenges: Dict[str, Dict[str, Any]] = {}
70
+
71
+
72
+ # ═══════════════════════════════════════════════════════════════════════════
73
+ # Helper math
74
+ # ═══════════════════════════════════════════════════════════════════════════
75
+
76
+ def haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
77
+ """Great-circle distance in km between two geographic points."""
78
+ R = 6371.0
79
+ d_lat = math.radians(lat2 - lat1)
80
+ d_lon = math.radians(lon2 - lon1)
81
+ a = (math.sin(d_lat / 2) ** 2
82
+ + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
83
+ * math.sin(d_lon / 2) ** 2)
84
+ return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
85
+
86
+
87
+ def _z_anomaly(value: float, mean: float, std: float) -> float:
88
+ """Convert value to 0–100 anomaly score using a Z-score transform."""
89
+ if std < 1e-6:
90
+ return 0.0 if abs(value - mean) < 0.01 else 50.0
91
+ z = abs(value - mean) / std
92
+ return min(100.0, (z / 3.5) ** 0.75 * 100.0)
93
+
94
+
95
+ def _resolve_coords(
96
+ city: Optional[str],
97
+ lat: Optional[float],
98
+ lon: Optional[float],
99
+ ) -> Tuple[Optional[float], Optional[float]]:
100
+ if lat is not None and lon is not None:
101
+ return lat, lon
102
+ if city:
103
+ return CITY_COORDS.get(city.lower().strip(), (None, None))
104
+ return None, None
105
+
106
+
107
+ def _classify_anomaly(score: float) -> str:
108
+ if score >= 80: return "critical"
109
+ if score >= 60: return "high"
110
+ if score >= 40: return "medium"
111
+ if score >= 20: return "low"
112
+ return "normal"
113
+
114
+
115
+ def _build_summary(anomalous_factors: List[str], security_level: int) -> str:
116
+ if not anomalous_factors:
117
+ return "All signals normal. Trusted session context."
118
+ if len(anomalous_factors) == 1:
119
+ return f"{anomalous_factors[0]} flagged. Minimal elevated risk."
120
+ return (
121
+ f"{len(anomalous_factors)} risk signals triggered: "
122
+ + ", ".join(anomalous_factors[:3])
123
+ + (f" and {len(anomalous_factors) - 3} more." if len(anomalous_factors) > 3 else ".")
124
+ )
125
+
126
+
127
+ # ═══════════════════════════════════════════════════════════════════════════
128
+ # Feature 1 & 3 – Continuous Verification + Dynamic Trust Score
129
+ # ═══════════════════════════════════════════════════════════════════════════
130
+
131
+ class TrustScoreManager:
132
+ """
133
+ Maintains an evolving Trust Score (0–100) throughout the session lifecycle.
134
+
135
+ Decay rules:
136
+ • Time-based: –0.25 pts/min of inactivity
137
+ • Behavior anomaly: –4 to –24 depending on severity
138
+ • Context change (IP/device): –20
139
+ • Impossible travel: –50
140
+
141
+ Recovery rules:
142
+ • Good behavior signal: +2 to +6
143
+ • Micro-challenge pass: +20
144
+ """
145
+
146
+ DECAY_RATE = 0.25 # pts / minute inactive
147
+
148
+ def __init__(self, db: Session):
149
+ self.db = db
150
+
151
+ def get(self, session: UserSession) -> float:
152
+ """Return current trust score, hydrating from DB if not cached."""
153
+ sid = session.id
154
+ if sid in _trust_cache:
155
+ return _trust_cache[sid]["score"]
156
+
157
+ last = (
158
+ self.db.query(SessionTrustEvent)
159
+ .filter(SessionTrustEvent.session_id == sid)
160
+ .order_by(SessionTrustEvent.created_at.desc())
161
+ .first()
162
+ )
163
+ score = float(last.trust_score) if last else TRUST_BASELINE.get(
164
+ {"low": 0, "medium": 1, "high": 2, "critical": 3}.get(
165
+ session.current_risk_level or "low", 1
166
+ ), 80.0
167
+ )
168
+ _trust_cache[sid] = {"score": score, "last_update": datetime.utcnow()}
169
+ return score
170
+
171
+ def apply_decay(self, session: UserSession) -> Tuple[float, float]:
172
+ """Apply time-based trust decay. Returns (new_score, delta)."""
173
+ sid = session.id
174
+ cache = _trust_cache.get(sid)
175
+ if not cache:
176
+ return self.get(session), 0.0
177
+
178
+ elapsed_minutes = (datetime.utcnow() - cache["last_update"]).total_seconds() / 60
179
+ delta = -self.DECAY_RATE * elapsed_minutes
180
+ new_score = max(0.0, cache["score"] + delta)
181
+ _trust_cache[sid] = {"score": new_score, "last_update": datetime.utcnow()}
182
+ return new_score, delta
183
+
184
+ def update(
185
+ self,
186
+ session: UserSession,
187
+ delta: float,
188
+ event_type: str,
189
+ reason: str,
190
+ signals: Optional[Dict] = None,
191
+ ) -> float:
192
+ """Apply delta, clamp to [0, 100], persist to DB, update cache."""
193
+ current = self.get(session)
194
+ new_score = max(0.0, min(100.0, current + delta))
195
+ _trust_cache[session.id] = {"score": new_score, "last_update": datetime.utcnow()}
196
+
197
+ self.db.add(SessionTrustEvent(
198
+ session_id=session.id,
199
+ user_id=session.user_id,
200
+ trust_score=new_score,
201
+ delta=delta,
202
+ event_type=event_type,
203
+ reason=reason,
204
+ signals=signals or {},
205
+ ))
206
+ self.db.commit()
207
+ return new_score
208
+
209
+ def get_history(self, session_id: int, limit: int = 40) -> List[Dict]:
210
+ events = (
211
+ self.db.query(SessionTrustEvent)
212
+ .filter(SessionTrustEvent.session_id == session_id)
213
+ .order_by(SessionTrustEvent.created_at.desc())
214
+ .limit(limit)
215
+ .all()
216
+ )
217
+ return [
218
+ {
219
+ "score": e.trust_score,
220
+ "delta": e.delta,
221
+ "event_type": e.event_type,
222
+ "reason": e.reason,
223
+ "at": e.created_at.isoformat(),
224
+ }
225
+ for e in reversed(events)
226
+ ]
227
+
228
+ @staticmethod
229
+ def label(score: float) -> str:
230
+ if score >= 80: return "trusted"
231
+ if score >= 60: return "watchful"
232
+ if score >= 40: return "elevated"
233
+ if score >= 20: return "high_risk"
234
+ return "critical"
235
+
236
+ @staticmethod
237
+ def color(score: float) -> str:
238
+ if score >= 80: return "#22c55e"
239
+ if score >= 60: return "#84cc16"
240
+ if score >= 40: return "#f59e0b"
241
+ if score >= 20: return "#f97316"
242
+ return "#ef4444"
243
+
244
+
245
+ # ═══════════════════════════════════════════════════��═══════════════════════
246
+ # Feature 2 & 8 – Behavioral Intelligence + Privacy-First Design
247
+ # ═══════════════════════════════════════════════════════════════════════════
248
+
249
+ class BehaviorSignalProcessor:
250
+ """
251
+ Processes privacy-first behavior signals submitted by the client.
252
+
253
+ The client JS collects raw events (keydown timings, mouse paths,
254
+ scroll deltas) and computes aggregated 0–1 scores LOCALLY.
255
+ Only those aggregated scores are transmitted — no raw events ever leave
256
+ the browser (Feature 8: Privacy-First Design).
257
+
258
+ Server validates plausibility, computes anomaly_score,
259
+ and derives a trust delta.
260
+ """
261
+
262
+ def process(
263
+ self,
264
+ session: UserSession,
265
+ typing_entropy: float,
266
+ mouse_linearity: float,
267
+ scroll_variance: float,
268
+ local_risk_score: float,
269
+ db: Session,
270
+ ) -> Dict[str, Any]:
271
+ # Clamp all inputs to [0, 1]
272
+ te = max(0.0, min(1.0, float(typing_entropy)))
273
+ ml = max(0.0, min(1.0, float(mouse_linearity)))
274
+ sv = max(0.0, min(1.0, float(scroll_variance)))
275
+ lr = max(0.0, min(1.0, float(local_risk_score)))
276
+
277
+ # Per-feature anomaly (0–100, higher = more anomalous)
278
+ te_a = _z_anomaly(te, *BEHAVIOR_BASELINE["typing_entropy"]) if te > 0 else 50.0
279
+ ml_a = _z_anomaly(ml, *BEHAVIOR_BASELINE["mouse_linearity"]) if ml > 0 else 50.0
280
+ sv_a = _z_anomaly(sv, *BEHAVIOR_BASELINE["scroll_variance"]) if sv > 0 else 50.0
281
+
282
+ # Weighted composite server score
283
+ server_score = 0.40 * te_a + 0.35 * ml_a + 0.25 * sv_a
284
+ # Blend with client-reported composite (60/40 split)
285
+ final = 0.60 * server_score + 0.40 * (lr * 100.0)
286
+
287
+ # Trust delta
288
+ if final < 20: td = +6.0
289
+ elif final < 40: td = +2.0
290
+ elif final < 60: td = -4.0
291
+ elif final < 80: td = -12.0
292
+ else: td = -24.0
293
+
294
+ db.add(BehaviorSignalRecord(
295
+ session_id=session.id,
296
+ user_id=session.user_id,
297
+ typing_entropy=te,
298
+ mouse_linearity=ml,
299
+ scroll_variance=sv,
300
+ local_risk_score=lr,
301
+ anomaly_score=final,
302
+ ))
303
+ db.commit()
304
+
305
+ return {
306
+ "anomaly_score": round(final, 2),
307
+ "trust_delta": td,
308
+ "signals": {
309
+ "typing_entropy": round(te, 3),
310
+ "mouse_linearity": round(ml, 3),
311
+ "scroll_variance": round(sv, 3),
312
+ "local_risk_score": round(lr, 3),
313
+ },
314
+ "per_feature_anomaly": {
315
+ "typing": round(te_a, 1),
316
+ "mouse": round(ml_a, 1),
317
+ "scroll": round(sv_a, 1),
318
+ },
319
+ "classification": _classify_anomaly(final),
320
+ "privacy_note": (
321
+ "Raw keystrokes, mouse coordinates, and scroll positions "
322
+ "were processed entirely in-browser. Only aggregated scores "
323
+ "were transmitted to the server."
324
+ ),
325
+ }
326
+
327
+
328
+ # ═══════════════════════════════════════════════════════════════════════════
329
+ # Feature 7 – Impossible Travel + Pattern Clustering
330
+ # ═══════════════════════════════════════════════════════════════════════════
331
+
332
+ class ImpossibleTravelDetector:
333
+ """
334
+ Detects impossible travel using haversine distance + elapsed time.
335
+
336
+ Thresholds:
337
+ > 900 km/h → IMPOSSIBLE (fastest commercial jet ~900 km/h)
338
+ > 400 km/h → SUSPICIOUS (implausible without air travel)
339
+ < 400 km/h → PLAUSIBLE
340
+ """
341
+
342
+ IMPOSSIBLE_KMH = 900.0
343
+ SUSPICIOUS_KMH = 400.0
344
+
345
+ def __init__(self, db: Session):
346
+ self.db = db
347
+
348
+ def check(
349
+ self,
350
+ user_id: int,
351
+ city_now: str,
352
+ country_now: str,
353
+ lat_now: Optional[float] = None,
354
+ lon_now: Optional[float] = None,
355
+ time_gap_hours: Optional[float] = None, # override for demo mode
356
+ ) -> Dict[str, Any]:
357
+ """
358
+ Compare the current login location against the most recent successful login.
359
+ Pass time_gap_hours to simulate an arbitrary gap for demos.
360
+ """
361
+ last = (
362
+ self.db.query(LoginAttempt)
363
+ .filter(
364
+ LoginAttempt.user_id == user_id,
365
+ LoginAttempt.success == True,
366
+ LoginAttempt.city.isnot(None),
367
+ )
368
+ .order_by(LoginAttempt.attempted_at.desc())
369
+ .first()
370
+ )
371
+
372
+ if not last:
373
+ return self._no_history(city_now, country_now, lat_now, lon_now)
374
+
375
+ lat1, lon1 = _resolve_coords(last.city, last.latitude, last.longitude)
376
+ lat2, lon2 = _resolve_coords(city_now, lat_now, lon_now)
377
+
378
+ if lat1 is None or lat2 is None:
379
+ return {
380
+ "possible": True,
381
+ "verdict": "coords_unknown",
382
+ "message": f"Cannot resolve coordinates for '{last.city}' or '{city_now}'.",
383
+ "distance_km": 0.0, "speed_kmh": 0.0, "time_gap_minutes": 0.0,
384
+ "trust_delta": 0.0,
385
+ }
386
+
387
+ distance_km = haversine(lat1, lon1, lat2, lon2)
388
+
389
+ if time_gap_hours is not None:
390
+ time_gap_s = time_gap_hours * 3600
391
+ else:
392
+ time_gap_s = (datetime.utcnow() - last.attempted_at).total_seconds()
393
+
394
+ time_gap_min = max(time_gap_s / 60, 0.001)
395
+ speed_kmh = (distance_km / (time_gap_s / 3600)) if time_gap_s > 0 else 0.0
396
+
397
+ if distance_km < 50:
398
+ verdict, possible, trust_delta = "same_area", True, 0.0
399
+ msg = f"Same area as last login ({last.city}). No anomaly."
400
+ elif speed_kmh > self.IMPOSSIBLE_KMH:
401
+ verdict, possible, trust_delta = "impossible", False, -50.0
402
+ msg = (
403
+ f"IMPOSSIBLE TRAVEL: {distance_km:.0f} km in {time_gap_min:.0f} min "
404
+ f"= {speed_kmh:.0f} km/h (fastest jet ~900 km/h)."
405
+ )
406
+ elif speed_kmh > self.SUSPICIOUS_KMH:
407
+ verdict, possible, trust_delta = "suspicious", True, -20.0
408
+ msg = (
409
+ f"Suspicious speed: {speed_kmh:.0f} km/h over "
410
+ f"{distance_km:.0f} km from {last.city} in {time_gap_min:.0f} min."
411
+ )
412
+ else:
413
+ verdict, possible, trust_delta = "plausible", True, 0.0
414
+ msg = (
415
+ f"Plausible: {distance_km:.0f} km from {last.city} "
416
+ f"at {speed_kmh:.0f} km/h in {time_gap_min:.0f} min."
417
+ )
418
+
419
+ return {
420
+ "possible": possible,
421
+ "verdict": verdict,
422
+ "message": msg,
423
+ "distance_km": round(distance_km, 1),
424
+ "speed_kmh": round(speed_kmh, 1),
425
+ "time_gap_minutes": round(time_gap_min, 1),
426
+ "from": {"city": last.city, "lat": lat1, "lon": lon1},
427
+ "to": {"city": city_now, "country": country_now, "lat": lat2, "lon": lon2},
428
+ "trust_delta": trust_delta,
429
+ }
430
+
431
+ def _no_history(self, city, country, lat, lon) -> Dict[str, Any]:
432
+ return {
433
+ "possible": True, "verdict": "no_history",
434
+ "message": "No previous login on record — travel check skipped.",
435
+ "distance_km": 0.0, "speed_kmh": 0.0, "time_gap_minutes": 0.0,
436
+ "from": None,
437
+ "to": {"city": city, "country": country, "lat": lat, "lon": lon},
438
+ "trust_delta": 0.0,
439
+ }
440
+
441
+
442
+ # ═══════════════════════════════════════════════════════════════════════════
443
+ # Feature 4 – Low-Friction Micro-Challenges
444
+ # ═══════════════════════════════════════════════════════════════════════════
445
+
446
+ class MicroChallengeEngine:
447
+ """
448
+ Issues lightweight inline challenges ONLY when trust drops below a threshold.
449
+ Inspired by CAPTCHA but far less intrusive — a single arithmetic question.
450
+
451
+ Trust restored: +20 on pass, –15 on fail.
452
+ Threshold: trust < 40 → challenge recommended.
453
+ """
454
+
455
+ THRESHOLD = 40.0
456
+
457
+ def should_challenge(self, trust_score: float) -> bool:
458
+ return trust_score < self.THRESHOLD
459
+
460
+ def generate(self) -> Dict[str, Any]:
461
+ ops = [('+', lambda a, b: a + b), ('-', lambda a, b: a - b),
462
+ ('×', lambda a, b: a * b)]
463
+ sym, fn = random.choice(ops)
464
+ a = random.randint(2, 9) if sym == '×' else random.randint(10, 50)
465
+ b = random.randint(2, 9) if sym == '×' else random.randint(1, 20)
466
+ answer = fn(a, b)
467
+
468
+ cid = hashlib.sha256(
469
+ f"{datetime.utcnow().isoformat()}{random.random()}".encode()
470
+ ).hexdigest()[:16]
471
+
472
+ _micro_challenges[cid] = {
473
+ "answer_hash": hashlib.sha256(str(answer).encode()).hexdigest(),
474
+ "expires": datetime.utcnow() + timedelta(minutes=5),
475
+ "attempts": 0,
476
+ "type": "math",
477
+ }
478
+ return {
479
+ "challenge_id": cid,
480
+ "type": "math",
481
+ "question": f"What is {a} {sym} {b} ?",
482
+ "hint": "Answer is an integer.",
483
+ "expires_in_seconds": 300,
484
+ }
485
+
486
+ def verify(self, challenge_id: str, response: str) -> Dict[str, Any]:
487
+ ch = _micro_challenges.get(challenge_id)
488
+ if not ch:
489
+ return {"correct": False, "reason": "Challenge not found or already used.", "trust_delta": -5.0}
490
+ if ch["expires"] < datetime.utcnow():
491
+ _micro_challenges.pop(challenge_id, None)
492
+ return {"correct": False, "reason": "Challenge expired.", "trust_delta": -5.0}
493
+
494
+ given_hash = hashlib.sha256(response.strip().encode()).hexdigest()
495
+ correct = given_hash == ch["answer_hash"]
496
+ ch["attempts"] = ch.get("attempts", 0) + 1
497
+
498
+ if correct or ch["attempts"] >= 3:
499
+ _micro_challenges.pop(challenge_id, None)
500
+
501
+ return {
502
+ "correct": correct,
503
+ "trust_delta": +20.0 if correct else -15.0,
504
+ "reason": (
505
+ "✅ Correct – trust score restored." if correct
506
+ else f"❌ Incorrect. {'Max attempts reached.' if ch['attempts'] >= 3 else 'Try again.'}"
507
+ ),
508
+ }
509
+
510
+
511
+ # ═══════════════════════════════════════════════════════════════════════════
512
+ # Feature 5 – Explainable Risk Transparency
513
+ # ═══════════════════════════════════════════════════════════════════════════
514
+
515
+ class RiskExplainer:
516
+ """
517
+ Generates audit-ready, human-readable risk explanations.
518
+ Shows exactly: which signals contributed, their magnitude, and confidence.
519
+ """
520
+
521
+ _FACTOR_META = {
522
+ "location": {"icon": "🌍", "label": "Location", "weight": "97.68%"},
523
+ "device": {"icon": "💻", "label": "Device", "weight": "0.21%"},
524
+ "time": {"icon": "🕐", "label": "Time Pattern", "weight": "0.02%"},
525
+ "velocity": {"icon": "⚡", "label": "Velocity", "weight": "2.08%"},
526
+ "behavior": {"icon": "🧠", "label": "Behavior", "weight": "0.01%"},
527
+ }
528
+ _LEVEL_LABEL = {
529
+ 0: "No step-up – trusted context",
530
+ 1: "Standard password auth",
531
+ 2: "Email verification required (new IP)",
532
+ 3: "2FA required (unknown device)",
533
+ 4: "Access BLOCKED – critical risk",
534
+ }
535
+
536
+ def explain_login(
537
+ self,
538
+ risk_factors: Dict[str, float],
539
+ risk_level: str,
540
+ security_level: int,
541
+ ) -> Dict[str, Any]:
542
+ factors = []
543
+ for key, score in risk_factors.items():
544
+ meta = self._FACTOR_META.get(key, {"icon": "📌", "label": key.title(), "weight": "N/A"})
545
+ anomalous = score > 30.0
546
+ factors.append({
547
+ "factor": meta["label"],
548
+ "score": round(score, 1),
549
+ "icon": meta["icon"],
550
+ "model_weight": meta["weight"],
551
+ "status": "anomalous" if anomalous else "normal",
552
+ "detail": self._detail(key, anomalous),
553
+ "contribution": round(-score * 0.4 if anomalous else (30 - score) * 0.1, 1),
554
+ })
555
+ factors.sort(key=lambda x: abs(x["contribution"]), reverse=True)
556
+
557
+ anomalous_names = [f["factor"] for f in factors if f["status"] == "anomalous"]
558
+ confidence = max(0.50, min(0.99, 1.0 - len(anomalous_names) * 0.08))
559
+
560
+ return {
561
+ "audit_id": hashlib.sha256(
562
+ f"{datetime.utcnow().isoformat()}{str(risk_factors)}".encode()
563
+ ).hexdigest()[:12],
564
+ "timestamp": datetime.utcnow().isoformat(),
565
+ "risk_level": risk_level,
566
+ "security_level": security_level,
567
+ "action": self._LEVEL_LABEL.get(security_level, "Unknown"),
568
+ "confidence": round(confidence, 2),
569
+ "factors": factors,
570
+ "summary": _build_summary(anomalous_names, security_level),
571
+ }
572
+
573
+ def explain_trust_event(self, event_type: str, delta: float, signals: Dict) -> str:
574
+ msgs = {
575
+ "behavior_good": f"Human-like behavior signals (+{abs(delta):.0f} trust)",
576
+ "behavior_anomaly": f"Unusual behavior pattern (–{abs(delta):.0f} trust)",
577
+ "decay": f"Inactivity decay (–{abs(delta):.1f} trust)",
578
+ "context_change": f"IP or device changed (–{abs(delta):.0f} trust)",
579
+ "micro_challenge_pass": "Micro-challenge passed (+20 trust)",
580
+ "micro_challenge_fail": "Micro-challenge failed (–15 trust)",
581
+ "impossible_travel": "Impossible travel detected (–50 trust)",
582
+ "init": "Session initialised",
583
+ }
584
+ return msgs.get(event_type, f"Trust updated by {delta:+.1f}")
585
+
586
+ @staticmethod
587
+ def _detail(key: str, anomalous: bool) -> str:
588
+ details = {
589
+ "location": ("Location matches behavioral profile.", "New or unexpected country/city."),
590
+ "device": ("Known device fingerprint.", "Unknown device fingerprint."),
591
+ "time": ("Login within typical hours.", "Login outside typical hours."),
592
+ "velocity": ("Normal login frequency.", "Rapid or repeated attempts detected."),
593
+ "behavior": ("Behavior matches past patterns.", "Behavioral deviation detected."),
594
+ }
595
+ pair = details.get(key, ("Normal.", "Anomalous."))
596
+ return pair[1] if anomalous else pair[0]
597
+
598
+
599
+ # ═══════════════════════════════════════════════════════════════════════════
600
+ # Feature 6 – AI-Powered Anomaly Detection (Statistical Isolation Forest)
601
+ # ═══════════════════════════════════════════════════════════════════════════
602
+
603
+ class StatisticalAnomalyDetector:
604
+ """
605
+ Isolation-Forest–inspired anomaly scorer.
606
+
607
+ Scores a multi-dimensional feature vector against a learned baseline.
608
+ Each feature's anomaly contribution is computed via Z-score transform,
609
+ then weighted into a composite 0–100 anomaly score.
610
+
611
+ In production this would be replaced with a trained sklearn IsolationForest
612
+ or an LSTM sequence model; the interface is kept identical for easy swap-in.
613
+ """
614
+
615
+ BASELINE: Dict[str, Tuple[float, float]] = {
616
+ # feature → (mean, std) derived from research on legitimate user behavior
617
+ "typing_entropy": (0.70, 0.15),
618
+ "mouse_linearity": (0.62, 0.18),
619
+ "scroll_variance": (0.48, 0.22),
620
+ "hour_normalized": (0.55, 0.28), # 0 = midnight, 1 = noon normalised to [0,1]
621
+ "failed_attempts_norm": (0.03, 0.10), # recent failed attempts / 20
622
+ }
623
+ WEIGHTS: Dict[str, float] = {
624
+ "typing_entropy": 0.28,
625
+ "mouse_linearity": 0.24,
626
+ "scroll_variance": 0.14,
627
+ "hour_normalized": 0.18,
628
+ "failed_attempts_norm": 0.16,
629
+ }
630
+
631
+ def score(self, features: Dict[str, float]) -> Dict[str, Any]:
632
+ """
633
+ Score a feature vector. Returns anomaly_score ∈ [0, 100].
634
+ Higher means more anomalous.
635
+ """
636
+ per_feature: Dict[str, float] = {}
637
+ total = 0.0
638
+ for feat, (mean, std) in self.BASELINE.items():
639
+ val = features.get(feat, mean)
640
+ a = _z_anomaly(float(val), mean, std)
641
+ per_feature[feat] = round(a, 1)
642
+ total += a * self.WEIGHTS.get(feat, 0.10)
643
+
644
+ label, color = self._classify(total)
645
+ confidence = round(min(0.99, 0.50 + total / 200.0), 2)
646
+
647
+ return {
648
+ "anomaly_score": round(total, 2),
649
+ "classification": label,
650
+ "color": color,
651
+ "confidence": confidence,
652
+ "per_feature": per_feature,
653
+ "baseline_comparison": {
654
+ feat: {
655
+ "your_value": round(features.get(feat, mean), 3),
656
+ "typical_mean": mean,
657
+ "typical_std": std,
658
+ "z_score": round(abs(features.get(feat, mean) - mean) / max(std, 1e-6), 2),
659
+ }
660
+ for feat, (mean, std) in self.BASELINE.items()
661
+ },
662
+ "method": "statistical_isolation_forest_analogy",
663
+ }
664
+
665
+ @staticmethod
666
+ def _classify(score: float) -> Tuple[str, str]:
667
+ if score >= 80: return "CRITICAL", "#ef4444"
668
+ if score >= 60: return "HIGH", "#f97316"
669
+ if score >= 40: return "MEDIUM", "#f59e0b"
670
+ if score >= 20: return "LOW", "#84cc16"
671
+ return "NORMAL", "#22c55e"
adaptiveauth/routers/__init__.py CHANGED
@@ -6,6 +6,8 @@ from .user import router as user_router
6
  from .admin import router as admin_router
7
  from .risk import router as risk_router
8
  from .adaptive import router as adaptive_router
 
 
9
 
10
  __all__ = [
11
  "auth_router",
@@ -13,4 +15,6 @@ __all__ = [
13
  "admin_router",
14
  "risk_router",
15
  "adaptive_router",
 
 
16
  ]
 
6
  from .admin import router as admin_router
7
  from .risk import router as risk_router
8
  from .adaptive import router as adaptive_router
9
+ from .demo import router as demo_router
10
+ from .session_intel import router as session_intel_router
11
 
12
  __all__ = [
13
  "auth_router",
 
15
  "admin_router",
16
  "risk_router",
17
  "adaptive_router",
18
+ "demo_router",
19
+ "session_intel_router",
20
  ]
adaptiveauth/routers/adaptive.py CHANGED
@@ -179,7 +179,8 @@ async def verify_challenge(
179
  current_user.tfa_secret, request.code
180
  )
181
  elif challenge.challenge_type == 'email':
182
- verified = challenge.challenge_code == request.code
 
183
 
184
  challenge.attempts += 1
185
 
 
179
  current_user.tfa_secret, request.code
180
  )
181
  elif challenge.challenge_type == 'email':
182
+ from ..core.security import constant_time_compare
183
+ verified = constant_time_compare(challenge.challenge_code or '', request.code)
184
 
185
  challenge.attempts += 1
186
 
adaptiveauth/routers/admin.py CHANGED
@@ -20,6 +20,35 @@ from .. import schemas
20
  router = APIRouter(prefix="/admin", tags=["Admin"])
21
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  @router.get("/users", response_model=schemas.AdminUserList)
24
  async def list_users(
25
  page: int = Query(1, ge=1),
 
20
  router = APIRouter(prefix="/admin", tags=["Admin"])
21
 
22
 
23
+ @router.get("/email-status")
24
+ async def email_status(
25
+ current_user: User = Depends(require_admin()),
26
+ ):
27
+ """Check email service configuration status (admin only)."""
28
+ from ..auth.email import get_email_service
29
+ from ..config import get_settings
30
+ svc = get_email_service()
31
+ s = get_settings()
32
+ return {
33
+ "configured": svc.is_configured,
34
+ "fields": {
35
+ "MAIL_USERNAME": bool(s.MAIL_USERNAME),
36
+ "MAIL_PASSWORD": bool(s.MAIL_PASSWORD),
37
+ "MAIL_SERVER": bool(s.MAIL_SERVER),
38
+ "MAIL_FROM": bool(s.MAIL_FROM),
39
+ },
40
+ "mail_port": s.MAIL_PORT,
41
+ "starttls": s.MAIL_STARTTLS,
42
+ "env_prefix": "ADAPTIVEAUTH_",
43
+ "setup_instructions": (
44
+ "Create a .env file in the project root with: "
45
+ "ADAPTIVEAUTH_MAIL_USERNAME, ADAPTIVEAUTH_MAIL_PASSWORD, "
46
+ "ADAPTIVEAUTH_MAIL_SERVER, ADAPTIVEAUTH_MAIL_FROM, "
47
+ "ADAPTIVEAUTH_MAIL_PORT=587, ADAPTIVEAUTH_MAIL_STARTTLS=True"
48
+ )
49
+ }
50
+
51
+
52
  @router.get("/users", response_model=schemas.AdminUserList)
53
  async def list_users(
54
  page: int = Query(1, ge=1),
adaptiveauth/routers/demo.py ADDED
@@ -0,0 +1,763 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AdaptiveAuth Demo Router
3
+ Specialized endpoints for demonstrating the two core framework scenarios.
4
+
5
+ Scenario 1: User Behavior Anomaly Detection
6
+ - User has an established behavioral profile (normal IP, device, hours)
7
+ - When they log in from a suspicious context (new country, new device)
8
+ - The framework detects the behavior change and escalates security
9
+
10
+ Scenario 2: Anomaly (Attack) Detection
11
+ - An attacker makes repeated failed login attempts (brute force)
12
+ - The AnomalyDetector fires and flags the attacker's IP
13
+ - Future logins from that IP are blocked/escalated
14
+ - Demonstrates how the framework protects the entire application
15
+
16
+ FOR DEMONSTRATION PURPOSES ONLY - Do not expose in production without auth.
17
+ """
18
+ from fastapi import APIRouter, Depends, HTTPException, status
19
+ from sqlalchemy.orm import Session
20
+ from datetime import datetime, timedelta
21
+ from typing import Optional
22
+
23
+ from ..core.database import get_db
24
+ from ..core.security import hash_password, constant_time_compare
25
+ from ..auth.service import AuthService
26
+ from ..models import (
27
+ User, UserProfile, LoginAttempt, AnomalyPattern,
28
+ StepUpChallenge, RiskLevel, RiskEvent
29
+ )
30
+ from ..risk.analyzer import BehaviorAnalyzer
31
+ from ..risk.monitor import AnomalyDetector
32
+
33
+ router = APIRouter(prefix="/demo", tags=["Demo Scenarios"])
34
+
35
+ # ─────────────────────────── Demo Constants ────────────────────────────────
36
+
37
+ DEMO_USER_EMAIL = "demo.normal@adaptive.demo"
38
+ DEMO_USER_PASSWORD = "Demo@Normal123!"
39
+ DEMO_ADMIN_EMAIL = "demo.admin@adaptive.demo"
40
+ DEMO_ADMIN_PASSWORD = "Admin@Demo456!"
41
+
42
+ # The user's established "normal" context (30 days of history)
43
+ NORMAL_CONTEXT = {
44
+ "ip_address": "203.0.113.10", # RFC-5737 documentation IP
45
+ "user_agent": (
46
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
47
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
48
+ "Chrome/121.0.0.0 Safari/537.36"
49
+ ),
50
+ "device_fingerprint": "trusted-win-chrome-abc123",
51
+ "country": "US",
52
+ "city": "New York",
53
+ }
54
+
55
+ # A completely different context – new country, new mobile device, unusual hour
56
+ SUSPICIOUS_CONTEXT = {
57
+ "ip_address": "198.51.100.55", # RFC-5737 documentation IP
58
+ "user_agent": (
59
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) "
60
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1"
61
+ ),
62
+ "device_fingerprint": "unknown-iphone-device-xyz987",
63
+ "country": "RU",
64
+ "city": "Moscow",
65
+ }
66
+
67
+ # Attacker context – automated script from a different country
68
+ ATTACKER_CONTEXT = {
69
+ "ip_address": "192.0.2.100", # RFC-5737 documentation IP
70
+ "user_agent": "python-requests/2.31.0",
71
+ "device_fingerprint": "attacker-bot-device-001",
72
+ "country": "CN",
73
+ "city": "Beijing",
74
+ }
75
+
76
+
77
+ # ─────────────────────────── Setup ─────────────────────────────────────────
78
+
79
+ @router.post("/setup")
80
+ async def setup_demo(
81
+ reset: bool = False,
82
+ db: Session = Depends(get_db)
83
+ ):
84
+ """
85
+ Initialise demo environment.
86
+
87
+ Creates two users:
88
+ - demo.normal@adaptive.demo → regular user with 30 days of clean history
89
+ - demo.admin@adaptive.demo → admin for the dashboard
90
+
91
+ Pass ?reset=true to wipe and rebuild the demo user's behavioral profile.
92
+ """
93
+ created = []
94
+ messages = []
95
+
96
+ # ── Normal demo user ──────────────────────────────────────────────────
97
+ demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
98
+
99
+ if demo_user and reset:
100
+ # Wipe behavioral data so the demo starts fresh
101
+ db.query(UserProfile).filter(UserProfile.user_id == demo_user.id).delete()
102
+ db.query(LoginAttempt).filter(LoginAttempt.user_id == demo_user.id).delete()
103
+ db.commit()
104
+ messages.append("Behavioral profile reset for demo user.")
105
+
106
+ if not demo_user:
107
+ demo_user = User(
108
+ email=DEMO_USER_EMAIL,
109
+ password_hash=hash_password(DEMO_USER_PASSWORD),
110
+ full_name="Demo User",
111
+ is_active=True,
112
+ is_verified=True,
113
+ role="user",
114
+ created_at=datetime.utcnow() - timedelta(days=30),
115
+ )
116
+ db.add(demo_user)
117
+ db.commit()
118
+ db.refresh(demo_user)
119
+ created.append("demo_user")
120
+
121
+ # ── Behavioural history ───────────────────────────────────────────────
122
+ profile = db.query(UserProfile).filter(
123
+ UserProfile.user_id == demo_user.id
124
+ ).first()
125
+
126
+ if not profile:
127
+ analyzer = BehaviorAnalyzer(db)
128
+ profile = analyzer.get_or_create_profile(demo_user)
129
+
130
+ # Simulate 15 successful logins over the past 30 days
131
+ for i in range(15):
132
+ login_time = datetime.utcnow() - timedelta(days=i * 2, hours=(i % 9) + 8)
133
+ db.add(LoginAttempt(
134
+ user_id=demo_user.id,
135
+ email=DEMO_USER_EMAIL,
136
+ ip_address=NORMAL_CONTEXT["ip_address"],
137
+ user_agent=NORMAL_CONTEXT["user_agent"],
138
+ device_fingerprint=NORMAL_CONTEXT["device_fingerprint"],
139
+ country="US",
140
+ city="New York",
141
+ risk_score=7.5,
142
+ risk_level=RiskLevel.LOW.value,
143
+ security_level=0,
144
+ risk_factors={
145
+ "device": 0.0, "location": 0.0,
146
+ "time": 5.0, "velocity": 0.0, "behavior": 0.0,
147
+ },
148
+ success=True,
149
+ attempted_at=login_time,
150
+ ))
151
+
152
+ db.commit()
153
+
154
+ # Build known profile data from those logins
155
+ now_iso = datetime.utcnow().isoformat()
156
+ profile.known_ips = [{
157
+ "ip": NORMAL_CONTEXT["ip_address"],
158
+ "country": "US", "city": "New York",
159
+ "first_seen": (datetime.utcnow() - timedelta(days=30)).isoformat(),
160
+ "last_seen": now_iso, "count": 15,
161
+ }]
162
+ profile.known_browsers = [{
163
+ "user_agent": NORMAL_CONTEXT["user_agent"],
164
+ "browser_name": "Chrome",
165
+ "first_seen": (datetime.utcnow() - timedelta(days=30)).isoformat(),
166
+ "last_seen": now_iso, "count": 15,
167
+ }]
168
+ profile.known_devices = [{
169
+ "fingerprint": NORMAL_CONTEXT["device_fingerprint"],
170
+ "name": "Windows - Chrome",
171
+ "first_seen": (datetime.utcnow() - timedelta(days=30)).isoformat(),
172
+ "last_seen": now_iso, "count": 15,
173
+ }]
174
+ profile.typical_login_hours = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
175
+ profile.typical_login_days = [0, 1, 2, 3, 4] # Mon–Fri
176
+ profile.total_logins = 15
177
+ profile.successful_logins = 15
178
+ profile.failed_logins = 0
179
+ db.commit()
180
+ created.append("behavioral_profile (15 logins)")
181
+
182
+ # ── Admin user ────────────────────────────────────────────────────────
183
+ admin_user = db.query(User).filter(User.email == DEMO_ADMIN_EMAIL).first()
184
+ if not admin_user:
185
+ admin_user = User(
186
+ email=DEMO_ADMIN_EMAIL,
187
+ password_hash=hash_password(DEMO_ADMIN_PASSWORD),
188
+ full_name="Demo Admin",
189
+ is_active=True,
190
+ is_verified=True,
191
+ role="admin",
192
+ created_at=datetime.utcnow() - timedelta(days=30),
193
+ )
194
+ db.add(admin_user)
195
+ db.commit()
196
+ created.append("admin_user")
197
+
198
+ return {
199
+ "status": "ready",
200
+ "created": created,
201
+ "messages": messages,
202
+ "demo_credentials": {
203
+ "normal_user": {
204
+ "email": DEMO_USER_EMAIL,
205
+ "password": DEMO_USER_PASSWORD,
206
+ "description": (
207
+ "Regular user – 30 days of login history from "
208
+ "New York (US) on Windows Chrome, Mon–Fri 8AM–5PM"
209
+ ),
210
+ },
211
+ "admin_user": {
212
+ "email": DEMO_ADMIN_EMAIL,
213
+ "password": DEMO_ADMIN_PASSWORD,
214
+ "description": "Admin user – access the full dashboard",
215
+ },
216
+ },
217
+ "established_normal_behavior": {
218
+ "ip": NORMAL_CONTEXT["ip_address"],
219
+ "location": "New York, US",
220
+ "device": "Windows – Chrome",
221
+ "typical_hours": "8 AM – 5 PM (UTC)",
222
+ "typical_days": "Monday – Friday",
223
+ "login_count": 15,
224
+ },
225
+ }
226
+
227
+
228
+ # ─────────────────────── Scenario 1: Behavior Change ───────────────────────
229
+
230
+ @router.post("/scenario1/normal-login")
231
+ async def scenario1_normal_login(db: Session = Depends(get_db)):
232
+ """
233
+ SCENARIO 1 – Step 1: Login from the user's known trusted context.
234
+
235
+ Expected result: LOW RISK, Security Level 0–1, immediate access.
236
+ The framework recognises the familiar IP, device, and browser.
237
+ """
238
+ demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
239
+ if not demo_user:
240
+ raise HTTPException(
241
+ status_code=400,
242
+ detail="Demo not initialised. Call POST /api/v1/demo/setup first.",
243
+ )
244
+
245
+ auth_service = AuthService(db)
246
+ result = await auth_service.adaptive_login(
247
+ email=DEMO_USER_EMAIL,
248
+ password=DEMO_USER_PASSWORD,
249
+ context=NORMAL_CONTEXT,
250
+ )
251
+
252
+ return {
253
+ "scenario": "1 – User Behaviour Anomaly Detection",
254
+ "step": "Step 1 of 3: Normal login (trusted context)",
255
+ "context": {
256
+ "ip": NORMAL_CONTEXT["ip_address"],
257
+ "location": "New York, US ✓ Known",
258
+ "device": "Windows Chrome ✓ Known",
259
+ "time_utc": f"{datetime.utcnow().strftime('%H:%M')} UTC",
260
+ },
261
+ "framework_decision": result,
262
+ "explanation": _explain(result),
263
+ "what_the_framework_checked": [
264
+ "IP 203.0.113.10 → found in behavioral profile (15 previous logins)",
265
+ "Device fingerprint → matches known trusted device",
266
+ "Browser (Chrome/Win) → matches known browser",
267
+ "Login time → within typical hours (8 AM–5 PM)",
268
+ ],
269
+ }
270
+
271
+
272
+ @router.post("/scenario1/suspicious-login")
273
+ async def scenario1_suspicious_login(db: Session = Depends(get_db)):
274
+ """
275
+ SCENARIO 1 – Step 2: Same credentials, completely different context.
276
+
277
+ Expected result: HIGH RISK, Security Level 2–3, challenge required.
278
+ The framework detects multiple behavioral anomalies simultaneously.
279
+ """
280
+ demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
281
+ if not demo_user:
282
+ raise HTTPException(
283
+ status_code=400,
284
+ detail="Demo not initialised. Call POST /api/v1/demo/setup first.",
285
+ )
286
+
287
+ auth_service = AuthService(db)
288
+ result = await auth_service.adaptive_login(
289
+ email=DEMO_USER_EMAIL,
290
+ password=DEMO_USER_PASSWORD,
291
+ context=SUSPICIOUS_CONTEXT,
292
+ )
293
+
294
+ return {
295
+ "scenario": "1 – User Behaviour Anomaly Detection",
296
+ "step": "Step 2 of 3: Suspicious login (unknown context)",
297
+ "context": {
298
+ "ip": SUSPICIOUS_CONTEXT["ip_address"],
299
+ "location": "Moscow, Russia ✗ NEVER SEEN BEFORE",
300
+ "device": "iPhone ✗ UNKNOWN DEVICE",
301
+ "time_utc": f"{datetime.utcnow().strftime('%H:%M')} UTC",
302
+ },
303
+ "framework_decision": result,
304
+ "explanation": _explain(result),
305
+ "anomalies_triggered": [
306
+ "IP 198.51.100.55 NOT in behavioral profile",
307
+ "Country changed: US → Russia",
308
+ "Device fingerprint unknown (mobile vs usual desktop)",
309
+ "User agent changed: Windows Chrome → iPhone Safari",
310
+ ],
311
+ "note_on_challenge": (
312
+ "If challenge_type is 'email', use code '000000' in the demo "
313
+ "(real deployment sends a live email)."
314
+ if result.get("status") == "challenge_required" else None
315
+ ),
316
+ }
317
+
318
+
319
+ @router.post("/scenario1/complete-challenge")
320
+ async def scenario1_complete_challenge(
321
+ challenge_id: str,
322
+ code: str,
323
+ db: Session = Depends(get_db),
324
+ ):
325
+ """
326
+ SCENARIO 1 – Step 3: User passes the step-up challenge.
327
+
328
+ The framework grants access after identity is verified,
329
+ and updates the behavioral profile with the new context.
330
+
331
+ In the live demo, the email OTP is patched to accept '000000'.
332
+ """
333
+ # Patch the stored code so the demo works without a real email
334
+ challenge = db.query(StepUpChallenge).filter(
335
+ StepUpChallenge.id == int(challenge_id)
336
+ ).first()
337
+
338
+ if not challenge:
339
+ raise HTTPException(status_code=404, detail="Challenge not found.")
340
+
341
+ if challenge.challenge_type == "email" and not challenge.is_completed:
342
+ # Allow '000000' as a universal demo OTP
343
+ if code == "000000":
344
+ challenge.challenge_code = "000000"
345
+ db.commit()
346
+
347
+ auth_service = AuthService(db)
348
+ result = await auth_service.verify_step_up(
349
+ challenge_id=challenge_id,
350
+ code=code,
351
+ context=SUSPICIOUS_CONTEXT,
352
+ )
353
+
354
+ if result.get("status") == "error":
355
+ raise HTTPException(
356
+ status_code=400,
357
+ detail=result.get("message", "Verification failed"),
358
+ )
359
+
360
+ return {
361
+ "scenario": "1 – User Behaviour Anomaly Detection",
362
+ "step": "Step 3 of 3: Step-up challenge completed",
363
+ "result": result,
364
+ "explanation": (
365
+ "Identity verified. Framework grants access and begins learning "
366
+ "the new context as a potential future trusted location/device."
367
+ ),
368
+ "framework_actions_taken": [
369
+ "Step-up challenge marked complete",
370
+ "JWT access token issued",
371
+ "New session created with elevated security awareness",
372
+ "Risk event logged for audit trail",
373
+ ],
374
+ }
375
+
376
+
377
+ # ─────────────────────── Scenario 2: Attack Detection ──────────────────────
378
+
379
+ @router.post("/scenario2/simulate-attack")
380
+ async def scenario2_simulate_attack(
381
+ num_attempts: int = 12,
382
+ db: Session = Depends(get_db),
383
+ ):
384
+ """
385
+ SCENARIO 2 – Step 1: Simulate a brute-force attack.
386
+
387
+ Injects `num_attempts` failed login records from the attacker IP,
388
+ then runs the AnomalyDetector. Returns detected anomaly patterns.
389
+ """
390
+ if num_attempts < 1:
391
+ num_attempts = 1
392
+ if num_attempts > 25:
393
+ num_attempts = 25
394
+
395
+ attacker_ip = ATTACKER_CONTEXT["ip_address"]
396
+
397
+ # Inject failed attempts directly into the DB (no real password check)
398
+ for i in range(num_attempts):
399
+ db.add(LoginAttempt(
400
+ user_id=None,
401
+ email=DEMO_USER_EMAIL,
402
+ ip_address=attacker_ip,
403
+ user_agent=ATTACKER_CONTEXT["user_agent"],
404
+ device_fingerprint=ATTACKER_CONTEXT["device_fingerprint"],
405
+ country=ATTACKER_CONTEXT["country"],
406
+ city=ATTACKER_CONTEXT["city"],
407
+ risk_score=97.0,
408
+ risk_level=RiskLevel.CRITICAL.value,
409
+ security_level=4,
410
+ risk_factors={
411
+ "velocity": 100.0, "location": 90.0,
412
+ "device": 80.0, "time": 30.0, "behavior": 50.0,
413
+ },
414
+ success=False,
415
+ failure_reason="invalid_password",
416
+ attempted_at=datetime.utcnow() - timedelta(
417
+ seconds=(num_attempts - i) * 4
418
+ ),
419
+ ))
420
+
421
+ db.commit()
422
+
423
+ # Run anomaly detection
424
+ detector = AnomalyDetector(db)
425
+ brute_force_pattern = detector.detect_brute_force(
426
+ attacker_ip, window_minutes=60, threshold=5
427
+ )
428
+ stuffing_pattern = detector.detect_credential_stuffing(
429
+ attacker_ip, window_minutes=60, unique_users_threshold=3
430
+ )
431
+
432
+ anomalies = []
433
+ if brute_force_pattern:
434
+ anomalies.append({
435
+ "id": brute_force_pattern.id,
436
+ "type": "BRUTE FORCE",
437
+ "severity": "CRITICAL",
438
+ "attacker_ip": attacker_ip,
439
+ "failed_attempts": num_attempts,
440
+ "confidence": f"{brute_force_pattern.confidence * 100:.0f}%",
441
+ "detected_at": brute_force_pattern.first_detected.isoformat(),
442
+ })
443
+ if stuffing_pattern:
444
+ anomalies.append({
445
+ "id": stuffing_pattern.id,
446
+ "type": "CREDENTIAL STUFFING",
447
+ "severity": "CRITICAL",
448
+ "attacker_ip": attacker_ip,
449
+ "confidence": f"{stuffing_pattern.confidence * 100:.0f}%",
450
+ })
451
+
452
+ return {
453
+ "scenario": "2 – Anomaly (Attack) Detection",
454
+ "step": "Step 1 of 3: Attack simulated",
455
+ "attack_details": {
456
+ "attacker_ip": attacker_ip,
457
+ "attacker_location": "Beijing, China",
458
+ "tool_signature": ATTACKER_CONTEXT["user_agent"],
459
+ "attempts_injected": num_attempts,
460
+ "target": DEMO_USER_EMAIL,
461
+ "all_attempts_failed": True,
462
+ },
463
+ "anomalies_detected": anomalies,
464
+ "framework_response": (
465
+ f"AnomalyDetector flagged {attacker_ip} as CRITICAL. "
466
+ "All future login attempts from this IP will be blocked (Security Level 4)."
467
+ ),
468
+ "next_step": (
469
+ "Now call POST /demo/scenario2/legitimate-user "
470
+ "to see how a real user is treated compared to the attacker."
471
+ ),
472
+ }
473
+
474
+
475
+ @router.post("/scenario2/legitimate-user")
476
+ async def scenario2_legitimate_user(db: Session = Depends(get_db)):
477
+ """
478
+ SCENARIO 2 – Step 2: Legitimate user logs in while the attack is ongoing.
479
+
480
+ Uses the demo user's trusted context (New York IP, known device).
481
+ Demonstrates that the framework distinguishes real users from attackers.
482
+ """
483
+ demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
484
+ if not demo_user:
485
+ raise HTTPException(
486
+ status_code=400,
487
+ detail="Demo not initialised. Call POST /api/v1/demo/setup first.",
488
+ )
489
+
490
+ auth_service = AuthService(db)
491
+ result = await auth_service.adaptive_login(
492
+ email=DEMO_USER_EMAIL,
493
+ password=DEMO_USER_PASSWORD,
494
+ context=NORMAL_CONTEXT,
495
+ )
496
+
497
+ active_anomalies = db.query(AnomalyPattern).filter(
498
+ AnomalyPattern.is_active == True
499
+ ).count()
500
+
501
+ return {
502
+ "scenario": "2 – Anomaly (Attack) Detection",
503
+ "step": "Step 2 of 3: Legitimate user logs in during attack",
504
+ "user_context": {
505
+ "ip": NORMAL_CONTEXT["ip_address"],
506
+ "location": "New York, US ✓ Trusted",
507
+ "device": "Windows Chrome ✓ Trusted",
508
+ },
509
+ "framework_decision": result,
510
+ "explanation": _explain(result),
511
+ "active_threats_right_now": active_anomalies,
512
+ "key_insight": (
513
+ "Even though an attack is in progress against this account, "
514
+ "the legitimate user is recognised by their full behavioral profile "
515
+ "(trusted IP + trusted device + known browser + time pattern). "
516
+ "The framework can differentiate them from the attacker."
517
+ ),
518
+ }
519
+
520
+
521
+ @router.post("/scenario2/attacker-login-attempt")
522
+ async def scenario2_attacker_attempt(db: Session = Depends(get_db)):
523
+ """
524
+ SCENARIO 2 – Step 3: The attacker now tries a valid password.
525
+
526
+ Even with correct credentials, the attacker is blocked because their
527
+ IP is flagged, their user-agent is suspicious, and velocity rules fire.
528
+ """
529
+ auth_service = AuthService(db)
530
+ result = await auth_service.adaptive_login(
531
+ email=DEMO_USER_EMAIL,
532
+ password=DEMO_USER_PASSWORD, # correct password!
533
+ context=ATTACKER_CONTEXT,
534
+ )
535
+
536
+ return {
537
+ "scenario": "2 – Anomaly (Attack) Detection",
538
+ "step": "Step 3 of 3: Attacker uses correct password – still blocked",
539
+ "attacker_context": {
540
+ "ip": ATTACKER_CONTEXT["ip_address"],
541
+ "location": "Beijing, China ✗ Flagged IP",
542
+ "tool": ATTACKER_CONTEXT["user_agent"] + " ✗ Suspicious",
543
+ },
544
+ "framework_decision": result,
545
+ "explanation": _explain(result),
546
+ "key_insight": (
547
+ "The attacker has the correct password but is BLOCKED because: "
548
+ "(1) IP is flagged as CRITICAL in the anomaly database, "
549
+ "(2) velocity rules detect rapid prior attempts, "
550
+ "(3) suspicious user-agent (python-requests). "
551
+ "Correct password alone is not enough."
552
+ ),
553
+ }
554
+
555
+
556
+ @router.get("/scenario2/anomalies")
557
+ async def get_scenario2_anomalies(db: Session = Depends(get_db)):
558
+ """Get all active anomaly patterns (used by the live anomaly feed in the UI)."""
559
+ detector = AnomalyDetector(db)
560
+ anomalies = detector.get_active_anomalies()
561
+
562
+ return {
563
+ "active_anomalies": [
564
+ {
565
+ "id": a.id,
566
+ "type": a.pattern_type.upper().replace("_", " "),
567
+ "severity": a.severity.upper(),
568
+ "ip": a.ip_address,
569
+ "user_id": a.user_id,
570
+ "confidence": f"{a.confidence * 100:.0f}%",
571
+ "first_detected": a.first_detected.isoformat(),
572
+ "last_detected": a.last_detected.isoformat(),
573
+ "data": a.pattern_data,
574
+ }
575
+ for a in anomalies
576
+ ],
577
+ "total": len(anomalies),
578
+ }
579
+
580
+
581
+ @router.delete("/scenario2/clear-anomalies")
582
+ async def clear_demo_anomalies(db: Session = Depends(get_db)):
583
+ """Resolve all anomalies (clean-up after demo)."""
584
+ anomalies = db.query(AnomalyPattern).filter(
585
+ AnomalyPattern.is_active == True
586
+ ).all()
587
+ for a in anomalies:
588
+ a.is_active = False
589
+ a.resolved_at = datetime.utcnow()
590
+ db.commit()
591
+ return {"message": f"Resolved {len(anomalies)} anomaly pattern(s)."}
592
+
593
+
594
+ @router.post("/scenario2/run-monitoring-cycle")
595
+ async def run_monitoring_cycle(db: Session = Depends(get_db)):
596
+ """
597
+ Run one complete continuous-monitoring cycle:
598
+ - Clean expired sessions
599
+ - Re-scan all known attack IPs for brute-force / credential-stuffing patterns
600
+ - Collect session statistics
601
+ - Return a full monitoring report
602
+
603
+ The UI calls this endpoint every 2 seconds while monitoring is active.
604
+ """
605
+ from ..risk.monitor import SessionMonitor
606
+
607
+ monitor = SessionMonitor(db)
608
+ detector = AnomalyDetector(db)
609
+ attacker_ip = ATTACKER_CONTEXT["ip_address"]
610
+
611
+ # 1. Clean expired sessions
612
+ expired_cleaned = monitor.cleanup_expired_sessions()
613
+
614
+ # 2. Re-scan for active attack patterns
615
+ brute_force = detector.detect_brute_force(
616
+ attacker_ip, window_minutes=60, threshold=5
617
+ )
618
+ stuffing = detector.detect_credential_stuffing(
619
+ attacker_ip, window_minutes=60, unique_users_threshold=3
620
+ )
621
+
622
+ # 3. Session statistics
623
+ session_stats = monitor.get_session_statistics()
624
+
625
+ # 4. Failed login count in last hour
626
+ recent_failed = db.query(LoginAttempt).filter(
627
+ LoginAttempt.success == False,
628
+ LoginAttempt.attempted_at >= datetime.utcnow() - timedelta(minutes=60),
629
+ ).count()
630
+
631
+ # 5. All active anomalies
632
+ active = detector.get_active_anomalies()
633
+
634
+ threat_level = "NORMAL"
635
+ if active:
636
+ sev_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
637
+ max_sev = max(active, key=lambda a: sev_order.get(a.severity, 0)).severity
638
+ threat_level = max_sev.upper()
639
+
640
+ return {
641
+ "cycle_at": datetime.utcnow().isoformat(),
642
+ "sessions": {
643
+ "expired_cleaned": expired_cleaned,
644
+ "active": session_stats["active"],
645
+ "suspicious": session_stats["suspicious"],
646
+ "total": session_stats["total"],
647
+ },
648
+ "scan": {
649
+ "brute_force_active": brute_force is not None,
650
+ "credential_stuffing_active": stuffing is not None,
651
+ "total_active_anomalies": len(active),
652
+ "recent_failed_logins_1h": recent_failed,
653
+ },
654
+ "active_anomalies": [
655
+ {
656
+ "type": a.pattern_type.upper().replace("_", " "),
657
+ "severity": a.severity.upper(),
658
+ "ip": a.ip_address,
659
+ "confidence": f"{a.confidence * 100:.0f}%",
660
+ "last_detected": a.last_detected.isoformat(),
661
+ }
662
+ for a in active
663
+ ],
664
+ "threat_level": threat_level,
665
+ }
666
+
667
+
668
+ # ─────────────────────── Demo State ────────────────────────────────────────
669
+
670
+ @router.get("/state")
671
+ async def get_demo_state(db: Session = Depends(get_db)):
672
+ """Return current demo environment state (used by the UI to show setup status)."""
673
+ demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
674
+ admin_user = db.query(User).filter(User.email == DEMO_ADMIN_EMAIL).first()
675
+
676
+ profile = None
677
+ login_history_count = 0
678
+ known_devices = 0
679
+ known_ips = 0
680
+
681
+ if demo_user:
682
+ profile = db.query(UserProfile).filter(
683
+ UserProfile.user_id == demo_user.id
684
+ ).first()
685
+ login_history_count = db.query(LoginAttempt).filter(
686
+ LoginAttempt.user_id == demo_user.id
687
+ ).count()
688
+ if profile:
689
+ known_devices = len(profile.known_devices or [])
690
+ known_ips = len(profile.known_ips or [])
691
+
692
+ active_anomalies = db.query(AnomalyPattern).filter(
693
+ AnomalyPattern.is_active == True
694
+ ).count()
695
+
696
+ is_ready = (
697
+ demo_user is not None
698
+ and profile is not None
699
+ and login_history_count >= 5
700
+ )
701
+
702
+ return {
703
+ "is_ready": is_ready,
704
+ "demo_user_exists": demo_user is not None,
705
+ "admin_user_exists": admin_user is not None,
706
+ "behavioral_profile": {
707
+ "exists": profile is not None,
708
+ "login_history_count": login_history_count,
709
+ "known_devices": known_devices,
710
+ "known_ips": known_ips,
711
+ "typical_hours": profile.typical_login_hours if profile else [],
712
+ "typical_days": profile.typical_login_days if profile else [],
713
+ },
714
+ "active_anomalies": active_anomalies,
715
+ "credentials": {
716
+ "normal_user": {
717
+ "email": DEMO_USER_EMAIL,
718
+ "password": DEMO_USER_PASSWORD,
719
+ },
720
+ "admin": {
721
+ "email": DEMO_ADMIN_EMAIL,
722
+ "password": DEMO_ADMIN_PASSWORD,
723
+ },
724
+ } if demo_user else None,
725
+ }
726
+
727
+
728
+ # ─────────────────────── Helpers ───────────────────────────────────────────
729
+
730
+ def _explain(result: dict) -> str:
731
+ """Human-readable explanation of the framework's decision."""
732
+ s = result.get("status", "")
733
+ lvl = result.get("security_level", 0)
734
+
735
+ if s == "success":
736
+ if lvl == 0:
737
+ return (
738
+ "Framework recognised the trusted device, IP, and time pattern. "
739
+ "No additional verification needed – access granted immediately."
740
+ )
741
+ return "Access granted after challenge completion."
742
+
743
+ if s == "challenge_required":
744
+ challenge = result.get("challenge_type", "verification")
745
+ if lvl == 2:
746
+ return (
747
+ f"Unknown IP detected. {challenge.upper()} challenge sent. "
748
+ "Medium-risk: new location but no other anomalies."
749
+ )
750
+ if lvl == 3:
751
+ return (
752
+ f"Unknown device AND new IP. {challenge.upper()} challenge required. "
753
+ "High-risk: full identity verification needed."
754
+ )
755
+ return f"Elevated risk. {challenge.upper()} challenge triggered."
756
+
757
+ if s == "blocked":
758
+ return (
759
+ "BLOCKED. Velocity rules, anomaly patterns, or critical risk score "
760
+ "exceeded the threshold. Access denied."
761
+ )
762
+
763
+ return "Framework evaluated the request and made a security decision."
adaptiveauth/routers/session_intel.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Session Intelligence Router
3
+ ============================
4
+ HTTP endpoints for all 8 advanced security features.
5
+
6
+ Protected endpoints require a valid Bearer JWT token.
7
+ Demo endpoints are unauthenticated for demo purposes.
8
+ """
9
+ from datetime import datetime
10
+ from typing import Optional, Dict, Any
11
+
12
+ from fastapi import APIRouter, Depends, HTTPException, status, Request
13
+ from pydantic import BaseModel, Field, field_validator
14
+ from sqlalchemy.orm import Session
15
+
16
+ from ..core.database import get_db
17
+ from ..core.dependencies import get_current_user, get_current_session, oauth2_scheme
18
+ from ..core.security import decode_token
19
+ from ..models import User, UserSession
20
+ from ..risk.session_intelligence import (
21
+ TrustScoreManager,
22
+ BehaviorSignalProcessor,
23
+ ImpossibleTravelDetector,
24
+ MicroChallengeEngine,
25
+ RiskExplainer,
26
+ StatisticalAnomalyDetector,
27
+ CITY_COORDS,
28
+ )
29
+
30
+ router = APIRouter(prefix="/session-intel", tags=["Session Intelligence"])
31
+
32
+
33
+ # ── Request / Response schemas ───────────────────────────────────────────────
34
+
35
+ class BehaviorSignalInput(BaseModel):
36
+ """Privacy-first: client sends only aggregated scores, never raw events."""
37
+ typing_entropy: float = Field(..., ge=0, le=1, description="1.0 = very human-like")
38
+ mouse_linearity: float = Field(..., ge=0, le=1, description="1.0 = natural curved paths")
39
+ scroll_variance: float = Field(..., ge=0, le=1, description="1.0 = organic scroll rhythm")
40
+ local_risk_score: float = Field(..., ge=0, le=1, description="Client-computed composite (0 = safe)")
41
+
42
+
43
+ class TravelCheckInput(BaseModel):
44
+ from_city: str = Field(..., description="Origin city name")
45
+ from_country: str = Field("", description="Origin country")
46
+ to_city: str = Field(..., description="Destination city name")
47
+ to_country: str = Field("", description="Destination country")
48
+ time_gap_hours: float = Field(..., gt=0, description="Hours between the two events")
49
+ from_lat: Optional[float] = None
50
+ from_lon: Optional[float] = None
51
+ to_lat: Optional[float] = None
52
+ to_lon: Optional[float] = None
53
+
54
+
55
+ class AnomalyScoreInput(BaseModel):
56
+ typing_entropy: float = Field(0.70, ge=0, le=1)
57
+ mouse_linearity: float = Field(0.62, ge=0, le=1)
58
+ scroll_variance: float = Field(0.48, ge=0, le=1)
59
+ hour_normalized: float = Field(0.55, ge=0, le=1,
60
+ description="Current hour / 24 (0 = midnight)")
61
+ failed_attempts_norm: float = Field(0.00, ge=0, le=1,
62
+ description="Recent failed logins / 20")
63
+
64
+
65
+ class ChallengeVerifyInput(BaseModel):
66
+ challenge_id: str
67
+ response: str = Field(..., description="User's answer as a string")
68
+
69
+
70
+ class SimulateTrustDropInput(BaseModel):
71
+ target_score: float = Field(25.0, ge=0, le=100)
72
+ reason: str = Field("Simulated trust drop for demo")
73
+
74
+
75
+ class ExplainInput(BaseModel):
76
+ """Standalone explainability — submit raw factor scores for a report."""
77
+ location_score: float = Field(0.0, ge=0, le=100)
78
+ device_score: float = Field(0.0, ge=0, le=100)
79
+ time_score: float = Field(0.0, ge=0, le=100)
80
+ velocity_score: float = Field(0.0, ge=0, le=100)
81
+ behavior_score: float = Field(0.0, ge=0, le=100)
82
+ security_level: int = Field(0, ge=0, le=4)
83
+ risk_level: str = Field("low")
84
+
85
+
86
+ # ── Helpers ──────────────────────────────────────────────────────────────────
87
+
88
+ def _get_or_create_demo_session(user: User, db: Session) -> UserSession:
89
+ """Return the user's most recent active session, or a lightweight synthetic one."""
90
+ session = (
91
+ db.query(UserSession)
92
+ .filter(UserSession.user_id == user.id, UserSession.status == "active")
93
+ .order_by(UserSession.created_at.desc())
94
+ .first()
95
+ )
96
+ if session:
97
+ return session
98
+
99
+ # Create a minimal demo session so we can track trust events
100
+ session = UserSession(
101
+ user_id=user.id,
102
+ session_token=f"demo-{user.id}-{datetime.utcnow().timestamp():.0f}",
103
+ current_risk_level="low",
104
+ status="active",
105
+ created_at=datetime.utcnow(),
106
+ )
107
+ db.add(session)
108
+ db.commit()
109
+ db.refresh(session)
110
+ return session
111
+
112
+
113
+ def _resolve_demo_travel(
114
+ input_data: TravelCheckInput,
115
+ ) -> Dict[str, Any]:
116
+ """
117
+ Pure coordinate-based travel check for the demo endpoint.
118
+ This does not require a registered user — it just calculates distance + speed.
119
+ """
120
+ from ..risk.session_intelligence import haversine, _resolve_coords, CITY_COORDS
121
+
122
+ lat1, lon1 = _resolve_coords(input_data.from_city, input_data.from_lat, input_data.from_lon)
123
+ lat2, lon2 = _resolve_coords(input_data.to_city, input_data.to_lat, input_data.to_lon)
124
+
125
+ if lat1 is None or lat2 is None:
126
+ known = sorted(CITY_COORDS.keys())
127
+ missing = input_data.from_city if lat1 is None else input_data.to_city
128
+ return {
129
+ "possible": None,
130
+ "verdict": "coords_unknown",
131
+ "message": f"Unknown city: '{missing}'. Known cities: {', '.join(known)}.",
132
+ "distance_km": 0.0, "speed_kmh": 0.0,
133
+ "time_gap_minutes": input_data.time_gap_hours * 60,
134
+ "trust_delta": 0.0,
135
+ }
136
+
137
+ distance_km = haversine(lat1, lon1, lat2, lon2)
138
+ time_gap_s = input_data.time_gap_hours * 3600
139
+ time_gap_min = time_gap_s / 60
140
+ speed_kmh = distance_km / input_data.time_gap_hours
141
+
142
+ IMPOSSIBLE = 900.0
143
+ SUSPICIOUS = 400.0
144
+
145
+ if distance_km < 50:
146
+ verdict, possible, trust_delta = "same_area", True, 0.0
147
+ msg = "Same geographic area. No anomaly detected."
148
+ elif speed_kmh > IMPOSSIBLE:
149
+ verdict, possible, trust_delta = "impossible", False, -50.0
150
+ msg = (
151
+ f"🚨 IMPOSSIBLE TRAVEL: {distance_km:.0f} km in {time_gap_min:.0f} min "
152
+ f"= {speed_kmh:.0f} km/h — faster than any commercial aircraft (~900 km/h)."
153
+ )
154
+ elif speed_kmh > SUSPICIOUS:
155
+ verdict, possible, trust_delta = "suspicious", True, -20.0
156
+ msg = (
157
+ f"⚠ Suspicious speed: {speed_kmh:.0f} km/h over {distance_km:.0f} km "
158
+ f"in {time_gap_min:.0f} min. Requires air travel justification."
159
+ )
160
+ else:
161
+ verdict, possible, trust_delta = "plausible", True, 0.0
162
+ msg = (
163
+ f"✓ Plausible: {distance_km:.0f} km at {speed_kmh:.0f} km/h "
164
+ f"in {time_gap_min:.0f} min."
165
+ )
166
+
167
+ return {
168
+ "possible": possible,
169
+ "verdict": verdict,
170
+ "message": msg,
171
+ "distance_km": round(distance_km, 1),
172
+ "speed_kmh": round(speed_kmh, 1),
173
+ "time_gap_minutes": round(time_gap_min, 1),
174
+ "from": {
175
+ "city": input_data.from_city, "country": input_data.from_country,
176
+ "lat": lat1, "lon": lon1,
177
+ },
178
+ "to": {
179
+ "city": input_data.to_city, "country": input_data.to_country,
180
+ "lat": lat2, "lon": lon2,
181
+ },
182
+ "trust_delta": trust_delta,
183
+ }
184
+
185
+
186
+ # ═══════════════════════════════════════════════════════════════════════════
187
+ # Protected endpoints (require JWT Bearer token)
188
+ # ═══════════════════════════════════════════════════════════════════════════
189
+
190
+ @router.post("/behavior-signal")
191
+ async def receive_behavior_signal(
192
+ signal: BehaviorSignalInput,
193
+ current_user: User = Depends(get_current_user),
194
+ db: Session = Depends(get_db),
195
+ ):
196
+ """
197
+ Feature 2 & 8 – Submit privacy-first behavior signal.
198
+
199
+ Client computes typing_entropy, mouse_linearity, scroll_variance locally.
200
+ Only aggregated 0–1 scores are transmitted — no raw events leave the browser.
201
+ """
202
+ session = _get_or_create_demo_session(current_user, db)
203
+ processor = BehaviorSignalProcessor()
204
+ result = processor.process(
205
+ session=session,
206
+ typing_entropy=signal.typing_entropy,
207
+ mouse_linearity=signal.mouse_linearity,
208
+ scroll_variance=signal.scroll_variance,
209
+ local_risk_score=signal.local_risk_score,
210
+ db=db,
211
+ )
212
+
213
+ # Update trust score
214
+ tsm = TrustScoreManager(db)
215
+ event_type = "behavior_good" if result["trust_delta"] > 0 else "behavior_anomaly"
216
+ new_trust = tsm.update(
217
+ session=session,
218
+ delta=result["trust_delta"],
219
+ event_type=event_type,
220
+ reason=f"Behavior signal processed — anomaly score: {result['anomaly_score']:.1f}",
221
+ signals=result["signals"],
222
+ )
223
+
224
+ mce = MicroChallengeEngine()
225
+ return {
226
+ "status": "processed",
227
+ "behavior": result,
228
+ "trust": {
229
+ "score": round(new_trust, 2),
230
+ "label": TrustScoreManager.label(new_trust),
231
+ "color": TrustScoreManager.color(new_trust),
232
+ "delta": result["trust_delta"],
233
+ },
234
+ "micro_challenge_recommended": mce.should_challenge(new_trust),
235
+ }
236
+
237
+
238
+ @router.get("/trust-score")
239
+ async def get_trust_score(
240
+ current_user: User = Depends(get_current_user),
241
+ db: Session = Depends(get_db),
242
+ ):
243
+ """Feature 1 & 3 – Get current trust score + decay-adjusted value + history."""
244
+ session = _get_or_create_demo_session(current_user, db)
245
+ tsm = TrustScoreManager(db)
246
+
247
+ score, decay_delta = tsm.apply_decay(session)
248
+ if abs(decay_delta) > 0.1:
249
+ tsm.update(session, decay_delta, "decay",
250
+ f"Time-based decay: {decay_delta:.2f} pts", {})
251
+ score = tsm.get(session)
252
+
253
+ history = tsm.get_history(session.id)
254
+ mce = MicroChallengeEngine()
255
+
256
+ return {
257
+ "trust_score": round(score, 2),
258
+ "label": TrustScoreManager.label(score),
259
+ "color": TrustScoreManager.color(score),
260
+ "session_id": session.id,
261
+ "history": history,
262
+ "micro_challenge_recommended": mce.should_challenge(score),
263
+ "thresholds": {
264
+ "trusted": 80,
265
+ "watchful": 60,
266
+ "elevated": 40,
267
+ "high_risk": 20,
268
+ },
269
+ }
270
+
271
+
272
+ @router.post("/continuous-verify")
273
+ async def continuous_verify(
274
+ request: Request,
275
+ current_user: User = Depends(get_current_user),
276
+ db: Session = Depends(get_db),
277
+ ):
278
+ """
279
+ Feature 1 – Continuous Verification cycle.
280
+ Re-evaluates the session context; applies decay; returns updated trust.
281
+ """
282
+ session = _get_or_create_demo_session(current_user, db)
283
+ tsm = TrustScoreManager(db)
284
+
285
+ # Apply time-based decay since last check
286
+ score, decay_delta = tsm.apply_decay(session)
287
+ if abs(decay_delta) > 0.05:
288
+ tsm.update(session, decay_delta, "decay",
289
+ f"Continuous verify — inactivity decay {decay_delta:.2f} pts", {})
290
+ score = tsm.get(session)
291
+
292
+ mce = MicroChallengeEngine()
293
+ return {
294
+ "verified": True,
295
+ "user_email": current_user.email,
296
+ "session_id": session.id,
297
+ "trust_score": round(score, 2),
298
+ "label": TrustScoreManager.label(score),
299
+ "color": TrustScoreManager.color(score),
300
+ "decay_applied": round(decay_delta, 3),
301
+ "micro_challenge_recommended": mce.should_challenge(score),
302
+ "verified_at": datetime.utcnow().isoformat(),
303
+ }
304
+
305
+
306
+ @router.get("/explain")
307
+ async def explain_session(
308
+ current_user: User = Depends(get_current_user),
309
+ db: Session = Depends(get_db),
310
+ ):
311
+ """
312
+ Feature 5 – Explainability: return history-driven trust explanation
313
+ and the last known login risk breakdown.
314
+ """
315
+ session = _get_or_create_demo_session(current_user, db)
316
+ tsm = TrustScoreManager(db)
317
+ score = tsm.get(session)
318
+ history = tsm.get_history(session.id, limit=10)
319
+
320
+ explainer = RiskExplainer()
321
+ trust_events_explained = [
322
+ {
323
+ "at": e["at"],
324
+ "score": e["score"],
325
+ "delta": e["delta"],
326
+ "explanation": explainer.explain_trust_event(e["event_type"], e["delta"], {}),
327
+ }
328
+ for e in history
329
+ ]
330
+
331
+ return {
332
+ "user_email": current_user.email,
333
+ "current_trust": round(score, 2),
334
+ "trust_label": TrustScoreManager.label(score),
335
+ "recent_events": trust_events_explained,
336
+ "summary": (
337
+ f"Trust score is {score:.0f}/100 ({TrustScoreManager.label(score)}). "
338
+ f"{len(history)} events recorded this session."
339
+ ),
340
+ }
341
+
342
+
343
+ @router.post("/micro-challenge/generate")
344
+ async def generate_challenge(
345
+ current_user: User = Depends(get_current_user),
346
+ db: Session = Depends(get_db),
347
+ ):
348
+ """Feature 4 – Generate an inline math micro-challenge."""
349
+ session = _get_or_create_demo_session(current_user, db)
350
+ tsm = TrustScoreManager(db)
351
+ score = tsm.get(session)
352
+
353
+ mce = MicroChallengeEngine()
354
+ challenge = mce.generate()
355
+ return {
356
+ "current_trust": round(score, 2),
357
+ "challenge_needed": mce.should_challenge(score),
358
+ "challenge": challenge,
359
+ }
360
+
361
+
362
+ @router.post("/micro-challenge/verify")
363
+ async def verify_challenge(
364
+ body: ChallengeVerifyInput,
365
+ current_user: User = Depends(get_current_user),
366
+ db: Session = Depends(get_db),
367
+ ):
368
+ """Feature 4 – Verify the answer to a micro-challenge and update trust."""
369
+ session = _get_or_create_demo_session(current_user, db)
370
+ mce = MicroChallengeEngine()
371
+ result = mce.verify(body.challenge_id, body.response)
372
+
373
+ tsm = TrustScoreManager(db)
374
+ event = "micro_challenge_pass" if result["correct"] else "micro_challenge_fail"
375
+ new_trust = tsm.update(session, result["trust_delta"], event, result["reason"], {})
376
+
377
+ return {
378
+ **result,
379
+ "new_trust": round(new_trust, 2),
380
+ "trust_label": TrustScoreManager.label(new_trust),
381
+ "trust_color": TrustScoreManager.color(new_trust),
382
+ }
383
+
384
+
385
+ @router.post("/simulate-trust-drop")
386
+ async def simulate_trust_drop(
387
+ body: SimulateTrustDropInput,
388
+ current_user: User = Depends(get_current_user),
389
+ db: Session = Depends(get_db),
390
+ ):
391
+ """Demo helper – force trust to a specific score to trigger micro-challenges."""
392
+ session = _get_or_create_demo_session(current_user, db)
393
+ tsm = TrustScoreManager(db)
394
+ current = tsm.get(session)
395
+ delta = body.target_score - current
396
+ new_score = tsm.update(session, delta, "context_change", body.reason, {})
397
+
398
+ mce = MicroChallengeEngine()
399
+ return {
400
+ "previous_trust": round(current, 2),
401
+ "new_trust": round(new_score, 2),
402
+ "delta": round(delta, 2),
403
+ "trust_label": TrustScoreManager.label(new_score),
404
+ "trust_color": TrustScoreManager.color(new_score),
405
+ "challenge_recommended": mce.should_challenge(new_score),
406
+ }
407
+
408
+
409
+ # ═══════════════════════════════════════════════════════════════════════════
410
+ # Demo / public endpoints (no auth required — for UI demos)
411
+ # ═══════════════════════════════════════════════════════════════════════════
412
+
413
+ @router.post("/demo/impossible-travel")
414
+ async def demo_impossible_travel(body: TravelCheckInput):
415
+ """
416
+ Feature 7 – Impossible Travel demo (no auth required).
417
+ Calculates haversine distance + velocity between two cities.
418
+ """
419
+ result = _resolve_demo_travel(body)
420
+ return result
421
+
422
+
423
+ @router.post("/demo/anomaly-score")
424
+ async def demo_anomaly_score(features: AnomalyScoreInput):
425
+ """
426
+ Feature 6 – AI Anomaly Scoring demo (no auth required).
427
+ Returns statistical isolation-forest anomaly score for the given feature vector.
428
+ """
429
+ detector = StatisticalAnomalyDetector()
430
+ return detector.score(features.model_dump())
431
+
432
+
433
+ @router.post("/demo/explain")
434
+ async def demo_explain(body: ExplainInput):
435
+ """
436
+ Feature 5 – Explainability demo (no auth required).
437
+ Submit raw factor scores and get a structured audit report.
438
+ """
439
+ explainer = RiskExplainer()
440
+ return explainer.explain_login(
441
+ risk_factors={
442
+ "location": body.location_score,
443
+ "device": body.device_score,
444
+ "time": body.time_score,
445
+ "velocity": body.velocity_score,
446
+ "behavior": body.behavior_score,
447
+ },
448
+ risk_level=body.risk_level,
449
+ security_level=body.security_level,
450
+ )
451
+
452
+
453
+ @router.get("/demo/city-list")
454
+ async def demo_city_list():
455
+ """Return list of cities known to the impossible travel detector."""
456
+ return {
457
+ "cities": [
458
+ {"name": k.title(), "lat": v[0], "lon": v[1]}
459
+ for k, v in sorted(CITY_COORDS.items())
460
+ ]
461
+ }
openapi_temp.json DELETED
@@ -1 +0,0 @@
1
- {"openapi":"3.1.0","info":{"title":"AdaptiveAuth Framework Live Test Application","description":"Interactive demonstration of all AdaptiveAuth features","version":"1.0.0"},"paths":{"/auth/auth/register":{"post":{"tags":["Authentication"],"summary":"Register","description":"Register a new user.","operationId":"register_auth_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/login":{"post":{"tags":["Authentication"],"summary":"Login","description":"Standard OAuth2 login endpoint.\nFor risk-based login, use /auth/adaptive-login.","operationId":"login_auth_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/adaptive-login":{"post":{"tags":["Authentication"],"summary":"Adaptive Login","description":"Risk-based adaptive login.\nReturns detailed risk assessment and may require step-up authentication.","operationId":"adaptive_login_auth_auth_adaptive_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/step-up":{"post":{"tags":["Authentication"],"summary":"Step Up Verification","description":"Complete step-up authentication challenge.","operationId":"step_up_verification_auth_auth_step_up_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/login-otp":{"post":{"tags":["Authentication"],"summary":"Login With Otp","description":"Login using TOTP code only (for 2FA-enabled users).","operationId":"login_with_otp_auth_auth_login_otp_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginOTP"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/logout":{"post":{"tags":["Authentication"],"summary":"Logout","description":"Logout current user.","operationId":"logout_auth_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/auth/forgot-password":{"post":{"tags":["Authentication"],"summary":"Forgot Password","description":"Request password reset email.","operationId":"forgot_password_auth_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/reset-password":{"post":{"tags":["Authentication"],"summary":"Reset Password","description":"Reset password with token.","operationId":"reset_password_auth_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirm"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/enable-2fa":{"post":{"tags":["Authentication"],"summary":"Enable 2Fa","description":"Enable 2FA for current user.","operationId":"enable_2fa_auth_auth_enable_2fa_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Enable2FAResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/auth/verify-2fa":{"post":{"tags":["Authentication"],"summary":"Verify 2Fa","description":"Verify and activate 2FA.","operationId":"verify_2fa_auth_auth_verify_2fa_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verify2FARequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/auth/disable-2fa":{"post":{"tags":["Authentication"],"summary":"Disable 2Fa","description":"Disable 2FA for current user.","operationId":"disable_2fa_auth_auth_disable_2fa_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"password","in":"query","required":true,"schema":{"type":"string","title":"Password"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user/profile":{"get":{"tags":["User"],"summary":"Get Profile","description":"Get current user's profile.","operationId":"get_profile_auth_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["User"],"summary":"Update Profile","description":"Update current user's profile.","operationId":"update_profile_auth_user_profile_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/security":{"get":{"tags":["User"],"summary":"Get Security Settings","description":"Get user's security settings.","operationId":"get_security_settings_auth_user_security_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSecuritySettings"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/change-password":{"post":{"tags":["User"],"summary":"Change Password","description":"Change user's password.","operationId":"change_password_auth_user_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChange"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/devices":{"get":{"tags":["User"],"summary":"Get Devices","description":"Get user's known devices.","operationId":"get_devices_auth_user_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/devices/{device_id}":{"delete":{"tags":["User"],"summary":"Remove Device","description":"Remove a known device.","operationId":"remove_device_auth_user_devices__device_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user/sessions":{"get":{"tags":["User"],"summary":"Get Sessions","description":"Get user's active sessions.","operationId":"get_sessions_auth_user_sessions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/sessions/revoke":{"post":{"tags":["User"],"summary":"Revoke Sessions","description":"Revoke user sessions.","operationId":"revoke_sessions_auth_user_sessions_revoke_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionRevokeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/risk-profile":{"get":{"tags":["User"],"summary":"Get Risk Profile","description":"Get user's risk profile summary.","operationId":"get_risk_profile_auth_user_risk_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","description":"List all users (admin only).","operationId":"list_users_auth_admin_users_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}},{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"}},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/users/{user_id}":{"get":{"tags":["Admin"],"summary":"Get User","description":"Get user details (admin only).","operationId":"get_user_auth_admin_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/users/{user_id}/block":{"post":{"tags":["Admin"],"summary":"Block User","description":"Block a user (admin only).","operationId":"block_user_auth_admin_users__user_id__block_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}},{"name":"duration_hours","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/users/{user_id}/unblock":{"post":{"tags":["Admin"],"summary":"Unblock User","description":"Unblock a user (admin only).","operationId":"unblock_user_auth_admin_users__user_id__unblock_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/sessions":{"get":{"tags":["Admin"],"summary":"List Sessions","description":"List all active sessions (admin only).","operationId":"list_sessions_auth_admin_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"status_filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Filter"}},{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/sessions/{session_id}/revoke":{"post":{"tags":["Admin"],"summary":"Revoke Session","description":"Revoke a specific session (admin only).","operationId":"revoke_session_auth_admin_sessions__session_id__revoke_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"integer","title":"Session Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/risk-events":{"get":{"tags":["Admin"],"summary":"List Risk Events","description":"List risk events (admin only).","operationId":"list_risk_events_auth_admin_risk_events_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Type"}},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskEventList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/anomalies":{"get":{"tags":["Admin"],"summary":"List Anomalies","description":"List detected anomaly patterns (admin only).","operationId":"list_anomalies_auth_admin_anomalies_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnomalyListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/anomalies/{anomaly_id}/resolve":{"post":{"tags":["Admin"],"summary":"Resolve Anomaly","description":"Resolve an anomaly pattern (admin only).","operationId":"resolve_anomaly_auth_admin_anomalies__anomaly_id__resolve_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"anomaly_id","in":"path","required":true,"schema":{"type":"integer","title":"Anomaly Id"}},{"name":"false_positive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"False Positive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/statistics":{"get":{"tags":["Admin"],"summary":"Get Statistics","description":"Get admin dashboard statistics.","operationId":"get_statistics_auth_admin_statistics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStatistics"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/admin/risk-statistics":{"get":{"tags":["Admin"],"summary":"Get Risk Statistics","description":"Get risk statistics for a period.","operationId":"get_risk_statistics_auth_admin_risk_statistics_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^(day|week|month)$","default":"day","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskStatistics"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/overview":{"get":{"tags":["Risk Dashboard"],"summary":"Get Risk Overview","description":"Get risk dashboard overview.","operationId":"get_risk_overview_auth_risk_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskDashboardOverview"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/risk/assess":{"post":{"tags":["Risk Dashboard"],"summary":"Assess Risk","description":"Manually assess risk for a context or user.","operationId":"assess_risk_auth_risk_assess_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/profile/{user_id}":{"get":{"tags":["Risk Dashboard"],"summary":"Get User Risk Profile","description":"Get detailed risk profile for a user.","operationId":"get_user_risk_profile_auth_risk_profile__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/active-sessions":{"get":{"tags":["Risk Dashboard"],"summary":"Get High Risk Sessions","description":"Get sessions with elevated risk levels.","operationId":"get_high_risk_sessions_auth_risk_active_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"min_risk_level","in":"query","required":false,"schema":{"type":"string","pattern":"^(low|medium|high|critical)$","default":"medium","title":"Min Risk Level"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/login-patterns":{"get":{"tags":["Risk Dashboard"],"summary":"Get Login Patterns","description":"Get login patterns analysis.","operationId":"get_login_patterns_auth_risk_login_patterns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/suspicious-ips":{"get":{"tags":["Risk Dashboard"],"summary":"Get Suspicious Ips","description":"Get IPs with suspicious activity.","operationId":"get_suspicious_ips_auth_risk_suspicious_ips_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/risk/block-ip":{"post":{"tags":["Risk Dashboard"],"summary":"Block Ip","description":"Block an IP address (creates anomaly pattern).","operationId":"block_ip_auth_risk_block_ip_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"ip_address","in":"query","required":true,"schema":{"type":"string","title":"Ip Address"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Suspicious activity","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/adaptive/assess":{"post":{"tags":["Adaptive Authentication"],"summary":"Assess Current Risk","description":"Assess current risk level for authenticated user.","operationId":"assess_current_risk_auth_adaptive_assess_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskAssessmentResult"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/verify-session":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Session","description":"Verify current session is still valid and not compromised.\nUse this periodically during sensitive operations.","operationId":"verify_session_auth_adaptive_verify_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/challenge":{"post":{"tags":["Adaptive Authentication"],"summary":"Request Challenge","description":"Request a new authentication challenge for step-up auth.","operationId":"request_challenge_auth_adaptive_challenge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/verify":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Challenge","description":"Verify a step-up authentication challenge.","operationId":"verify_challenge_auth_adaptive_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/security-status":{"get":{"tags":["Adaptive Authentication"],"summary":"Get Security Status","description":"Get current security status for the user.","operationId":"get_security_status_auth_adaptive_security_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/trust-device":{"post":{"tags":["Adaptive Authentication"],"summary":"Trust Current Device","description":"Mark current device as trusted.","operationId":"trust_current_device_auth_adaptive_trust_device_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/trust-device/{device_index}":{"delete":{"tags":["Adaptive Authentication"],"summary":"Remove Trusted Device","description":"Remove a device from trusted devices.","operationId":"remove_trusted_device_auth_adaptive_trust_device__device_index__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_index","in":"path","required":true,"schema":{"type":"integer","title":"Device Index"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test-interface":{"get":{"summary":"Test Interface","description":"Serve the test interface","operationId":"test_interface_test_interface_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/protected":{"get":{"summary":"Protected Endpoint","description":"Protected endpoint that requires authentication","operationId":"protected_endpoint_protected_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin-only":{"get":{"summary":"Admin Only Endpoint","description":"Admin-only endpoint that requires admin role","operationId":"admin_only_endpoint_admin_only_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/demo/features":{"get":{"summary":"Demo Features","description":"Demonstrate all framework features","operationId":"demo_features_demo_features_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test/register":{"post":{"summary":"Test Register","description":"Test endpoint for user registration","operationId":"test_register_test_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/login":{"post":{"summary":"Test Login","description":"Test endpoint for user login","operationId":"test_login_test_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLogin"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/create-user":{"post":{"summary":"Create Test User","description":"Create a test user programmatically","operationId":"create_test_user_test_create_user_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_create_test_user_test_create_user_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdaptiveLoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"},"device_fingerprint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Fingerprint"},"remember_device":{"type":"boolean","title":"Remember Device","default":false}},"type":"object","required":["email","password"],"title":"AdaptiveLoginRequest","description":"Adaptive login request with context."},"AdaptiveLoginResponse":{"properties":{"status":{"type":"string","title":"Status"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","title":"Security Level"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"challenge_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Type"},"challenge_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Id"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["status","risk_level","security_level"],"title":"AdaptiveLoginResponse","description":"Adaptive login response."},"AdminStatistics":{"properties":{"total_users":{"type":"integer","title":"Total Users"},"active_users":{"type":"integer","title":"Active Users"},"blocked_users":{"type":"integer","title":"Blocked Users"},"active_sessions":{"type":"integer","title":"Active Sessions"},"high_risk_events_today":{"type":"integer","title":"High Risk Events Today"},"failed_logins_today":{"type":"integer","title":"Failed Logins Today"},"new_users_today":{"type":"integer","title":"New Users Today"}},"type":"object","required":["total_users","active_users","blocked_users","active_sessions","high_risk_events_today","failed_logins_today","new_users_today"],"title":"AdminStatistics","description":"Admin dashboard statistics."},"AdminUserList":{"properties":{"users":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Users"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["users","total","page","page_size"],"title":"AdminUserList","description":"Admin user list response."},"AnomalyListResponse":{"properties":{"anomalies":{"items":{"$ref":"#/components/schemas/AnomalyPatternResponse"},"type":"array","title":"Anomalies"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["anomalies","total"],"title":"AnomalyListResponse","description":"List of anomaly patterns."},"AnomalyPatternResponse":{"properties":{"id":{"type":"integer","title":"Id"},"pattern_type":{"type":"string","title":"Pattern Type"},"severity":{"type":"string","title":"Severity"},"confidence":{"type":"number","title":"Confidence"},"is_active":{"type":"boolean","title":"Is Active"},"first_detected":{"type":"string","format":"date-time","title":"First Detected"},"last_detected":{"type":"string","format":"date-time","title":"Last Detected"},"pattern_data":{"additionalProperties":true,"type":"object","title":"Pattern Data"}},"type":"object","required":["id","pattern_type","severity","confidence","is_active","first_detected","last_detected","pattern_data"],"title":"AnomalyPatternResponse","description":"Detected anomaly pattern."},"Body_create_test_user_test_create_user_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"full_name":{"type":"string","title":"Full Name"},"role":{"type":"string","title":"Role","default":"user"}},"type":"object","required":["email","password"],"title":"Body_create_test_user_test_create_user_post"},"Body_login_auth_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_auth_login_post"},"ChallengeRequest":{"properties":{"challenge_type":{"type":"string","pattern":"^(otp|email|sms)$","title":"Challenge Type"},"session_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Id"}},"type":"object","required":["challenge_type"],"title":"ChallengeRequest","description":"Request a new challenge."},"ChallengeResponse":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"challenge_type":{"type":"string","title":"Challenge Type"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"message":{"type":"string","title":"Message"}},"type":"object","required":["challenge_id","challenge_type","expires_at","message"],"title":"ChallengeResponse","description":"Challenge created response."},"DeviceInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"browser":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser"},"os":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Os"},"first_seen":{"type":"string","format":"date-time","title":"First Seen"},"last_seen":{"type":"string","format":"date-time","title":"Last Seen"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","name","browser","os","first_seen","last_seen"],"title":"DeviceInfo","description":"Known device information."},"DeviceListResponse":{"properties":{"devices":{"items":{"$ref":"#/components/schemas/DeviceInfo"},"type":"array","title":"Devices"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["devices","total"],"title":"DeviceListResponse","description":"List of known devices."},"Enable2FAResponse":{"properties":{"secret":{"type":"string","title":"Secret"},"qr_code":{"type":"string","title":"Qr Code"},"backup_codes":{"items":{"type":"string"},"type":"array","title":"Backup Codes"}},"type":"object","required":["secret","qr_code","backup_codes"],"title":"Enable2FAResponse","description":"Enable 2FA response with QR code."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoginOTP":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["email","otp"],"title":"LoginOTP","description":"Login with TOTP code."},"PasswordChange":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["current_password","new_password","confirm_password"],"title":"PasswordChange","description":"Change password (authenticated)."},"PasswordResetConfirm":{"properties":{"reset_token":{"type":"string","title":"Reset Token"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["reset_token","new_password","confirm_password"],"title":"PasswordResetConfirm","description":"Confirm password reset."},"PasswordResetRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"PasswordResetRequest","description":"Request password reset."},"RiskAssessmentResult":{"properties":{"risk_score":{"type":"number","maximum":100.0,"minimum":0.0,"title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","maximum":4.0,"minimum":0.0,"title":"Security Level"},"risk_factors":{"additionalProperties":{"type":"number"},"type":"object","title":"Risk Factors"},"required_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Required Action"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["risk_score","risk_level","security_level","risk_factors"],"title":"RiskAssessmentResult","description":"Risk assessment result."},"RiskDashboardOverview":{"properties":{"total_risk_events":{"type":"integer","title":"Total Risk Events"},"high_risk_events":{"type":"integer","title":"High Risk Events"},"active_anomalies":{"type":"integer","title":"Active Anomalies"},"blocked_users":{"type":"integer","title":"Blocked Users"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_trend":{"type":"string","title":"Risk Trend"}},"type":"object","required":["total_risk_events","high_risk_events","active_anomalies","blocked_users","average_risk_score","risk_trend"],"title":"RiskDashboardOverview","description":"Risk dashboard overview."},"RiskEventList":{"properties":{"events":{"items":{"$ref":"#/components/schemas/RiskEventResponse"},"type":"array","title":"Events"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["events","total","page","page_size"],"title":"RiskEventList","description":"List of risk events."},"RiskEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"risk_score":{"type":"number","title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"risk_factors":{"additionalProperties":true,"type":"object","title":"Risk Factors"},"action_taken":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Taken"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"resolved":{"type":"boolean","title":"Resolved"}},"type":"object","required":["id","event_type","risk_score","risk_level","ip_address","risk_factors","action_taken","created_at","resolved"],"title":"RiskEventResponse","description":"Risk event information."},"RiskStatistics":{"properties":{"period":{"type":"string","title":"Period"},"total_logins":{"type":"integer","title":"Total Logins"},"successful_logins":{"type":"integer","title":"Successful Logins"},"failed_logins":{"type":"integer","title":"Failed Logins"},"blocked_attempts":{"type":"integer","title":"Blocked Attempts"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_distribution":{"additionalProperties":{"type":"integer"},"type":"object","title":"Risk Distribution"}},"type":"object","required":["period","total_logins","successful_logins","failed_logins","blocked_attempts","average_risk_score","risk_distribution"],"title":"RiskStatistics","description":"Risk statistics."},"SessionInfo":{"properties":{"id":{"type":"integer","title":"Id"},"ip_address":{"type":"string","title":"Ip Address"},"user_agent":{"type":"string","title":"User Agent"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"risk_level":{"type":"string","title":"Risk Level"},"status":{"type":"string","title":"Status"},"last_activity":{"type":"string","format":"date-time","title":"Last Activity"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","ip_address","user_agent","country","city","risk_level","status","last_activity","created_at"],"title":"SessionInfo","description":"Active session information."},"SessionListResponse":{"properties":{"sessions":{"items":{"$ref":"#/components/schemas/SessionInfo"},"type":"array","title":"Sessions"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["sessions","total"],"title":"SessionListResponse","description":"List of user sessions."},"SessionRevokeRequest":{"properties":{"session_ids":{"items":{"type":"integer"},"type":"array","title":"Session Ids"},"revoke_all":{"type":"boolean","title":"Revoke All","default":false}},"type":"object","required":["session_ids"],"title":"SessionRevokeRequest","description":"Request to revoke session(s)."},"StepUpRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"verification_code":{"type":"string","title":"Verification Code"}},"type":"object","required":["challenge_id","verification_code"],"title":"StepUpRequest","description":"Step-up authentication request."},"StepUpResponse":{"properties":{"status":{"type":"string","title":"Status"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["status"],"title":"StepUpResponse","description":"Step-up authentication response."},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"expires_in":{"type":"integer","title":"Expires In"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["access_token","expires_in"],"title":"TokenResponse","description":"JWT token response."},"UserLogin":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"UserLogin","description":"Standard login request."},"UserRegister":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":8,"title":"Password"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"}},"type":"object","required":["email","password"],"title":"UserRegister","description":"User registration request."},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"role":{"type":"string","title":"Role"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","full_name","role","is_active","is_verified","tfa_enabled","created_at"],"title":"UserResponse","description":"User information response."},"UserSecuritySettings":{"properties":{"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"last_password_change":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Password Change"},"active_sessions":{"type":"integer","title":"Active Sessions"},"known_devices":{"type":"integer","title":"Known Devices"},"recent_login_attempts":{"type":"integer","title":"Recent Login Attempts"}},"type":"object","required":["tfa_enabled","last_password_change","active_sessions","known_devices","recent_login_attempts"],"title":"UserSecuritySettings","description":"User security settings response."},"UserUpdate":{"properties":{"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"}},"type":"object","title":"UserUpdate","description":"Update user information."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Verify2FARequest":{"properties":{"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["otp"],"title":"Verify2FARequest","description":"Verify 2FA setup."},"VerifyChallengeRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"code":{"type":"string","title":"Code"}},"type":"object","required":["challenge_id","code"],"title":"VerifyChallengeRequest","description":"Verify challenge code."}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}}
 
 
openapi_test.json DELETED
@@ -1 +0,0 @@
1
- {"openapi":"3.1.0","info":{"title":"AdaptiveAuth Framework Live Test Application","description":"Interactive demonstration of all AdaptiveAuth features","version":"1.0.0"},"paths":{"/auth/register":{"post":{"tags":["Authentication"],"summary":"Register","description":"Register a new user.","operationId":"register_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login":{"post":{"tags":["Authentication"],"summary":"Login","description":"Standard OAuth2 login endpoint.\nFor risk-based login, use /auth/adaptive-login.","operationId":"login_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/adaptive-login":{"post":{"tags":["Authentication"],"summary":"Adaptive Login","description":"Risk-based adaptive login.\nReturns detailed risk assessment and may require step-up authentication.","operationId":"adaptive_login_auth_adaptive_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/step-up":{"post":{"tags":["Authentication"],"summary":"Step Up Verification","description":"Complete step-up authentication challenge.","operationId":"step_up_verification_auth_step_up_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login-otp":{"post":{"tags":["Authentication"],"summary":"Login With Otp","description":"Login using TOTP code only (for 2FA-enabled users).","operationId":"login_with_otp_auth_login_otp_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginOTP"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/logout":{"post":{"tags":["Authentication"],"summary":"Logout","description":"Logout current user.","operationId":"logout_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/forgot-password":{"post":{"tags":["Authentication"],"summary":"Forgot Password","description":"Request password reset email.","operationId":"forgot_password_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/reset-password":{"post":{"tags":["Authentication"],"summary":"Reset Password","description":"Reset password with token.","operationId":"reset_password_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirm"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/enable-2fa":{"post":{"tags":["Authentication"],"summary":"Enable 2Fa","description":"Enable 2FA for current user.","operationId":"enable_2fa_auth_enable_2fa_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Enable2FAResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/verify-2fa":{"post":{"tags":["Authentication"],"summary":"Verify 2Fa","description":"Verify and activate 2FA.","operationId":"verify_2fa_auth_verify_2fa_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verify2FARequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/disable-2fa":{"post":{"tags":["Authentication"],"summary":"Disable 2Fa","description":"Disable 2FA for current user.","operationId":"disable_2fa_auth_disable_2fa_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"password","in":"query","required":true,"schema":{"type":"string","title":"Password"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/profile":{"get":{"tags":["User"],"summary":"Get Profile","description":"Get current user's profile.","operationId":"get_profile_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["User"],"summary":"Update Profile","description":"Update current user's profile.","operationId":"update_profile_user_profile_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/security":{"get":{"tags":["User"],"summary":"Get Security Settings","description":"Get user's security settings.","operationId":"get_security_settings_user_security_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSecuritySettings"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/change-password":{"post":{"tags":["User"],"summary":"Change Password","description":"Change user's password.","operationId":"change_password_user_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChange"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/devices":{"get":{"tags":["User"],"summary":"Get Devices","description":"Get user's known devices.","operationId":"get_devices_user_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/devices/{device_id}":{"delete":{"tags":["User"],"summary":"Remove Device","description":"Remove a known device.","operationId":"remove_device_user_devices__device_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/sessions":{"get":{"tags":["User"],"summary":"Get Sessions","description":"Get user's active sessions.","operationId":"get_sessions_user_sessions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/sessions/revoke":{"post":{"tags":["User"],"summary":"Revoke Sessions","description":"Revoke user sessions.","operationId":"revoke_sessions_user_sessions_revoke_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionRevokeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/risk-profile":{"get":{"tags":["User"],"summary":"Get Risk Profile","description":"Get user's risk profile summary.","operationId":"get_risk_profile_user_risk_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","description":"List all users (admin only).","operationId":"list_users_admin_users_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}},{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"}},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}":{"get":{"tags":["Admin"],"summary":"Get User","description":"Get user details (admin only).","operationId":"get_user_admin_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/block":{"post":{"tags":["Admin"],"summary":"Block User","description":"Block a user (admin only).","operationId":"block_user_admin_users__user_id__block_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}},{"name":"duration_hours","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/unblock":{"post":{"tags":["Admin"],"summary":"Unblock User","description":"Unblock a user (admin only).","operationId":"unblock_user_admin_users__user_id__unblock_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions":{"get":{"tags":["Admin"],"summary":"List Sessions","description":"List all active sessions (admin only).","operationId":"list_sessions_admin_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"status_filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Filter"}},{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions/{session_id}/revoke":{"post":{"tags":["Admin"],"summary":"Revoke Session","description":"Revoke a specific session (admin only).","operationId":"revoke_session_admin_sessions__session_id__revoke_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"integer","title":"Session Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/risk-events":{"get":{"tags":["Admin"],"summary":"List Risk Events","description":"List risk events (admin only).","operationId":"list_risk_events_admin_risk_events_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Type"}},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskEventList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/anomalies":{"get":{"tags":["Admin"],"summary":"List Anomalies","description":"List detected anomaly patterns (admin only).","operationId":"list_anomalies_admin_anomalies_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnomalyListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/anomalies/{anomaly_id}/resolve":{"post":{"tags":["Admin"],"summary":"Resolve Anomaly","description":"Resolve an anomaly pattern (admin only).","operationId":"resolve_anomaly_admin_anomalies__anomaly_id__resolve_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"anomaly_id","in":"path","required":true,"schema":{"type":"integer","title":"Anomaly Id"}},{"name":"false_positive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"False Positive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/statistics":{"get":{"tags":["Admin"],"summary":"Get Statistics","description":"Get admin dashboard statistics.","operationId":"get_statistics_admin_statistics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStatistics"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/risk-statistics":{"get":{"tags":["Admin"],"summary":"Get Risk Statistics","description":"Get risk statistics for a period.","operationId":"get_risk_statistics_admin_risk_statistics_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^(day|week|month)$","default":"day","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskStatistics"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/overview":{"get":{"tags":["Risk Dashboard"],"summary":"Get Risk Overview","description":"Get risk dashboard overview.","operationId":"get_risk_overview_risk_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskDashboardOverview"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/risk/assess":{"post":{"tags":["Risk Dashboard"],"summary":"Assess Risk","description":"Manually assess risk for a context or user.","operationId":"assess_risk_risk_assess_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/profile/{user_id}":{"get":{"tags":["Risk Dashboard"],"summary":"Get User Risk Profile","description":"Get detailed risk profile for a user.","operationId":"get_user_risk_profile_risk_profile__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/active-sessions":{"get":{"tags":["Risk Dashboard"],"summary":"Get High Risk Sessions","description":"Get sessions with elevated risk levels.","operationId":"get_high_risk_sessions_risk_active_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"min_risk_level","in":"query","required":false,"schema":{"type":"string","pattern":"^(low|medium|high|critical)$","default":"medium","title":"Min Risk Level"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/login-patterns":{"get":{"tags":["Risk Dashboard"],"summary":"Get Login Patterns","description":"Get login patterns analysis.","operationId":"get_login_patterns_risk_login_patterns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/suspicious-ips":{"get":{"tags":["Risk Dashboard"],"summary":"Get Suspicious Ips","description":"Get IPs with suspicious activity.","operationId":"get_suspicious_ips_risk_suspicious_ips_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/risk/block-ip":{"post":{"tags":["Risk Dashboard"],"summary":"Block Ip","description":"Block an IP address (creates anomaly pattern).","operationId":"block_ip_risk_block_ip_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"ip_address","in":"query","required":true,"schema":{"type":"string","title":"Ip Address"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Suspicious activity","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/adaptive/assess":{"post":{"tags":["Adaptive Authentication"],"summary":"Assess Current Risk","description":"Assess current risk level for authenticated user.","operationId":"assess_current_risk_adaptive_assess_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskAssessmentResult"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/verify-session":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Session","description":"Verify current session is still valid and not compromised.\nUse this periodically during sensitive operations.","operationId":"verify_session_adaptive_verify_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/challenge":{"post":{"tags":["Adaptive Authentication"],"summary":"Request Challenge","description":"Request a new authentication challenge for step-up auth.","operationId":"request_challenge_adaptive_challenge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/verify":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Challenge","description":"Verify a step-up authentication challenge.","operationId":"verify_challenge_adaptive_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/security-status":{"get":{"tags":["Adaptive Authentication"],"summary":"Get Security Status","description":"Get current security status for the user.","operationId":"get_security_status_adaptive_security_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/trust-device":{"post":{"tags":["Adaptive Authentication"],"summary":"Trust Current Device","description":"Mark current device as trusted.","operationId":"trust_current_device_adaptive_trust_device_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/trust-device/{device_index}":{"delete":{"tags":["Adaptive Authentication"],"summary":"Remove Trusted Device","description":"Remove a device from trusted devices.","operationId":"remove_trusted_device_adaptive_trust_device__device_index__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_index","in":"path","required":true,"schema":{"type":"integer","title":"Device Index"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test-interface":{"get":{"summary":"Test Interface","description":"Serve the test interface","operationId":"test_interface_test_interface_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/protected":{"get":{"summary":"Protected Endpoint","description":"Protected endpoint that requires authentication","operationId":"protected_endpoint_protected_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin-only":{"get":{"summary":"Admin Only Endpoint","description":"Admin-only endpoint that requires admin role","operationId":"admin_only_endpoint_admin_only_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/demo/features":{"get":{"summary":"Demo Features","description":"Demonstrate all framework features","operationId":"demo_features_demo_features_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test/register":{"post":{"summary":"Test Register","description":"Test endpoint for user registration","operationId":"test_register_test_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/login":{"post":{"summary":"Test Login","description":"Test endpoint for user login","operationId":"test_login_test_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLogin"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/create-user":{"post":{"summary":"Create Test User","description":"Create a test user programmatically","operationId":"create_test_user_test_create_user_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_create_test_user_test_create_user_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdaptiveLoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"},"device_fingerprint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Fingerprint"},"remember_device":{"type":"boolean","title":"Remember Device","default":false}},"type":"object","required":["email","password"],"title":"AdaptiveLoginRequest","description":"Adaptive login request with context."},"AdaptiveLoginResponse":{"properties":{"status":{"type":"string","title":"Status"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","title":"Security Level"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"challenge_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Type"},"challenge_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Id"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["status","risk_level","security_level"],"title":"AdaptiveLoginResponse","description":"Adaptive login response."},"AdminStatistics":{"properties":{"total_users":{"type":"integer","title":"Total Users"},"active_users":{"type":"integer","title":"Active Users"},"blocked_users":{"type":"integer","title":"Blocked Users"},"active_sessions":{"type":"integer","title":"Active Sessions"},"high_risk_events_today":{"type":"integer","title":"High Risk Events Today"},"failed_logins_today":{"type":"integer","title":"Failed Logins Today"},"new_users_today":{"type":"integer","title":"New Users Today"}},"type":"object","required":["total_users","active_users","blocked_users","active_sessions","high_risk_events_today","failed_logins_today","new_users_today"],"title":"AdminStatistics","description":"Admin dashboard statistics."},"AdminUserList":{"properties":{"users":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Users"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["users","total","page","page_size"],"title":"AdminUserList","description":"Admin user list response."},"AnomalyListResponse":{"properties":{"anomalies":{"items":{"$ref":"#/components/schemas/AnomalyPatternResponse"},"type":"array","title":"Anomalies"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["anomalies","total"],"title":"AnomalyListResponse","description":"List of anomaly patterns."},"AnomalyPatternResponse":{"properties":{"id":{"type":"integer","title":"Id"},"pattern_type":{"type":"string","title":"Pattern Type"},"severity":{"type":"string","title":"Severity"},"confidence":{"type":"number","title":"Confidence"},"is_active":{"type":"boolean","title":"Is Active"},"first_detected":{"type":"string","format":"date-time","title":"First Detected"},"last_detected":{"type":"string","format":"date-time","title":"Last Detected"},"pattern_data":{"additionalProperties":true,"type":"object","title":"Pattern Data"}},"type":"object","required":["id","pattern_type","severity","confidence","is_active","first_detected","last_detected","pattern_data"],"title":"AnomalyPatternResponse","description":"Detected anomaly pattern."},"Body_create_test_user_test_create_user_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"full_name":{"type":"string","title":"Full Name"},"role":{"type":"string","title":"Role","default":"user"}},"type":"object","required":["email","password"],"title":"Body_create_test_user_test_create_user_post"},"Body_login_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_login_post"},"ChallengeRequest":{"properties":{"challenge_type":{"type":"string","pattern":"^(otp|email|sms)$","title":"Challenge Type"},"session_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Id"}},"type":"object","required":["challenge_type"],"title":"ChallengeRequest","description":"Request a new challenge."},"ChallengeResponse":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"challenge_type":{"type":"string","title":"Challenge Type"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"message":{"type":"string","title":"Message"}},"type":"object","required":["challenge_id","challenge_type","expires_at","message"],"title":"ChallengeResponse","description":"Challenge created response."},"DeviceInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"browser":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser"},"os":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Os"},"first_seen":{"type":"string","format":"date-time","title":"First Seen"},"last_seen":{"type":"string","format":"date-time","title":"Last Seen"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","name","browser","os","first_seen","last_seen"],"title":"DeviceInfo","description":"Known device information."},"DeviceListResponse":{"properties":{"devices":{"items":{"$ref":"#/components/schemas/DeviceInfo"},"type":"array","title":"Devices"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["devices","total"],"title":"DeviceListResponse","description":"List of known devices."},"Enable2FAResponse":{"properties":{"secret":{"type":"string","title":"Secret"},"qr_code":{"type":"string","title":"Qr Code"},"backup_codes":{"items":{"type":"string"},"type":"array","title":"Backup Codes"}},"type":"object","required":["secret","qr_code","backup_codes"],"title":"Enable2FAResponse","description":"Enable 2FA response with QR code."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoginOTP":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["email","otp"],"title":"LoginOTP","description":"Login with TOTP code."},"PasswordChange":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["current_password","new_password","confirm_password"],"title":"PasswordChange","description":"Change password (authenticated)."},"PasswordResetConfirm":{"properties":{"reset_token":{"type":"string","title":"Reset Token"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["reset_token","new_password","confirm_password"],"title":"PasswordResetConfirm","description":"Confirm password reset."},"PasswordResetRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"PasswordResetRequest","description":"Request password reset."},"RiskAssessmentResult":{"properties":{"risk_score":{"type":"number","maximum":100.0,"minimum":0.0,"title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","maximum":4.0,"minimum":0.0,"title":"Security Level"},"risk_factors":{"additionalProperties":{"type":"number"},"type":"object","title":"Risk Factors"},"required_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Required Action"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["risk_score","risk_level","security_level","risk_factors"],"title":"RiskAssessmentResult","description":"Risk assessment result."},"RiskDashboardOverview":{"properties":{"total_risk_events":{"type":"integer","title":"Total Risk Events"},"high_risk_events":{"type":"integer","title":"High Risk Events"},"active_anomalies":{"type":"integer","title":"Active Anomalies"},"blocked_users":{"type":"integer","title":"Blocked Users"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_trend":{"type":"string","title":"Risk Trend"}},"type":"object","required":["total_risk_events","high_risk_events","active_anomalies","blocked_users","average_risk_score","risk_trend"],"title":"RiskDashboardOverview","description":"Risk dashboard overview."},"RiskEventList":{"properties":{"events":{"items":{"$ref":"#/components/schemas/RiskEventResponse"},"type":"array","title":"Events"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["events","total","page","page_size"],"title":"RiskEventList","description":"List of risk events."},"RiskEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"risk_score":{"type":"number","title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"risk_factors":{"additionalProperties":true,"type":"object","title":"Risk Factors"},"action_taken":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Taken"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"resolved":{"type":"boolean","title":"Resolved"}},"type":"object","required":["id","event_type","risk_score","risk_level","ip_address","risk_factors","action_taken","created_at","resolved"],"title":"RiskEventResponse","description":"Risk event information."},"RiskStatistics":{"properties":{"period":{"type":"string","title":"Period"},"total_logins":{"type":"integer","title":"Total Logins"},"successful_logins":{"type":"integer","title":"Successful Logins"},"failed_logins":{"type":"integer","title":"Failed Logins"},"blocked_attempts":{"type":"integer","title":"Blocked Attempts"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_distribution":{"additionalProperties":{"type":"integer"},"type":"object","title":"Risk Distribution"}},"type":"object","required":["period","total_logins","successful_logins","failed_logins","blocked_attempts","average_risk_score","risk_distribution"],"title":"RiskStatistics","description":"Risk statistics."},"SessionInfo":{"properties":{"id":{"type":"integer","title":"Id"},"ip_address":{"type":"string","title":"Ip Address"},"user_agent":{"type":"string","title":"User Agent"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"risk_level":{"type":"string","title":"Risk Level"},"status":{"type":"string","title":"Status"},"last_activity":{"type":"string","format":"date-time","title":"Last Activity"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","ip_address","user_agent","country","city","risk_level","status","last_activity","created_at"],"title":"SessionInfo","description":"Active session information."},"SessionListResponse":{"properties":{"sessions":{"items":{"$ref":"#/components/schemas/SessionInfo"},"type":"array","title":"Sessions"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["sessions","total"],"title":"SessionListResponse","description":"List of user sessions."},"SessionRevokeRequest":{"properties":{"session_ids":{"items":{"type":"integer"},"type":"array","title":"Session Ids"},"revoke_all":{"type":"boolean","title":"Revoke All","default":false}},"type":"object","required":["session_ids"],"title":"SessionRevokeRequest","description":"Request to revoke session(s)."},"StepUpRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"verification_code":{"type":"string","title":"Verification Code"}},"type":"object","required":["challenge_id","verification_code"],"title":"StepUpRequest","description":"Step-up authentication request."},"StepUpResponse":{"properties":{"status":{"type":"string","title":"Status"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["status"],"title":"StepUpResponse","description":"Step-up authentication response."},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"expires_in":{"type":"integer","title":"Expires In"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["access_token","expires_in"],"title":"TokenResponse","description":"JWT token response."},"UserLogin":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"UserLogin","description":"Standard login request."},"UserRegister":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":8,"title":"Password"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"}},"type":"object","required":["email","password"],"title":"UserRegister","description":"User registration request."},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"role":{"type":"string","title":"Role"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","full_name","role","is_active","is_verified","tfa_enabled","created_at"],"title":"UserResponse","description":"User information response."},"UserSecuritySettings":{"properties":{"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"last_password_change":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Password Change"},"active_sessions":{"type":"integer","title":"Active Sessions"},"known_devices":{"type":"integer","title":"Known Devices"},"recent_login_attempts":{"type":"integer","title":"Recent Login Attempts"}},"type":"object","required":["tfa_enabled","last_password_change","active_sessions","known_devices","recent_login_attempts"],"title":"UserSecuritySettings","description":"User security settings response."},"UserUpdate":{"properties":{"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"}},"type":"object","title":"UserUpdate","description":"Update user information."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Verify2FARequest":{"properties":{"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["otp"],"title":"Verify2FARequest","description":"Verify 2FA setup."},"VerifyChallengeRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"code":{"type":"string","title":"Code"}},"type":"object","required":["challenge_id","code"],"title":"VerifyChallengeRequest","description":"Verify challenge code."}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}}
 
 
static/index.html CHANGED
The diff for this file is too large to render. See raw diff