vectorplasticity commited on
Commit
fccb659
·
verified ·
1 Parent(s): 0b0e0df

Simplify auth - remove middleware, keep session handling only

Browse files
Files changed (1) hide show
  1. app/auth.py +18 -145
app/auth.py CHANGED
@@ -1,50 +1,18 @@
1
  """
2
  Authentication module for Universal Model Trainer
3
  Provides password-based authentication with session management
 
4
  """
5
 
6
- from fastapi import HTTPException, Request, status
7
  from starlette.middleware.sessions import SessionMiddleware
8
  from starlette.responses import JSONResponse, HTMLResponse
9
- from starlette.types import ASGIApp, Receive, Scope, Send
10
- from itsdangerous import TimestampSigner, BadSignature
11
- import base64
12
- import json
13
  import secrets
14
- import time
15
- from typing import Optional, Callable
16
  from app.config import settings
17
 
18
 
19
- def decode_session_cookie(cookie_value: str) -> dict:
20
- """
21
- Decode session cookie value to dictionary.
22
- Must match Starlette's SessionMiddleware encoding exactly:
23
- 1. TimestampSigner signs the data
24
- 2. Data is base64 encoded JSON
25
- """
26
- if not cookie_value:
27
- return {}
28
- try:
29
- # Create the same signer that SessionMiddleware uses
30
- signer = TimestampSigner(
31
- settings.SESSION_SECRET_KEY,
32
- salt="cookie-session"
33
- )
34
-
35
- # Unsign the cookie value
36
- unsigned_value = signer.unsign(cookie_value)
37
-
38
- # Decode base64 and parse JSON
39
- decoded = base64.urlsafe_b64decode(unsigned_value)
40
- return json.loads(decoded)
41
- except (BadSignature, Exception) as e:
42
- # Log error for debugging but don't crash
43
- print(f"Session decode error: {e}")
44
- return {}
45
-
46
-
47
- def setup_auth(app: ASGIApp) -> None:
48
  """Add session middleware to the FastAPI app"""
49
  app.add_middleware(
50
  SessionMiddleware,
@@ -59,13 +27,20 @@ def setup_auth(app: ASGIApp) -> None:
59
  def verify_password(password: str) -> bool:
60
  """Verify password against env variable using constant-time comparison"""
61
  if not settings.APP_PASSWORD:
62
- # No password configured - allow access
63
- return True
64
  return secrets.compare_digest(password, settings.APP_PASSWORD)
65
 
66
 
67
- async def get_current_user(request: Request) -> Optional[dict]:
68
- """Check if user is logged in via session"""
 
 
 
 
 
 
 
 
69
  user = request.session.get("user")
70
  if user and user.get("authenticated"):
71
  return user
@@ -73,12 +48,11 @@ async def get_current_user(request: Request) -> Optional[dict]:
73
 
74
 
75
  async def require_auth(request: Request) -> dict:
76
- """Dependency that requires authentication"""
77
- # If no password is configured, allow access
78
  if not settings.APP_PASSWORD:
79
  return {"authenticated": True, "username": "anonymous"}
80
 
81
- user = await get_current_user(request)
82
  if not user:
83
  raise HTTPException(
84
  status_code=status.HTTP_401_UNAUTHORIZED,
@@ -88,107 +62,6 @@ async def require_auth(request: Request) -> dict:
88
  return user
89
 
90
 
91
- class AuthMiddleware:
92
- """
93
- Middleware to protect all /api and dashboard routes
94
- Allows public access to login, static files, and health checks
95
-
96
- IMPORTANT: This middleware decodes the session cookie directly because
97
- SessionMiddleware hasn't run yet when this middleware executes.
98
- Starlette middleware runs in REVERSE order - so AuthMiddleware (added first)
99
- runs BEFORE SessionMiddleware (added second).
100
- """
101
-
102
- # Paths that don't require authentication (exact match)
103
- PUBLIC_PATHS = [
104
- "/login",
105
- "/api/auth/login",
106
- "/api/auth/logout",
107
- "/api/auth/check",
108
- "/api/auth/status",
109
- "/health",
110
- "/favicon.ico",
111
- ]
112
-
113
- # Path prefixes that don't require authentication
114
- PUBLIC_PREFIXES = [
115
- "/static/",
116
- ]
117
-
118
- def __init__(self, app: ASGIApp):
119
- self.app = app
120
-
121
- async def __call__(self, scope: Scope, receive: Receive, send: Send):
122
- # Only handle HTTP requests
123
- if scope["type"] != "http":
124
- await self.app(scope, receive, send)
125
- return
126
-
127
- path = scope["path"]
128
-
129
- # Check if auth is configured
130
- if not settings.APP_PASSWORD:
131
- await self.app(scope, receive, send)
132
- return
133
-
134
- # Allow root path "/" - the dashboard handles its own auth check
135
- if path == "/":
136
- await self.app(scope, receive, send)
137
- return
138
-
139
- # Allow public paths
140
- if path in self.PUBLIC_PATHS:
141
- await self.app(scope, receive, send)
142
- return
143
-
144
- # Allow public prefixes
145
- for prefix in self.PUBLIC_PREFIXES:
146
- if path.startswith(prefix):
147
- await self.app(scope, receive, send)
148
- return
149
-
150
- # Check session cookie directly (SessionMiddleware hasn't run yet)
151
- headers = dict(scope.get("headers", []))
152
- cookie_header = headers.get(b"cookie", b"").decode("utf-8", errors="ignore")
153
-
154
- # Parse cookies
155
- session_value = None
156
- for cookie in cookie_header.split(";"):
157
- cookie = cookie.strip()
158
- if cookie.startswith("trainer_session="):
159
- session_value = cookie.split("=", 1)[1] if "=" in cookie else None
160
- break
161
-
162
- # Decode and check session
163
- if session_value:
164
- session = decode_session_cookie(session_value)
165
- if session.get("user", {}).get("authenticated"):
166
- await self.app(scope, receive, send)
167
- return
168
-
169
- # Unauthorized - return appropriate response
170
- accept = headers.get(b"accept", b"").decode("utf-8", errors="ignore")
171
-
172
- # Check if this is an API request or browser request
173
- is_api_request = path.startswith("/api/") or "application/json" in accept
174
-
175
- if is_api_request and "text/html" not in accept:
176
- # API request - return JSON error
177
- response = JSONResponse(
178
- {"detail": "Authentication required", "login_url": "/login"},
179
- status_code=401
180
- )
181
- else:
182
- # Browser request - redirect to login
183
- response = HTMLResponse(
184
- content='<script>window.location.href="/login";</script>',
185
- status_code=302,
186
- headers={"location": "/login"}
187
- )
188
-
189
- await response(scope, receive, send)
190
-
191
-
192
  def is_authenticated(request: Request) -> bool:
193
  """Check if request is authenticated (helper for templates)"""
194
  if not settings.APP_PASSWORD:
@@ -378,4 +251,4 @@ def get_login_html(error: str = None) -> str:
378
  document.getElementById('password').focus();
379
  </script>
380
  </body>
381
- </html>'''
 
1
  """
2
  Authentication module for Universal Model Trainer
3
  Provides password-based authentication with session management
4
+ SIMPLIFIED VERSION - No middleware, direct session checks
5
  """
6
 
7
+ from fastapi import HTTPException, Request, status, Depends
8
  from starlette.middleware.sessions import SessionMiddleware
9
  from starlette.responses import JSONResponse, HTMLResponse
 
 
 
 
10
  import secrets
11
+ from typing import Optional
 
12
  from app.config import settings
13
 
14
 
15
+ def setup_auth(app) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  """Add session middleware to the FastAPI app"""
17
  app.add_middleware(
18
  SessionMiddleware,
 
27
  def verify_password(password: str) -> bool:
28
  """Verify password against env variable using constant-time comparison"""
29
  if not settings.APP_PASSWORD:
30
+ return True # No password configured
 
31
  return secrets.compare_digest(password, settings.APP_PASSWORD)
32
 
33
 
34
+ def is_auth_enabled() -> bool:
35
+ """Check if authentication is enabled"""
36
+ return bool(settings.APP_PASSWORD)
37
+
38
+
39
+ def get_current_user(request: Request) -> Optional[dict]:
40
+ """Get current user from session if authenticated"""
41
+ if not settings.APP_PASSWORD:
42
+ return {"authenticated": True, "username": "anonymous"}
43
+
44
  user = request.session.get("user")
45
  if user and user.get("authenticated"):
46
  return user
 
48
 
49
 
50
  async def require_auth(request: Request) -> dict:
51
+ """Dependency that requires authentication - returns user or raises 401"""
 
52
  if not settings.APP_PASSWORD:
53
  return {"authenticated": True, "username": "anonymous"}
54
 
55
+ user = get_current_user(request)
56
  if not user:
57
  raise HTTPException(
58
  status_code=status.HTTP_401_UNAUTHORIZED,
 
62
  return user
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  def is_authenticated(request: Request) -> bool:
66
  """Check if request is authenticated (helper for templates)"""
67
  if not settings.APP_PASSWORD:
 
251
  document.getElementById('password').focus();
252
  </script>
253
  </body>
254
+ </html>'''