Coverage for src/alprina_cli/api/middleware/auth.py: 19%
43 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2Authentication middleware for Alprina API.
3Handles API key verification and user authentication.
4"""
6from fastapi import Header, HTTPException, Depends
7from typing import Optional, Dict, Any
8from loguru import logger
10from ..services.neon_service import neon_service
13async def verify_api_key(authorization: Optional[str] = Header(None)) -> Dict[str, Any]:
14 """
15 Verify authentication from Authorization header.
17 Supports both:
18 - JWT tokens (web/mobile users)
19 - API keys (CLI users)
21 Args:
22 authorization: Authorization header (Bearer token)
24 Returns:
25 User dict if authenticated
27 Raises:
28 HTTPException: If authentication fails
29 """
30 if not authorization:
31 raise HTTPException(
32 status_code=401,
33 detail={
34 "error": "missing_authorization",
35 "message": "Authorization header is required",
36 "hint": "Include header: Authorization: Bearer <token>"
37 }
38 )
40 # Extract token from "Bearer <token>"
41 parts = authorization.split()
42 if len(parts) != 2 or parts[0].lower() != "bearer":
43 raise HTTPException(
44 status_code=401,
45 detail={
46 "error": "invalid_authorization_format",
47 "message": "Authorization header must be: Bearer <token>"
48 }
49 )
51 token = parts[1]
53 # NOTE: For Stack Auth JWT tokens, we don't verify them here because:
54 # 1. Stack Auth handles JWT verification on the frontend
55 # 2. Users are synced to Neon DB via /v1/auth/sync-stack-user endpoint
56 # 3. The frontend should use API keys (not JWT) for backend API calls
57 #
58 # Only API keys (starting with alprina_sk_) are supported for backend authentication
60 # Try API key (CLI users and web dashboard)
61 if token.startswith("alprina_sk_"):
62 user = await neon_service.verify_api_key(token)
64 if user:
65 # Check rate limits
66 rate_limit = await neon_service.check_rate_limit(user["id"])
68 if not rate_limit["allowed"]:
69 raise HTTPException(
70 status_code=429,
71 detail={
72 "error": "rate_limit_exceeded",
73 "message": rate_limit.get("reason", "Rate limit exceeded"),
74 "limit": rate_limit.get("limit"),
75 "used": rate_limit.get("used"),
76 "hint": "Upgrade to Pro for higher limits"
77 }
78 )
80 user["auth_type"] = "api_key"
81 logger.info(f"User authenticated via API key: {user['email']} (ID: {user['id']})")
82 return user
83 else:
84 logger.warning(f"Invalid API key attempted: {token[:20]}...")
85 raise HTTPException(
86 status_code=401,
87 detail={
88 "error": "invalid_api_key",
89 "message": "API key is invalid or has been revoked",
90 "hint": "Check your API key at /v1/auth/api-keys"
91 }
92 )
94 # Neither JWT nor API key worked
95 raise HTTPException(
96 status_code=401,
97 detail={
98 "error": "invalid_credentials",
99 "message": "Invalid authentication token",
100 "hint": "Use a valid JWT token or API key"
101 }
102 )
105async def get_current_user(user: Dict[str, Any] = Depends(verify_api_key)) -> Dict[str, Any]:
106 """
107 Get current authenticated user.
108 Convenience dependency that wraps verify_api_key.
109 """
110 return user
113async def get_current_user_no_rate_limit(
114 authorization: Optional[str] = Header(None, alias="Authorization")
115) -> Dict[str, Any]:
116 """
117 Get current authenticated user WITHOUT rate limiting.
119 Use this for critical endpoints like billing/checkout where rate limits
120 should not apply (users need to be able to purchase).
121 """
122 if not authorization:
123 raise HTTPException(
124 status_code=401,
125 detail={"error": "unauthorized", "message": "Authorization header required"}
126 )
128 if not authorization.startswith("Bearer "):
129 raise HTTPException(
130 status_code=401,
131 detail={"error": "invalid_token", "message": "Invalid authorization format"}
132 )
134 token = authorization.replace("Bearer ", "")
136 # Verify API key (no rate limiting check!)
137 user = await neon_service.verify_api_key(token)
139 if not user:
140 raise HTTPException(
141 status_code=401,
142 detail={"error": "invalid_token", "message": "Invalid or expired API key"}
143 )
145 user["auth_type"] = "api_key"
146 return user
149# Optional: Public endpoint (no auth required)
150async def optional_auth(authorization: Optional[str] = Header(None)) -> Optional[Dict[str, Any]]:
151 """
152 Optional authentication.
153 Returns user if authenticated, None if not.
154 Used for endpoints that work with or without auth.
155 """
156 if not authorization:
157 return None
159 try:
160 return await verify_api_key(authorization)
161 except HTTPException:
162 return None