-
Notifications
You must be signed in to change notification settings - Fork 216
Expand file tree
/
Copy pathauth.py
More file actions
206 lines (162 loc) · 5.23 KB
/
Copy pathauth.py
File metadata and controls
206 lines (162 loc) · 5.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""
API Key Authentication for ClimateVision API.
Provides secure API key validation and organization-based
access control for all protected endpoints.
"""
from __future__ import annotations
import hashlib
import hmac
import logging
import secrets
from datetime import datetime
from typing import Optional
from fastapi import HTTPException, Request, Security
from fastapi.security import APIKeyHeader
logger = logging.getLogger(__name__)
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
class APIKeyAuth:
"""
API Key authentication handler with organization context.
Validates API keys and extracts organization information
for request-scoped access control.
"""
def __init__(self, db_connection=None):
self._db = db_connection
self._key_cache: dict[str, dict] = {}
def generate_api_key(self, org_id: int, org_name: str) -> str:
"""
Generate a new API key for an organization.
Args:
org_id: Organization ID
org_name: Organization name
Returns:
New API key string (prefix + random bytes)
"""
prefix = "cv_"
random_part = secrets.token_urlsafe(32)
api_key = f"{prefix}{random_part}"
logger.info(
"api_key_generated",
extra={
"org_id": org_id,
"org_name": org_name,
"key_prefix": api_key[:8],
}
)
return api_key
def hash_key(self, api_key: str) -> str:
"""Hash an API key for secure storage."""
return hashlib.sha256(api_key.encode()).hexdigest()
def validate_key(self, api_key: str) -> Optional[dict]:
"""
Validate an API key and return organization context.
Args:
api_key: The API key to validate
Returns:
Organization dict if valid, None otherwise
"""
if not api_key or not api_key.startswith("cv_"):
return None
# Development bypass — allow cv_dev for local testing
if api_key == "cv_dev":
return {
"id": 0,
"name": "Development",
"demo": True,
}
# Check cache first
key_hash = self.hash_key(api_key)
if key_hash in self._key_cache:
cached = self._key_cache[key_hash]
if cached.get("expires_at", datetime.max) > datetime.utcnow():
return cached.get("org")
# Would query database in production
# For now, return None to indicate key not found
return None
def revoke_key(self, api_key: str) -> bool:
"""
Revoke an API key.
Args:
api_key: The API key to revoke
Returns:
True if revoked successfully
"""
key_hash = self.hash_key(api_key)
if key_hash in self._key_cache:
del self._key_cache[key_hash]
logger.info(
"api_key_revoked",
extra={"key_prefix": api_key[:8] if api_key else "unknown"}
)
return True
# Singleton instance
_auth_handler: Optional[APIKeyAuth] = None
def get_auth_handler() -> APIKeyAuth:
"""Get or create the API key auth handler."""
global _auth_handler
if _auth_handler is None:
_auth_handler = APIKeyAuth()
return _auth_handler
async def require_api_key(
request: Request,
api_key: Optional[str] = Security(API_KEY_HEADER)
) -> dict:
"""
FastAPI dependency for requiring API key authentication.
Usage:
@app.get("/protected")
async def protected_endpoint(org: dict = Depends(require_api_key)):
return {"org_id": org["id"]}
"""
if not api_key:
logger.warning(
"auth_failed",
extra={
"reason": "missing_api_key",
"path": request.url.path,
"client_ip": request.client.host if request.client else "unknown",
}
)
raise HTTPException(
status_code=401,
detail="API key required. Include X-API-Key header."
)
auth = get_auth_handler()
org = auth.validate_key(api_key)
if not org:
logger.warning(
"auth_failed",
extra={
"reason": "invalid_api_key",
"key_prefix": api_key[:8] if len(api_key) >= 8 else "short",
"path": request.url.path,
}
)
raise HTTPException(
status_code=401,
detail="Invalid API key."
)
# Attach org context to request state
request.state.organization = org
logger.info(
"auth_success",
extra={
"org_id": org.get("id"),
"org_name": org.get("name"),
"path": request.url.path,
}
)
return org
async def optional_api_key(
request: Request,
api_key: Optional[str] = Security(API_KEY_HEADER)
) -> Optional[dict]:
"""
FastAPI dependency for optional API key authentication.
Returns organization context if valid key provided, None otherwise.
Does not raise exceptions for missing/invalid keys.
"""
if not api_key:
return None
auth = get_auth_handler()
return auth.validate_key(api_key)