verge-auth-sdk 0.1.14__py3-none-any.whl → 0.1.48__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- verge_auth_sdk/__init__.py +1 -1
- verge_auth_sdk/middleware.py +828 -84
- verge_auth_sdk/verge_routes.py +1 -1
- {verge_auth_sdk-0.1.14.dist-info → verge_auth_sdk-0.1.48.dist-info}/METADATA +43 -14
- verge_auth_sdk-0.1.48.dist-info/RECORD +9 -0
- verge_auth_sdk-0.1.14.dist-info/RECORD +0 -9
- {verge_auth_sdk-0.1.14.dist-info → verge_auth_sdk-0.1.48.dist-info}/WHEEL +0 -0
- {verge_auth_sdk-0.1.14.dist-info → verge_auth_sdk-0.1.48.dist-info}/licenses/LICENSE +0 -0
- {verge_auth_sdk-0.1.14.dist-info → verge_auth_sdk-0.1.48.dist-info}/top_level.txt +0 -0
verge_auth_sdk/__init__.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
verge_auth_sdk package public API.
|
|
4
4
|
Exports:
|
|
5
|
-
- add_central_auth(app) -> middleware to attach to
|
|
5
|
+
- add_central_auth(app) -> middleware to attach to Python app
|
|
6
6
|
- get_secret(name) -> secret retrieval helper
|
|
7
7
|
"""
|
|
8
8
|
from .middleware import add_central_auth
|
verge_auth_sdk/middleware.py
CHANGED
|
@@ -1,135 +1,879 @@
|
|
|
1
|
+
# from fastapi import FastAPI, Request
|
|
2
|
+
# from fastapi.responses import RedirectResponse, JSONResponse
|
|
3
|
+
# import httpx
|
|
4
|
+
# import os
|
|
5
|
+
# import asyncio
|
|
6
|
+
# import jwt
|
|
7
|
+
# from typing import List
|
|
8
|
+
# from .secret_provider import get_secret
|
|
9
|
+
# from .verge_routes import router as verge_routes_router
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# def is_request_secure(request: Request) -> bool:
|
|
13
|
+
# """
|
|
14
|
+
# Returns True when the browser-facing connection is HTTPS.
|
|
15
|
+
# Works for:
|
|
16
|
+
# - Real production HTTPS
|
|
17
|
+
# - Ngrok (HTTPS → HTTP backend)
|
|
18
|
+
# - Cloud LB / Nginx / ALB setups
|
|
19
|
+
# """
|
|
20
|
+
# # Case 1: Native HTTPS
|
|
21
|
+
# if request.url.scheme == "https":
|
|
22
|
+
# return True
|
|
23
|
+
|
|
24
|
+
# # Case 2: Behind proxy / ngrok
|
|
25
|
+
# forwarded_proto = request.headers.get("x-forwarded-proto")
|
|
26
|
+
# if forwarded_proto and forwarded_proto.lower() == "https":
|
|
27
|
+
# return True
|
|
28
|
+
|
|
29
|
+
# return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# REGISTERED_ROUTES: List = []
|
|
33
|
+
|
|
34
|
+
# # ============================================================
|
|
35
|
+
# # GLOBAL JWT CACHE
|
|
36
|
+
# # ============================================================
|
|
37
|
+
# JWT_PUBLIC_KEY: str | None = None
|
|
38
|
+
# JWT_KEY_ID: str | None = None
|
|
39
|
+
# JWT_ALGORITHMS = ["RS256"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# # -----------------------------------------------------------
|
|
43
|
+
# # HTTP helper with retries
|
|
44
|
+
# # -----------------------------------------------------------
|
|
45
|
+
# async def _post_with_retries(
|
|
46
|
+
# client,
|
|
47
|
+
# url,
|
|
48
|
+
# json=None,
|
|
49
|
+
# headers=None,
|
|
50
|
+
# timeout=10,
|
|
51
|
+
# retries=8,
|
|
52
|
+
# backoff=1,
|
|
53
|
+
# ):
|
|
54
|
+
# last_exc = None
|
|
55
|
+
# for attempt in range(1, retries + 1):
|
|
56
|
+
# try:
|
|
57
|
+
# resp = await client.post(
|
|
58
|
+
# url,
|
|
59
|
+
# json=json,
|
|
60
|
+
# headers=headers,
|
|
61
|
+
# timeout=timeout,
|
|
62
|
+
# )
|
|
63
|
+
# return resp
|
|
64
|
+
# except Exception as e:
|
|
65
|
+
# last_exc = e
|
|
66
|
+
# print(
|
|
67
|
+
# f"❗ Retry {attempt}/{retries} failed for {url}: "
|
|
68
|
+
# f"{type(e).__name__}: {e}"
|
|
69
|
+
# )
|
|
70
|
+
# await asyncio.sleep(backoff * attempt)
|
|
71
|
+
# raise last_exc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# # -----------------------------------------------------------
|
|
75
|
+
# # PUBLIC KEY DISCOVERY
|
|
76
|
+
# # -----------------------------------------------------------
|
|
77
|
+
# async def load_public_key(force: bool = False):
|
|
78
|
+
# global JWT_PUBLIC_KEY, JWT_KEY_ID
|
|
79
|
+
|
|
80
|
+
# if JWT_PUBLIC_KEY and not force:
|
|
81
|
+
# return
|
|
82
|
+
|
|
83
|
+
# AUTH_PUBLIC_KEY_URL = os.getenv("AUTH_PUBLIC_KEY_URL")
|
|
84
|
+
# if not AUTH_PUBLIC_KEY_URL:
|
|
85
|
+
# print("❌ AUTH_PUBLIC_KEY_URL not set! Please Set it.")
|
|
86
|
+
# return
|
|
87
|
+
|
|
88
|
+
# try:
|
|
89
|
+
# async with httpx.AsyncClient(timeout=5) as client:
|
|
90
|
+
# resp = await client.get(AUTH_PUBLIC_KEY_URL)
|
|
91
|
+
# resp.raise_for_status()
|
|
92
|
+
# data = resp.json()
|
|
93
|
+
|
|
94
|
+
# JWT_PUBLIC_KEY = data.get("public_key")
|
|
95
|
+
# JWT_KEY_ID = data.get("kid")
|
|
96
|
+
|
|
97
|
+
# if JWT_PUBLIC_KEY:
|
|
98
|
+
# print("✅ Security Key Loaded Successfully")
|
|
99
|
+
# else:
|
|
100
|
+
# print("❌ Security Key Loading Failed")
|
|
101
|
+
|
|
102
|
+
# except Exception as e:
|
|
103
|
+
# print("❌ Failed to load Security Key:", str(e))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# # -----------------------------------------------------------
|
|
107
|
+
# # MAIN INTEGRATION
|
|
108
|
+
# # -----------------------------------------------------------
|
|
109
|
+
# def add_central_auth(app: FastAPI):
|
|
110
|
+
# AUTH_BASE_URL = os.getenv("AUTH_BASE_URL")
|
|
111
|
+
# SERVICE_NAME = os.getenv("SERVICE_NAME")
|
|
112
|
+
# SERVICE_BASE_URL = os.getenv("SERVICE_BASE_URL")
|
|
113
|
+
# CLIENT_ID = os.getenv("VERGE_CLIENT_ID")
|
|
114
|
+
# CLIENT_SECRET = os.getenv("VERGE_CLIENT_SECRET")
|
|
115
|
+
# VERGE_SERVICE_SECRET = get_secret("VERGE_SERVICE_SECRET")
|
|
116
|
+
# AUTH_REGISTER_URL = os.getenv("AUTH_REGISTER_URL")
|
|
117
|
+
# AUTH_ROUTE_SYNC_URL = os.getenv("AUTH_ROUTE_SYNC_URL")
|
|
118
|
+
# INTROSPECT_URL = os.getenv("AUTH_INTROSPECT_URL")
|
|
119
|
+
|
|
120
|
+
# # Include internal verge routes
|
|
121
|
+
# app.include_router(verge_routes_router)
|
|
122
|
+
|
|
123
|
+
# # -------------------------------------------------------
|
|
124
|
+
# # MICROSERVICE BOOTSTRAP ON STARTUP
|
|
125
|
+
# # -------------------------------------------------------
|
|
126
|
+
# @app.on_event("startup")
|
|
127
|
+
# async def verge_bootstrap():
|
|
128
|
+
# print("🔥 Verge Auth started")
|
|
129
|
+
|
|
130
|
+
# # Load JWT public key
|
|
131
|
+
# await load_public_key(force=True)
|
|
132
|
+
# await asyncio.sleep(2)
|
|
133
|
+
|
|
134
|
+
# REGISTERED_ROUTES.clear()
|
|
135
|
+
# print("📌 Collecting routes...")
|
|
136
|
+
|
|
137
|
+
# for route in app.routes:
|
|
138
|
+
# try:
|
|
139
|
+
# path = getattr(route, "path", None)
|
|
140
|
+
# methods = getattr(route, "methods", [])
|
|
141
|
+
|
|
142
|
+
# if not path:
|
|
143
|
+
# continue
|
|
144
|
+
|
|
145
|
+
# if path.startswith(("/docs", "/openapi", "/__verge__")):
|
|
146
|
+
# continue
|
|
147
|
+
|
|
148
|
+
# for m in methods:
|
|
149
|
+
# if m in ("GET", "POST", "PUT", "PATCH", "DELETE"):
|
|
150
|
+
# REGISTERED_ROUTES.append({"path": path, "method": m})
|
|
151
|
+
|
|
152
|
+
# except Exception as e:
|
|
153
|
+
# print("❌ Error collecting route:", e)
|
|
154
|
+
|
|
155
|
+
# print("\n📡 Registering service with Verge Auth...")
|
|
156
|
+
|
|
157
|
+
# async with httpx.AsyncClient() as client:
|
|
158
|
+
# if AUTH_REGISTER_URL:
|
|
159
|
+
# try:
|
|
160
|
+
# resp = await _post_with_retries(
|
|
161
|
+
# client,
|
|
162
|
+
# AUTH_REGISTER_URL,
|
|
163
|
+
# json={
|
|
164
|
+
# "service_name": SERVICE_NAME,
|
|
165
|
+
# "base_url": SERVICE_BASE_URL,
|
|
166
|
+
# },
|
|
167
|
+
# headers={
|
|
168
|
+
# "X-Client-Id": CLIENT_ID or "",
|
|
169
|
+
# "X-Client-Secret": CLIENT_SECRET or "",
|
|
170
|
+
# "X-Verge-Service-Secret": VERGE_SERVICE_SECRET or "",
|
|
171
|
+
# },
|
|
172
|
+
# )
|
|
173
|
+
# print(
|
|
174
|
+
# "📡 Registration response:",
|
|
175
|
+
# resp.status_code,
|
|
176
|
+
# resp.text,
|
|
177
|
+
# )
|
|
178
|
+
# except Exception as e:
|
|
179
|
+
# print("❌ Registration failed:", e)
|
|
180
|
+
|
|
181
|
+
# if AUTH_ROUTE_SYNC_URL:
|
|
182
|
+
# try:
|
|
183
|
+
# resp = await _post_with_retries(
|
|
184
|
+
# client,
|
|
185
|
+
# AUTH_ROUTE_SYNC_URL,
|
|
186
|
+
# json={
|
|
187
|
+
# "service_name": SERVICE_NAME,
|
|
188
|
+
# "base_url": SERVICE_BASE_URL,
|
|
189
|
+
# "routes": REGISTERED_ROUTES,
|
|
190
|
+
# },
|
|
191
|
+
# headers={
|
|
192
|
+
# "X-Client-Id": CLIENT_ID or "",
|
|
193
|
+
# "X-Client-Secret": CLIENT_SECRET or "",
|
|
194
|
+
# "X-Verge-Service-Secret": VERGE_SERVICE_SECRET or "",
|
|
195
|
+
# },
|
|
196
|
+
# timeout=20,
|
|
197
|
+
# )
|
|
198
|
+
# print(
|
|
199
|
+
# "📡 Route sync response:",
|
|
200
|
+
# resp.status_code,
|
|
201
|
+
# resp.text,
|
|
202
|
+
# )
|
|
203
|
+
# except Exception as e:
|
|
204
|
+
# print("❌ Route sync failed:", e)
|
|
205
|
+
|
|
206
|
+
# # -------------------------------------------------------
|
|
207
|
+
# # CENTRAL AUTHZ MIDDLEWARE (CORRECTED FLOW)
|
|
208
|
+
# # -------------------------------------------------------
|
|
209
|
+
# @app.middleware("http")
|
|
210
|
+
# async def central_auth(request: Request, call_next):
|
|
211
|
+
# path = request.url.path
|
|
212
|
+
|
|
213
|
+
# SKIP_PATHS = {
|
|
214
|
+
# "/health",
|
|
215
|
+
# "/openapi.json",
|
|
216
|
+
# "/favicon.ico",
|
|
217
|
+
# "/service-registry/register",
|
|
218
|
+
# "/route-sync",
|
|
219
|
+
# "/__verge__",
|
|
220
|
+
# "/post-login",
|
|
221
|
+
# }
|
|
222
|
+
|
|
223
|
+
# # safer matching (handles trailing slash)
|
|
224
|
+
# if path.rstrip("/") in SKIP_PATHS or path.startswith("/__verge__"):
|
|
225
|
+
# return await call_next(request)
|
|
226
|
+
|
|
227
|
+
# # ---------------------------------------------------
|
|
228
|
+
# # STEP 1 — HANDLE AUTH CODE FIRST
|
|
229
|
+
# # ---------------------------------------------------
|
|
230
|
+
# if "code" in request.query_params:
|
|
231
|
+
# code = request.query_params.get("code")
|
|
232
|
+
|
|
233
|
+
# # Prevent infinite redirect loop
|
|
234
|
+
# if request.cookies.get("verge_access"):
|
|
235
|
+
# return await call_next(request)
|
|
236
|
+
|
|
237
|
+
# try:
|
|
238
|
+
# async with httpx.AsyncClient(timeout=5) as client:
|
|
239
|
+
# resp = await client.post(
|
|
240
|
+
# f"{AUTH_BASE_URL}/auth/exchange",
|
|
241
|
+
# json={"code": code},
|
|
242
|
+
# headers={
|
|
243
|
+
# "X-Client-Id": os.getenv("VERGE_CLIENT_ID") or "",
|
|
244
|
+
# "X-Client-Secret": os.getenv("VERGE_CLIENT_SECRET") or "",
|
|
245
|
+
# },
|
|
246
|
+
# )
|
|
247
|
+
# resp.raise_for_status()
|
|
248
|
+
# token = resp.json().get("access_token")
|
|
249
|
+
|
|
250
|
+
# if not token:
|
|
251
|
+
# return JSONResponse(
|
|
252
|
+
# {"detail": "Authorization failed: no token returned"},
|
|
253
|
+
# status_code=401,
|
|
254
|
+
# )
|
|
255
|
+
# clean_url = str(request.url.remove_query_params("code"))
|
|
256
|
+
# response = RedirectResponse(clean_url)
|
|
257
|
+
# response.set_cookie(
|
|
258
|
+
# "verge_access",
|
|
259
|
+
# token,
|
|
260
|
+
# httponly=True,
|
|
261
|
+
# secure=is_request_secure(request),
|
|
262
|
+
# samesite="lax",
|
|
263
|
+
# path="/",
|
|
264
|
+
# )
|
|
265
|
+
|
|
266
|
+
# response.set_cookie(
|
|
267
|
+
# "verge_fresh_auth",
|
|
268
|
+
# "1",
|
|
269
|
+
# httponly=True,
|
|
270
|
+
# secure=is_request_secure(request),
|
|
271
|
+
# samesite="lax",
|
|
272
|
+
# path="/",
|
|
273
|
+
# max_age=5, # only valid for 5 seconds
|
|
274
|
+
# )
|
|
275
|
+
# return response
|
|
276
|
+
|
|
277
|
+
# except Exception as e:
|
|
278
|
+
# return JSONResponse(
|
|
279
|
+
# {"detail": "Authorization failed", "error": str(e)},
|
|
280
|
+
# status_code=401,
|
|
281
|
+
# )
|
|
282
|
+
|
|
283
|
+
# # ---------------------------------------------------
|
|
284
|
+
# # STEP 2 — COLLECT TOKEN (cookie → header → session)
|
|
285
|
+
# # ---------------------------------------------------
|
|
286
|
+
# token = request.cookies.get("verge_access")
|
|
287
|
+
|
|
288
|
+
# if not token:
|
|
289
|
+
# auth_header = request.headers.get("authorization")
|
|
290
|
+
# if auth_header and auth_header.lower().startswith("bearer "):
|
|
291
|
+
# token = auth_header.split(" ", 1)[1].strip()
|
|
292
|
+
|
|
293
|
+
# if not token and "session" in request.scope:
|
|
294
|
+
# token = request.scope["session"].get("access_token")
|
|
295
|
+
|
|
296
|
+
# # if getattr(request.state, "_just_authenticated", False):
|
|
297
|
+
# # return await call_next(request)
|
|
298
|
+
|
|
299
|
+
# if request.cookies.get("verge_fresh_auth") == "1":
|
|
300
|
+
# response = await call_next(request)
|
|
301
|
+
|
|
302
|
+
# # clean up the flag so normal auth resumes next time
|
|
303
|
+
# response.delete_cookie("verge_fresh_auth")
|
|
304
|
+
# return response
|
|
305
|
+
|
|
306
|
+
# # ---------------------------------------------------
|
|
307
|
+
# # STEP 3 — FAIL CLEANLY IF STILL NO TOKEN
|
|
308
|
+
# # ---------------------------------------------------
|
|
309
|
+
# if not token:
|
|
310
|
+
# login_url = f"{os.getenv('AUTH_LOGIN_URL')}?redirect={request.url}"
|
|
311
|
+
# return RedirectResponse(login_url)
|
|
312
|
+
|
|
313
|
+
# # ---------------------------------------------------
|
|
314
|
+
# # STEP 4 — LOCAL JWT VERIFICATION
|
|
315
|
+
# # ---------------------------------------------------
|
|
316
|
+
# try:
|
|
317
|
+
# if not JWT_PUBLIC_KEY:
|
|
318
|
+
# await load_public_key(force=True)
|
|
319
|
+
|
|
320
|
+
# if not JWT_PUBLIC_KEY:
|
|
321
|
+
# return JSONResponse(
|
|
322
|
+
# {"detail": "Auth key not ready"},
|
|
323
|
+
# status_code=503,
|
|
324
|
+
# )
|
|
325
|
+
|
|
326
|
+
# payload = jwt.decode(
|
|
327
|
+
# token,
|
|
328
|
+
# JWT_PUBLIC_KEY,
|
|
329
|
+
# algorithms=JWT_ALGORITHMS,
|
|
330
|
+
# options={"require": ["exp", "iat"]},
|
|
331
|
+
# )
|
|
332
|
+
|
|
333
|
+
# except jwt.ExpiredSignatureError:
|
|
334
|
+
# return JSONResponse({"detail": "Token expired"}, status_code=401)
|
|
335
|
+
# except jwt.InvalidTokenError:
|
|
336
|
+
# return JSONResponse({"detail": "Invalid token"}, status_code=401)
|
|
337
|
+
# except Exception as e:
|
|
338
|
+
# return JSONResponse(
|
|
339
|
+
# {"detail": "Auth verification failed", "error": str(e)},
|
|
340
|
+
# status_code=401,
|
|
341
|
+
# )
|
|
342
|
+
|
|
343
|
+
# # ---------------------------------------------------
|
|
344
|
+
# # STEP 5 — CENTRAL INTROSPECTION
|
|
345
|
+
# # ---------------------------------------------------
|
|
346
|
+
# try:
|
|
347
|
+
# async with httpx.AsyncClient(timeout=5) as client:
|
|
348
|
+
# resp = await client.post(
|
|
349
|
+
# INTROSPECT_URL,
|
|
350
|
+
# headers={
|
|
351
|
+
# "Authorization": f"Bearer {token}",
|
|
352
|
+
# "X-Client-Id": os.getenv("VERGE_CLIENT_ID") or "",
|
|
353
|
+
# "X-Client-Secret": os.getenv("VERGE_CLIENT_SECRET") or "",
|
|
354
|
+
# },
|
|
355
|
+
# )
|
|
356
|
+
|
|
357
|
+
# data = resp.json()
|
|
358
|
+
# if not data.get("active"):
|
|
359
|
+
# return JSONResponse(
|
|
360
|
+
# {
|
|
361
|
+
# "detail": "Session inactive",
|
|
362
|
+
# "reason": data.get("reason"),
|
|
363
|
+
# },
|
|
364
|
+
# status_code=401,
|
|
365
|
+
# )
|
|
366
|
+
|
|
367
|
+
# request.state.introspect = data
|
|
368
|
+
|
|
369
|
+
# except Exception as e:
|
|
370
|
+
# return JSONResponse(
|
|
371
|
+
# {"detail": "Auth introspection failed", "error": str(e)},
|
|
372
|
+
# status_code=401,
|
|
373
|
+
# )
|
|
374
|
+
|
|
375
|
+
# # ---------------------------------------------------
|
|
376
|
+
# # STEP 6 — PERMISSION CHECK
|
|
377
|
+
# # ---------------------------------------------------
|
|
378
|
+
# request.state.user = payload
|
|
379
|
+
# permissions = payload.get("roles") or []
|
|
380
|
+
|
|
381
|
+
# route_obj = request.scope.get("route")
|
|
382
|
+
# route_path = route_obj.path if route_obj else path
|
|
383
|
+
# method = request.method
|
|
384
|
+
|
|
385
|
+
# # 🔹 SPECIAL CASE: allow redoc AFTER auth
|
|
386
|
+
# if path.rstrip("/") in {"/redoc", "/docs"}:
|
|
387
|
+
# return await call_next(request)
|
|
388
|
+
|
|
389
|
+
# required_key = f"{SERVICE_NAME}:{route_path}:{method}".lower()
|
|
390
|
+
# normalized_permissions = [p.lower() for p in permissions]
|
|
391
|
+
|
|
392
|
+
# if required_key not in normalized_permissions:
|
|
393
|
+
# return JSONResponse(
|
|
394
|
+
# {"detail": "Contact admin for access"},
|
|
395
|
+
# status_code=403,
|
|
396
|
+
# )
|
|
397
|
+
|
|
398
|
+
# return await call_next(request)
|
|
399
|
+
|
|
400
|
+
|
|
1
401
|
from fastapi import FastAPI, Request
|
|
2
402
|
from fastapi.responses import RedirectResponse, JSONResponse
|
|
3
403
|
import httpx
|
|
4
404
|
import os
|
|
5
|
-
|
|
405
|
+
import asyncio
|
|
406
|
+
import jwt
|
|
407
|
+
from typing import List
|
|
6
408
|
from .secret_provider import get_secret
|
|
7
409
|
from .verge_routes import router as verge_routes_router
|
|
8
410
|
|
|
9
|
-
REGISTERED_ROUTES = []
|
|
10
411
|
|
|
412
|
+
def is_request_secure(request: Request) -> bool:
|
|
413
|
+
"""
|
|
414
|
+
Returns True when the browser-facing connection is HTTPS.
|
|
415
|
+
Works for:
|
|
416
|
+
- Real production HTTPS
|
|
417
|
+
- Ngrok (HTTPS → HTTP backend)
|
|
418
|
+
- Cloud LB / Nginx / ALB setups
|
|
419
|
+
"""
|
|
420
|
+
# Case 1: Native HTTPS
|
|
421
|
+
if request.url.scheme == "https":
|
|
422
|
+
return True
|
|
11
423
|
|
|
12
|
-
|
|
424
|
+
# Case 2: Behind proxy / ngrok
|
|
425
|
+
forwarded_proto = request.headers.get("x-forwarded-proto")
|
|
426
|
+
if forwarded_proto and forwarded_proto.lower() == "https":
|
|
427
|
+
return True
|
|
13
428
|
|
|
14
|
-
|
|
15
|
-
AUTH_LOGIN_URL = os.getenv("AUTH_LOGIN_URL")
|
|
429
|
+
return False
|
|
16
430
|
|
|
17
|
-
CLIENT_ID = os.getenv("VERGE_CLIENT_ID")
|
|
18
|
-
CLIENT_SECRET = os.getenv("VERGE_CLIENT_SECRET")
|
|
19
431
|
|
|
432
|
+
REGISTERED_ROUTES: List = []
|
|
433
|
+
|
|
434
|
+
# ============================================================
|
|
435
|
+
# GLOBAL JWT CACHE
|
|
436
|
+
# ============================================================
|
|
437
|
+
JWT_PUBLIC_KEY: str | None = None
|
|
438
|
+
JWT_KEY_ID: str | None = None
|
|
439
|
+
JWT_ALGORITHMS = ["RS256"]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# -----------------------------------------------------------
|
|
443
|
+
# HTTP helper with retries
|
|
444
|
+
# -----------------------------------------------------------
|
|
445
|
+
async def _post_with_retries(
|
|
446
|
+
client,
|
|
447
|
+
url,
|
|
448
|
+
json=None,
|
|
449
|
+
headers=None,
|
|
450
|
+
timeout=10,
|
|
451
|
+
retries=8,
|
|
452
|
+
backoff=1,
|
|
453
|
+
):
|
|
454
|
+
last_exc = None
|
|
455
|
+
for attempt in range(1, retries + 1):
|
|
456
|
+
try:
|
|
457
|
+
resp = await client.post(
|
|
458
|
+
url,
|
|
459
|
+
json=json,
|
|
460
|
+
headers=headers,
|
|
461
|
+
timeout=timeout,
|
|
462
|
+
)
|
|
463
|
+
return resp
|
|
464
|
+
except Exception as e:
|
|
465
|
+
last_exc = e
|
|
466
|
+
print(
|
|
467
|
+
f"❗ Retry {attempt}/{retries} failed for {url}: "
|
|
468
|
+
f"{type(e).__name__}: {e}"
|
|
469
|
+
)
|
|
470
|
+
await asyncio.sleep(backoff * attempt)
|
|
471
|
+
raise last_exc
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# -----------------------------------------------------------
|
|
475
|
+
# PUBLIC KEY DISCOVERY
|
|
476
|
+
# -----------------------------------------------------------
|
|
477
|
+
async def load_public_key(force: bool = False):
|
|
478
|
+
global JWT_PUBLIC_KEY, JWT_KEY_ID
|
|
479
|
+
|
|
480
|
+
if JWT_PUBLIC_KEY and not force:
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
AUTH_PUBLIC_KEY_URL = os.getenv("AUTH_PUBLIC_KEY_URL")
|
|
484
|
+
if not AUTH_PUBLIC_KEY_URL:
|
|
485
|
+
print("❌ AUTH_PUBLIC_KEY_URL not set! Please Set it.")
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
async with httpx.AsyncClient(timeout=50) as client:
|
|
490
|
+
resp = await client.get(AUTH_PUBLIC_KEY_URL)
|
|
491
|
+
resp.raise_for_status()
|
|
492
|
+
data = resp.json()
|
|
493
|
+
|
|
494
|
+
JWT_PUBLIC_KEY = data.get("public_key")
|
|
495
|
+
JWT_KEY_ID = data.get("kid")
|
|
496
|
+
|
|
497
|
+
if JWT_PUBLIC_KEY:
|
|
498
|
+
print("✅ Security Key Loaded Successfully")
|
|
499
|
+
else:
|
|
500
|
+
print("❌ Security Key Loading Failed")
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
print("❌ Failed to load Security Key:", str(e))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# -----------------------------------------------------------
|
|
507
|
+
# MAIN INTEGRATION
|
|
508
|
+
# -----------------------------------------------------------
|
|
509
|
+
def add_central_auth(app: FastAPI):
|
|
510
|
+
AUTH_BASE_URL = os.getenv("AUTH_BASE_URL")
|
|
20
511
|
SERVICE_NAME = os.getenv("SERVICE_NAME")
|
|
21
512
|
SERVICE_BASE_URL = os.getenv("SERVICE_BASE_URL")
|
|
513
|
+
CLIENT_ID = os.getenv("VERGE_CLIENT_ID")
|
|
514
|
+
CLIENT_SECRET = os.getenv("VERGE_CLIENT_SECRET")
|
|
515
|
+
VERGE_SERVICE_SECRET = get_secret("VERGE_SERVICE_SECRET")
|
|
22
516
|
AUTH_REGISTER_URL = os.getenv("AUTH_REGISTER_URL")
|
|
517
|
+
AUTH_ROUTE_SYNC_URL = os.getenv("AUTH_ROUTE_SYNC_URL")
|
|
518
|
+
INTROSPECT_URL = os.getenv("AUTH_INTROSPECT_URL")
|
|
23
519
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# ---------------------------------------
|
|
27
|
-
# INTERNAL ROUTES (MUST LOAD FIRST)
|
|
28
|
-
# ---------------------------------------
|
|
520
|
+
# Include internal verge routes
|
|
29
521
|
app.include_router(verge_routes_router)
|
|
30
522
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
523
|
+
# ================== ADD THIS BLOCK HERE ==================
|
|
524
|
+
@app.post("/__verge__/set-cookie")
|
|
525
|
+
def verge_set_cookie(request: Request):
|
|
526
|
+
auth = request.headers.get("authorization")
|
|
527
|
+
if not auth:
|
|
528
|
+
return JSONResponse({"detail": "Missing Authorization header"}, status_code=401)
|
|
529
|
+
|
|
530
|
+
token = auth.split(" ")[1]
|
|
531
|
+
|
|
532
|
+
response = JSONResponse({"ok": True})
|
|
533
|
+
response.set_cookie(
|
|
534
|
+
"verge_access",
|
|
535
|
+
token,
|
|
536
|
+
httponly=True,
|
|
537
|
+
secure=True,
|
|
538
|
+
# samesite="lax",
|
|
539
|
+
samesite="None",
|
|
540
|
+
path="/",
|
|
541
|
+
)
|
|
542
|
+
return response
|
|
543
|
+
# ================== END OF INSERT =======================
|
|
544
|
+
|
|
545
|
+
# -------------------------------------------------------
|
|
546
|
+
# MICROSERVICE BOOTSTRAP ON STARTUP
|
|
547
|
+
# -------------------------------------------------------
|
|
34
548
|
@app.on_event("startup")
|
|
35
|
-
async def
|
|
36
|
-
print("🔥 Verge
|
|
549
|
+
async def verge_bootstrap():
|
|
550
|
+
print("🔥 Verge Auth started")
|
|
551
|
+
|
|
552
|
+
# Load JWT public key
|
|
553
|
+
await load_public_key(force=True)
|
|
554
|
+
await asyncio.sleep(2)
|
|
555
|
+
|
|
37
556
|
REGISTERED_ROUTES.clear()
|
|
557
|
+
print("📌 Collecting routes...")
|
|
38
558
|
|
|
39
559
|
for route in app.routes:
|
|
40
|
-
|
|
41
|
-
|
|
560
|
+
try:
|
|
561
|
+
path = getattr(route, "path", None)
|
|
562
|
+
methods = getattr(route, "methods", [])
|
|
42
563
|
|
|
43
|
-
|
|
44
|
-
|
|
564
|
+
if not path:
|
|
565
|
+
continue
|
|
45
566
|
|
|
46
|
-
|
|
47
|
-
|
|
567
|
+
if path.startswith(("/docs", "/openapi", "/__verge__")):
|
|
568
|
+
continue
|
|
48
569
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
570
|
+
for m in methods:
|
|
571
|
+
if m in ("GET", "POST", "PUT", "PATCH", "DELETE"):
|
|
572
|
+
REGISTERED_ROUTES.append({"path": path, "method": m})
|
|
52
573
|
|
|
53
|
-
|
|
574
|
+
except Exception as e:
|
|
575
|
+
print("❌ Error collecting route:", e)
|
|
54
576
|
|
|
55
|
-
|
|
56
|
-
# SUPER ADMIN TRIGGERED REGISTRATION
|
|
57
|
-
# ---------------------------------------
|
|
58
|
-
async def register_with_auth():
|
|
59
|
-
try:
|
|
60
|
-
async with httpx.AsyncClient(timeout=5) as client:
|
|
61
|
-
resp = await client.post(
|
|
62
|
-
AUTH_REGISTER_URL,
|
|
63
|
-
params={
|
|
64
|
-
"name": SERVICE_NAME,
|
|
65
|
-
"base_url": SERVICE_BASE_URL,
|
|
66
|
-
},
|
|
67
|
-
headers={"X-Verge-Service-Secret": VERGE_SECRET}
|
|
68
|
-
)
|
|
577
|
+
print("\n📡 Registering service with Verge Auth...")
|
|
69
578
|
|
|
70
|
-
|
|
71
|
-
|
|
579
|
+
async with httpx.AsyncClient() as client:
|
|
580
|
+
if AUTH_REGISTER_URL:
|
|
581
|
+
try:
|
|
582
|
+
resp = await _post_with_retries(
|
|
583
|
+
client,
|
|
584
|
+
AUTH_REGISTER_URL,
|
|
585
|
+
json={
|
|
586
|
+
"service_name": SERVICE_NAME,
|
|
587
|
+
"base_url": SERVICE_BASE_URL,
|
|
588
|
+
},
|
|
589
|
+
headers={
|
|
590
|
+
"X-Client-Id": CLIENT_ID or "",
|
|
591
|
+
"X-Client-Secret": CLIENT_SECRET or "",
|
|
592
|
+
"X-Verge-Service-Secret": VERGE_SERVICE_SECRET or "",
|
|
593
|
+
},
|
|
594
|
+
)
|
|
595
|
+
print(
|
|
596
|
+
"📡 Registration response:",
|
|
597
|
+
resp.status_code,
|
|
598
|
+
resp.text,
|
|
599
|
+
)
|
|
600
|
+
except Exception as e:
|
|
601
|
+
print("❌ Registration failed:", e)
|
|
72
602
|
|
|
73
|
-
|
|
74
|
-
|
|
603
|
+
if AUTH_ROUTE_SYNC_URL:
|
|
604
|
+
try:
|
|
605
|
+
resp = await _post_with_retries(
|
|
606
|
+
client,
|
|
607
|
+
AUTH_ROUTE_SYNC_URL,
|
|
608
|
+
json={
|
|
609
|
+
"service_name": SERVICE_NAME,
|
|
610
|
+
"base_url": SERVICE_BASE_URL,
|
|
611
|
+
"routes": REGISTERED_ROUTES,
|
|
612
|
+
},
|
|
613
|
+
headers={
|
|
614
|
+
"X-Client-Id": CLIENT_ID or "",
|
|
615
|
+
"X-Client-Secret": CLIENT_SECRET or "",
|
|
616
|
+
"X-Verge-Service-Secret": VERGE_SERVICE_SECRET or "",
|
|
617
|
+
},
|
|
618
|
+
timeout=20,
|
|
619
|
+
)
|
|
620
|
+
print(
|
|
621
|
+
"📡 Route sync response:",
|
|
622
|
+
resp.status_code,
|
|
623
|
+
resp.text,
|
|
624
|
+
)
|
|
625
|
+
except Exception as e:
|
|
626
|
+
print("❌ Route sync failed:", e)
|
|
75
627
|
|
|
76
|
-
#
|
|
77
|
-
# MIDDLEWARE
|
|
78
|
-
#
|
|
628
|
+
# -------------------------------------------------------
|
|
629
|
+
# CENTRAL AUTHZ MIDDLEWARE (FIXED FLOW)
|
|
630
|
+
# -------------------------------------------------------
|
|
79
631
|
@app.middleware("http")
|
|
80
632
|
async def central_auth(request: Request, call_next):
|
|
81
633
|
path = request.url.path
|
|
82
634
|
|
|
83
|
-
|
|
84
|
-
|
|
635
|
+
SKIP_PATHS = {
|
|
636
|
+
"/service-registry/register",
|
|
637
|
+
"/route-sync",
|
|
638
|
+
"/__verge__",
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
# Safer matching (handles trailing slash)
|
|
642
|
+
normalized_path = path.rstrip("/")
|
|
643
|
+
if normalized_path in SKIP_PATHS or path.startswith("/__verge__"):
|
|
85
644
|
return await call_next(request)
|
|
86
645
|
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
646
|
+
# ---------------------------------------------------
|
|
647
|
+
# STEP 1 — HANDLE AUTH CODE EXCHANGE (SERVICE-SIDE ONLY)
|
|
648
|
+
# ---------------------------------------------------
|
|
649
|
+
# NOTE: The auth frontend already exchanges codes via /auth/exchange
|
|
650
|
+
# This middleware only needs to handle codes coming from /services/launch
|
|
651
|
+
# which generates codes for microservice access
|
|
652
|
+
|
|
653
|
+
# code = request.query_params.get("code")
|
|
654
|
+
# print("code in request param", code)
|
|
655
|
+
|
|
656
|
+
# if code:
|
|
657
|
+
# # Check if we already have a valid token cookie
|
|
658
|
+
# existing_token = request.cookies.get("verge_access")
|
|
659
|
+
# print("existing token ", existing_token)
|
|
660
|
+
# if existing_token:
|
|
661
|
+
# # Already authenticated - just clean URL and proceed
|
|
662
|
+
# clean_url = str(request.url.remove_query_params("code"))
|
|
663
|
+
# print("clean_url RedirectResponse", clean_url)
|
|
664
|
+
# return RedirectResponse(clean_url, status_code=302)
|
|
665
|
+
|
|
666
|
+
# # Exchange code for token (from /services/launch flow)
|
|
667
|
+
# try:
|
|
668
|
+
# async with httpx.AsyncClient(timeout=30) as client:
|
|
669
|
+
# resp = await client.post(
|
|
670
|
+
# f"{AUTH_BASE_URL}/auth/exchange",
|
|
671
|
+
# json={"code": code},
|
|
672
|
+
# headers={
|
|
673
|
+
# "X-Client-Id": CLIENT_ID or "",
|
|
674
|
+
# "X-Client-Secret": CLIENT_SECRET or "",
|
|
675
|
+
# },
|
|
676
|
+
# )
|
|
677
|
+
# resp.raise_for_status()
|
|
678
|
+
# data = resp.json()
|
|
679
|
+
# token = data.get("access_token")
|
|
680
|
+
|
|
681
|
+
# if not token:
|
|
682
|
+
# return JSONResponse(
|
|
683
|
+
# {"detail": "Authorization failed: no token returned"},
|
|
684
|
+
# status_code=401,
|
|
685
|
+
# )
|
|
686
|
+
|
|
687
|
+
# # Set cookie with token and redirect to clean URL
|
|
688
|
+
# clean_url = str(request.url.remove_query_params("code"))
|
|
689
|
+
# response = RedirectResponse(clean_url, status_code=302)
|
|
690
|
+
# print("clean url when no existinig toke ", clean_url)
|
|
691
|
+
# response.set_cookie(
|
|
692
|
+
# key="verge_access",
|
|
693
|
+
# value=token,
|
|
694
|
+
# httponly=True,
|
|
695
|
+
# secure=is_request_secure(request),
|
|
696
|
+
# samesite="lax",
|
|
697
|
+
# path="/",
|
|
698
|
+
# max_age=28800, # 8 hours to match session
|
|
699
|
+
# )
|
|
700
|
+
|
|
701
|
+
# return response
|
|
702
|
+
|
|
703
|
+
# except httpx.HTTPStatusError as e:
|
|
704
|
+
# error_detail = e.response.text if hasattr(
|
|
705
|
+
# e.response, 'text') else str(e)
|
|
706
|
+
# print(f"❌ Code exchange failed: {error_detail}")
|
|
707
|
+
# return JSONResponse(
|
|
708
|
+
# {
|
|
709
|
+
# "detail": "Authorization code exchange failed",
|
|
710
|
+
# "error": error_detail,
|
|
711
|
+
# "status_code": e.response.status_code if hasattr(e.response, 'status_code') else 500
|
|
712
|
+
# },
|
|
713
|
+
# status_code=401,
|
|
714
|
+
# )
|
|
715
|
+
# except Exception as e:
|
|
716
|
+
# print(f"❌ Code exchange error: {str(e)}")
|
|
717
|
+
# return JSONResponse(
|
|
718
|
+
# {"detail": "Authorization failed", "error": str(e)},
|
|
719
|
+
# status_code=401,
|
|
720
|
+
# )
|
|
90
721
|
|
|
91
|
-
|
|
92
|
-
|
|
722
|
+
# ---------------------------------------------------
|
|
723
|
+
# STEP 2 — COLLECT TOKEN (cookie → header → session)
|
|
724
|
+
# ---------------------------------------------------
|
|
725
|
+
token = request.cookies.get("verge_access")
|
|
93
726
|
|
|
94
727
|
if not token:
|
|
95
|
-
|
|
728
|
+
auth_header = request.headers.get("authorization")
|
|
729
|
+
if auth_header and auth_header.lower().startswith("bearer "):
|
|
730
|
+
token = auth_header.split(" ", 1)[1].strip()
|
|
96
731
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
732
|
+
if not token and "session" in request.scope:
|
|
733
|
+
token = request.scope["session"].get("access_token")
|
|
734
|
+
|
|
735
|
+
# ---------------------------------------------------
|
|
736
|
+
# STEP 3 — REDIRECT TO LOGIN IF NO TOKEN
|
|
737
|
+
# ---------------------------------------------------
|
|
738
|
+
# add the paths that needs to be public paths ex "/redoc", "/docs"
|
|
739
|
+
_raw_public = os.getenv("PUBLIC_PATHS", "")
|
|
740
|
+
PUBLIC_PATHS = {
|
|
741
|
+
"/" + p.strip("/ ")
|
|
742
|
+
for p in _raw_public.split(",")
|
|
743
|
+
if p.strip()
|
|
744
|
+
}
|
|
102
745
|
|
|
103
746
|
if not token:
|
|
104
|
-
|
|
747
|
+
if normalized_path in PUBLIC_PATHS:
|
|
748
|
+
return await call_next(request)
|
|
749
|
+
|
|
750
|
+
# login_url = f"{os.getenv('AUTH_LOGIN_URL')}?redirect={request.url}"
|
|
751
|
+
login_url = f"{os.getenv('AUTH_LOGIN_URL')}?redirect_url={request.url}"
|
|
752
|
+
|
|
753
|
+
print("Login_Url ***", login_url)
|
|
105
754
|
|
|
106
|
-
|
|
755
|
+
return RedirectResponse(login_url, status_code=302)
|
|
756
|
+
|
|
757
|
+
# ---------------------------------------------------
|
|
758
|
+
# STEP 4 — LOCAL JWT VERIFICATION
|
|
759
|
+
# ---------------------------------------------------
|
|
107
760
|
try:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
761
|
+
if not JWT_PUBLIC_KEY:
|
|
762
|
+
await load_public_key(force=True)
|
|
763
|
+
|
|
764
|
+
if not JWT_PUBLIC_KEY:
|
|
765
|
+
return JSONResponse(
|
|
766
|
+
{"detail": "Auth key not ready, please try again"},
|
|
767
|
+
status_code=503,
|
|
116
768
|
)
|
|
117
|
-
data = res.json()
|
|
118
769
|
|
|
119
|
-
|
|
120
|
-
|
|
770
|
+
payload = jwt.decode(
|
|
771
|
+
token,
|
|
772
|
+
JWT_PUBLIC_KEY,
|
|
773
|
+
algorithms=JWT_ALGORITHMS,
|
|
774
|
+
options={"require": ["exp", "iat"]},
|
|
775
|
+
)
|
|
121
776
|
|
|
122
|
-
|
|
123
|
-
|
|
777
|
+
except jwt.ExpiredSignatureError:
|
|
778
|
+
# Clear expired cookie and redirect to login
|
|
779
|
+
response = RedirectResponse(
|
|
780
|
+
# f"{os.getenv('AUTH_LOGIN_URL')}?redirect={request.url}&reason=expired",
|
|
781
|
+
f"{os.getenv('AUTH_LOGIN_URL')}?redirect_url={request.url}&reason=expired",
|
|
782
|
+
status_code=302
|
|
783
|
+
)
|
|
784
|
+
response.delete_cookie("verge_access")
|
|
785
|
+
return response
|
|
124
786
|
|
|
125
|
-
|
|
126
|
-
|
|
787
|
+
except jwt.InvalidTokenError as e:
|
|
788
|
+
# Clear invalid cookie and redirect to login
|
|
789
|
+
response = RedirectResponse(
|
|
790
|
+
# f"{os.getenv('AUTH_LOGIN_URL')}?redirect={request.url}&reason=invalid",
|
|
791
|
+
f"{os.getenv('AUTH_LOGIN_URL')}?redirect_url={request.url}&reason=invalid",
|
|
792
|
+
status_code=302
|
|
793
|
+
)
|
|
794
|
+
response.delete_cookie("verge_access")
|
|
795
|
+
return response
|
|
796
|
+
|
|
797
|
+
except Exception as e:
|
|
798
|
+
return JSONResponse(
|
|
799
|
+
{"detail": "Token verification failed", "error": str(e)},
|
|
800
|
+
status_code=401,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
# ---------------------------------------------------
|
|
804
|
+
# STEP 5 — CENTRAL INTROSPECTION (Optional but recommended)
|
|
805
|
+
# ---------------------------------------------------
|
|
806
|
+
if INTROSPECT_URL:
|
|
807
|
+
try:
|
|
808
|
+
async with httpx.AsyncClient(timeout=5) as client:
|
|
809
|
+
resp = await client.post(
|
|
810
|
+
INTROSPECT_URL,
|
|
811
|
+
headers={
|
|
812
|
+
"Authorization": f"Bearer {token}",
|
|
813
|
+
"X-Client-Id": CLIENT_ID or "",
|
|
814
|
+
"X-Client-Secret": CLIENT_SECRET or "",
|
|
815
|
+
},
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
if resp.status_code == 200:
|
|
819
|
+
data = resp.json()
|
|
820
|
+
if not data.get("active"):
|
|
821
|
+
# Session revoked/inactive - clear cookie and redirect
|
|
822
|
+
response = RedirectResponse(
|
|
823
|
+
# f"{os.getenv('AUTH_LOGIN_URL')}?redirect={request.url}&reason=inactive",
|
|
824
|
+
f"{os.getenv('AUTH_LOGIN_URL')}?redirect_url={request.url}&reason=inactive",
|
|
825
|
+
status_code=302
|
|
826
|
+
)
|
|
827
|
+
response.delete_cookie("verge_access")
|
|
828
|
+
return response
|
|
829
|
+
|
|
830
|
+
request.state.introspect = data
|
|
831
|
+
else:
|
|
832
|
+
# Introspection failed - treat as unauthorized
|
|
833
|
+
response = RedirectResponse(
|
|
834
|
+
f"{os.getenv('AUTH_LOGIN_URL')}?redirect={request.url}",
|
|
835
|
+
status_code=302
|
|
836
|
+
)
|
|
837
|
+
response.delete_cookie("verge_access")
|
|
838
|
+
return response
|
|
839
|
+
|
|
840
|
+
except Exception as e:
|
|
841
|
+
# If introspection fails, log but continue with JWT validation
|
|
842
|
+
print(f"⚠️ Introspection failed: {e}")
|
|
843
|
+
# Could optionally fail open or closed here based on your security posture
|
|
127
844
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
845
|
+
# ---------------------------------------------------
|
|
846
|
+
# STEP 6 — PERMISSION CHECK
|
|
847
|
+
# ---------------------------------------------------
|
|
848
|
+
request.state.user = payload
|
|
849
|
+
permissions = payload.get("roles") or []
|
|
850
|
+
|
|
851
|
+
route_obj = request.scope.get("route")
|
|
852
|
+
route_path = route_obj.path if route_obj else path
|
|
853
|
+
method = request.method
|
|
854
|
+
|
|
855
|
+
# # Allow docs/redoc after authentication
|
|
856
|
+
# if normalized_path in {"/redoc", "/docs"}:
|
|
857
|
+
# return await call_next(request)
|
|
858
|
+
|
|
859
|
+
if normalized_path in PUBLIC_PATHS:
|
|
860
|
+
return await call_next(request)
|
|
861
|
+
|
|
862
|
+
# Build permission key: service_name:route_path:method
|
|
863
|
+
required_key = f"{SERVICE_NAME}:{route_path}:{method}".lower()
|
|
864
|
+
normalized_permissions = [p.lower() for p in permissions]
|
|
865
|
+
|
|
866
|
+
if required_key not in normalized_permissions:
|
|
867
|
+
return JSONResponse(
|
|
868
|
+
{
|
|
869
|
+
"detail": "Insufficient permissions. Contact admin for access.",
|
|
870
|
+
"required": required_key,
|
|
871
|
+
"user_permissions": permissions
|
|
872
|
+
},
|
|
873
|
+
status_code=403,
|
|
874
|
+
)
|
|
134
875
|
|
|
876
|
+
# ---------------------------------------------------
|
|
877
|
+
# STEP 7 — PROCEED TO ROUTE HANDLER
|
|
878
|
+
# ---------------------------------------------------
|
|
135
879
|
return await call_next(request)
|
verge_auth_sdk/verge_routes.py
CHANGED
|
@@ -10,8 +10,8 @@ async def verge_internal_routes(request: Request):
|
|
|
10
10
|
expected_secret = get_secret("VERGE_SERVICE_SECRET")
|
|
11
11
|
received_secret = request.headers.get("X-Verge-Service-Secret")
|
|
12
12
|
|
|
13
|
-
# Enforce exact secret match
|
|
14
13
|
if not expected_secret or expected_secret != received_secret:
|
|
14
|
+
|
|
15
15
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
16
16
|
|
|
17
17
|
collected = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: verge-auth-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.48
|
|
4
4
|
Summary: Secure centralized authentication SDK for FastAPI microservices
|
|
5
5
|
Author-email: Verge Infosoft <contactus@vergeinfosoft.com>
|
|
6
6
|
License: MIT
|
|
@@ -14,7 +14,8 @@ Requires-Dist: python-dotenv
|
|
|
14
14
|
Dynamic: license-file
|
|
15
15
|
|
|
16
16
|
🔐 Verge Auth SDK
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
Secure Identity & Access Management for FastAPI Microservices and all Python-based frameworks Monolithic and Microservice Architectures
|
|
18
19
|
|
|
19
20
|
Verge Auth SDK is a lightweight integration library that connects your FastAPI microservices to the Verge Auth Platform — a centralized identity, role management, and access-control system built for modern SaaS applications.
|
|
20
21
|
|
|
@@ -53,8 +54,8 @@ Users sign up with their organization details, company domain, and email.
|
|
|
53
54
|
|
|
54
55
|
2. Email Verification
|
|
55
56
|
|
|
56
|
-
A verification
|
|
57
|
-
|
|
57
|
+
A verification email is sent to the registered address.
|
|
58
|
+
|
|
58
59
|
Once verified, the user is redirected to the Verge Auth platform.
|
|
59
60
|
|
|
60
61
|
3. Login
|
|
@@ -155,6 +156,7 @@ Add users into the group
|
|
|
155
156
|
This makes onboarding smoother and keeps role management scalable.
|
|
156
157
|
|
|
157
158
|
🔌 Integrating the SDK Into a Microservice
|
|
159
|
+
|
|
158
160
|
Install from PyPI
|
|
159
161
|
pip install verge_auth_sdk
|
|
160
162
|
|
|
@@ -163,6 +165,8 @@ from fastapi import FastAPI
|
|
|
163
165
|
from verge_auth_sdk import add_central_auth
|
|
164
166
|
|
|
165
167
|
app = FastAPI()
|
|
168
|
+
|
|
169
|
+
# call this at the last line of your apps main
|
|
166
170
|
add_central_auth(app)
|
|
167
171
|
|
|
168
172
|
That’s it.
|
|
@@ -171,30 +175,55 @@ The service will now:
|
|
|
171
175
|
✓ Authenticate incoming requests
|
|
172
176
|
✓ Communicate securely with Verge Auth
|
|
173
177
|
✓ Provide user identity + roles
|
|
174
|
-
✓
|
|
178
|
+
✓ Secure synchronization of service access metadata for centralized permission governance.
|
|
175
179
|
|
|
176
180
|
⚙ Environment Configuration
|
|
177
181
|
|
|
178
182
|
Each service requires a minimal set of environment variables:
|
|
183
|
+
Exact endpoint configurations and integration details may vary by deployment and are abstracted by the SDK.
|
|
184
|
+
|
|
185
|
+
############## DO NOT CHANGE THIS #################################
|
|
179
186
|
|
|
180
|
-
|
|
187
|
+
AUTH_BASE_URL=https://auth.vergeinfosoft.com
|
|
188
|
+
AUTH_SESSION_URL=https://auth.vergeinfosoft.com/session
|
|
189
|
+
AUTH_INTROSPECT_URL=https://auth.vergeinfosoft.com/introspect
|
|
190
|
+
AUTH_REGISTER_URL=https://auth.vergeinfosoft.com/service-registry/register
|
|
191
|
+
AUTH_ROUTE_SYNC_URL=https://auth.vergeinfosoft.com/route-sync
|
|
192
|
+
AUTH_PUBLIC_KEY_URL=https://auth.vergeinfosoft.com/auth/keys/public
|
|
193
|
+
AUTH_LOGIN_URL=https://auth.vergeinfosoft.com/login
|
|
181
194
|
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
############## DO NOT CHANGE THIS #################################
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
################# CHANGE THESE AS PER DETAILS PROVIDED #############################################
|
|
184
199
|
|
|
185
200
|
VERGE_CLIENT_ID=<client-id>
|
|
186
201
|
VERGE_CLIENT_SECRET=<client-secret>
|
|
187
|
-
|
|
188
202
|
VERGE_SERVICE_SECRET=<service-integration-secret>
|
|
189
|
-
|
|
190
203
|
# These are provided by Verge Infosoft during onboarding.
|
|
191
204
|
|
|
192
|
-
|
|
205
|
+
####################################################################################################
|
|
193
206
|
|
|
194
|
-
|
|
207
|
+
|
|
208
|
+
# Select Optional secret provider:
|
|
209
|
+
|
|
210
|
+
SECRETS_PROVIDER=env | AZURE | AWS | GCP | ORACLE # Supported cloud providers for secret management
|
|
211
|
+
|
|
212
|
+
env=env # if you want to load from your local ENV
|
|
213
|
+
azure=<AZURE_URL>
|
|
214
|
+
aws=<AWS_URL>
|
|
215
|
+
gcp=<GCP_URL>
|
|
216
|
+
oracle=<ORACLE_URL>
|
|
217
|
+
|
|
218
|
+
########################################################################
|
|
219
|
+
|
|
220
|
+
SERVICE_NAME=<SERVICE_NAME> # example billing service or hr service
|
|
221
|
+
SERVICE_BASE_URL=<SERVICE_BASE_URL> example https://hr.yourdomain.com
|
|
195
222
|
|
|
196
223
|
########################################################################
|
|
197
224
|
|
|
225
|
+
|
|
226
|
+
|
|
198
227
|
🛡 Middleware Responsibilities
|
|
199
228
|
|
|
200
229
|
The SDK transparently handles:
|
|
@@ -215,7 +244,7 @@ You do not need to implement any auth or RBAC logic manually.
|
|
|
215
244
|
|
|
216
245
|
🔐 Security Highlights
|
|
217
246
|
|
|
218
|
-
|
|
247
|
+
Industry-standard asymmetric token verification with key rotation support
|
|
219
248
|
|
|
220
249
|
Centralized session & token lifecycle management
|
|
221
250
|
|
|
@@ -227,7 +256,7 @@ HTTPS-only communication
|
|
|
227
256
|
|
|
228
257
|
Support for cloud key vaults
|
|
229
258
|
|
|
230
|
-
💼 Ideal For
|
|
259
|
+
💼 Ideal For (including but not limited to):
|
|
231
260
|
|
|
232
261
|
HRMS, ERP, CRM, Billing platforms
|
|
233
262
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
verge_auth_sdk/__init__.py,sha256=fna_P4l-tc3ZLKwERFEdOpT4hqPQoJSikrbt_ze5yME,410
|
|
2
|
+
verge_auth_sdk/middleware.py,sha256=MJdukBVCMAwZbBcnHY-viccyc5TsUbX4Mbdp13at1MM,34019
|
|
3
|
+
verge_auth_sdk/secret_provider.py,sha256=j89rj9LZHl4qTI2fdf0qnn-mgDD3oTbdP3imsm0S9n8,1653
|
|
4
|
+
verge_auth_sdk/verge_routes.py,sha256=GAFS8HdSNznuh6two6DAHZZeq2t29l3IlzDJjBcOGKA,1413
|
|
5
|
+
verge_auth_sdk-0.1.48.dist-info/licenses/LICENSE,sha256=OLuFd1VUvl1QKIJJ_qJ8zR4ypcDhzeRkEywc7PX2ABQ,1090
|
|
6
|
+
verge_auth_sdk-0.1.48.dist-info/METADATA,sha256=H4XiQRVtQWbmsynBw4AT_dG7Zw3dwqyAIcXqE-VpmeE,7075
|
|
7
|
+
verge_auth_sdk-0.1.48.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
verge_auth_sdk-0.1.48.dist-info/top_level.txt,sha256=0ik2K2CJZrP11DvuR7USTYJkX_kv2DSJS5wyhP1yAfA,15
|
|
9
|
+
verge_auth_sdk-0.1.48.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
verge_auth_sdk/__init__.py,sha256=n76KSaK3CohCP7dzrUXWlTPNPGzWesLTlYl-C7rfW60,411
|
|
2
|
-
verge_auth_sdk/middleware.py,sha256=2WKXMgW7mxaHbEyE1g8qpTM0SAcuXs4XVnB5xr5KEKs,4707
|
|
3
|
-
verge_auth_sdk/secret_provider.py,sha256=j89rj9LZHl4qTI2fdf0qnn-mgDD3oTbdP3imsm0S9n8,1653
|
|
4
|
-
verge_auth_sdk/verge_routes.py,sha256=R0WUb0OyIkapZ0dKhqfYzfhI6I_cnHSJsB6p-8lt9V4,1445
|
|
5
|
-
verge_auth_sdk-0.1.14.dist-info/licenses/LICENSE,sha256=OLuFd1VUvl1QKIJJ_qJ8zR4ypcDhzeRkEywc7PX2ABQ,1090
|
|
6
|
-
verge_auth_sdk-0.1.14.dist-info/METADATA,sha256=FqrFVVuW2xS5mSWo9xGRWR_nT3IFvVxNiG6pgcPuIwA,5702
|
|
7
|
-
verge_auth_sdk-0.1.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
verge_auth_sdk-0.1.14.dist-info/top_level.txt,sha256=0ik2K2CJZrP11DvuR7USTYJkX_kv2DSJS5wyhP1yAfA,15
|
|
9
|
-
verge_auth_sdk-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|