mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 22:14:46 +00:00
feat(pat): authenticate personal access tokens
handle_auth resolves a dgpt_pat_ bearer against the database instead of decoding it as a JWT, for both the Flask and the ASGI routes. Scopes and the resource filter always come from the token row, and the claims that mark a PAT are stripped from decoded JWTs so a session token cannot pose as one. ASGI routes reject tokens unless they name the scope that admits them, and /api/user/me reports what the calling token may do.
This commit is contained in:
1 parent
22ecc0aee3
commit
174130607a
8 files changed
+604
-3
No files matched your search
@@ -16,27 +16,43 @@ from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from docsgpt.api.oidc.denylist import is_denied as oidc_session_denied
|
||||
from docsgpt.api.pat.tokens import is_pat
|
||||
from docsgpt.auth import handle_auth
|
||||
from docsgpt.core import log_context
|
||||
from docsgpt.core.settings import settings
|
||||
|
||||
|
||||
async def authenticate(request: Request) -> Tuple[Optional[dict], Optional[JSONResponse]]:
|
||||
async def authenticate(
|
||||
request: Request, *, pat_scope: Optional[str] = None
|
||||
) -> Tuple[Optional[dict], Optional[JSONResponse]]:
|
||||
"""Decode the caller's JWT the way Flask's ``authenticate_request`` does.
|
||||
|
||||
Args:
|
||||
request: The incoming Starlette request.
|
||||
pat_scope: Scope a personal access token needs for this route. Left
|
||||
unset, the route rejects PATs outright (deny by default, matching
|
||||
the Flask rule table in ``docsgpt/api/pat/rules.py``).
|
||||
|
||||
Returns:
|
||||
tuple: ``(claims, None)`` for an authenticated caller, ``(None, None)``
|
||||
when no token was sent (the route decides whether that is allowed), or
|
||||
``(None, response)`` carrying the 401 to return.
|
||||
"""
|
||||
decoded = handle_auth(request)
|
||||
# A personal access token resolves against Postgres; keep that sync read off the event loop.
|
||||
decoded = await anyio.to_thread.run_sync(handle_auth, request)
|
||||
if not decoded:
|
||||
return None, None
|
||||
if "error" in decoded:
|
||||
return None, JSONResponse(decoded, status_code=401)
|
||||
if is_pat(decoded):
|
||||
# A PAT lookup already excludes revoked tokens and deactivated users,
|
||||
# so the session denylist below does not apply to it.
|
||||
if pat_scope is None or pat_scope not in (decoded.get("scopes") or []):
|
||||
return None, JSONResponse(
|
||||
{"success": False, "message": "Token lacks the required scope", "error": "insufficient_scope"},
|
||||
status_code=403,
|
||||
)
|
||||
return decoded, None
|
||||
# The denylist is a sync Redis read; keep it off the event loop.
|
||||
if settings.AUTH_TYPE == "oidc" and await anyio.to_thread.run_sync(oidc_session_denied, decoded):
|
||||
return None, JSONResponse(
|
||||
|
||||
Reference in new issue
Block a user