Users with Google accounts can now sign in without a password. Auth flow: - GET /auth/google → Google consent page (CSRF state cookie) - GET /auth/google/callback → exchange code, lookup user, set JWT - auth.json gains google_sub + google_email fields - set_password() no longer overwrites unrelated auth.json fields Admin setup: python manage_passwords.py google-add <username> <email> # add GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET to .env Per-user Gemini key: - get_user_gemini_key() reads gemini_api_key from auth.json - orchestrator_engine.run() accepts gemini_api_key param - orchestrator router passes user's key, falls back to server key login.html: "Sign in with Google" button above the password form. manage_passwords.py list: now shows auth method columns (pw / google). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
52 lines
1.8 KiB
Python
52 lines
1.8 KiB
Python
"""
|
|
Session auth middleware.
|
|
|
|
Validates the JWT cookie on every request. Unprotected paths are explicitly
|
|
listed in _PUBLIC. Webhook endpoints have their own auth (HMAC/JWT) so they
|
|
are also excluded.
|
|
|
|
Sets request.state.session_user to the authenticated username so downstream
|
|
routers can enforce ownership without re-reading the cookie.
|
|
"""
|
|
|
|
import jwt
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import RedirectResponse, JSONResponse
|
|
|
|
from auth_utils import COOKIE_NAME, decode_token
|
|
|
|
# Paths that don't require a session cookie
|
|
_PUBLIC = {"/login", "/logout", "/health"}
|
|
|
|
# Path prefixes that are always public (setup flow + webhooks + Google OAuth)
|
|
_PUBLIC_PREFIXES = ("/setup/", "/channels/", "/webhook/", "/auth/google")
|
|
|
|
|
|
class SessionAuthMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
path = request.url.path
|
|
|
|
# Always allow public paths and setup/webhook prefixes
|
|
if path in _PUBLIC or any(path.startswith(p) for p in _PUBLIC_PREFIXES):
|
|
return await call_next(request)
|
|
|
|
# Allow static assets without a cookie
|
|
if path.startswith("/static/"):
|
|
return await call_next(request)
|
|
|
|
# Validate session cookie
|
|
token = request.cookies.get(COOKIE_NAME)
|
|
if token:
|
|
try:
|
|
request.state.session_user = decode_token(token)
|
|
return await call_next(request)
|
|
except jwt.InvalidTokenError:
|
|
pass
|
|
|
|
# No valid session — redirect browser requests, 401 for API/JSON
|
|
accept = request.headers.get("accept", "")
|
|
if "text/html" in accept:
|
|
return RedirectResponse("/login", status_code=302)
|
|
return JSONResponse({"detail": "Not authenticated"}, status_code=401)
|