Spaces:
Running
Running
Commit ·
8604693
1
Parent(s): 870ca8d
ok
Browse files- app/api/server.py +27 -2
- app/api/v1/router.py +2 -1
- app/api/v1/token_generator.py +90 -0
- app/config.py +5 -0
- app/models/schemas.py +40 -0
- app/services/jwt_service.py +161 -0
- requirements.txt +1 -0
app/api/server.py
CHANGED
|
@@ -89,8 +89,33 @@ def create_application() -> FastAPI:
|
|
| 89 |
app.include_router(api_v1_router, prefix="/api/v1")
|
| 90 |
|
| 91 |
@app.get("/", include_in_schema=False)
|
| 92 |
-
async def root():
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
@app.get("/health", include_in_schema=False)
|
| 96 |
async def root_health():
|
|
|
|
| 89 |
app.include_router(api_v1_router, prefix="/api/v1")
|
| 90 |
|
| 91 |
@app.get("/", include_in_schema=False)
|
| 92 |
+
async def root(request: Request):
|
| 93 |
+
from collections import defaultdict
|
| 94 |
+
routes_by_tag: dict[str, list[dict]] = defaultdict(list)
|
| 95 |
+
for route in app.routes:
|
| 96 |
+
if not hasattr(route, "methods") or not hasattr(route, "path"):
|
| 97 |
+
continue
|
| 98 |
+
if route.path in ("/", "/health", "/ping", "/openapi.json", "/docs", "/redoc", "/docs/oauth2-redirect"):
|
| 99 |
+
continue
|
| 100 |
+
tags = getattr(route, "tags", None) or ["default"]
|
| 101 |
+
for tag in tags:
|
| 102 |
+
routes_by_tag[tag].append({
|
| 103 |
+
"method": list(route.methods - {"HEAD", "OPTIONS"}),
|
| 104 |
+
"path": route.path,
|
| 105 |
+
"summary": getattr(route, "summary", ""),
|
| 106 |
+
})
|
| 107 |
+
return {
|
| 108 |
+
"name": _settings.app_name,
|
| 109 |
+
"version": _settings.app_version,
|
| 110 |
+
"docs": {
|
| 111 |
+
"swagger": str(request.base_url) + "docs",
|
| 112 |
+
"redoc": str(request.base_url) + "redoc",
|
| 113 |
+
},
|
| 114 |
+
"endpoints": [
|
| 115 |
+
{"tag": tag, "routes": sorted(routes, key=lambda r: r["path"])}
|
| 116 |
+
for tag, routes in sorted(routes_by_tag.items())
|
| 117 |
+
],
|
| 118 |
+
}
|
| 119 |
|
| 120 |
@app.get("/health", include_in_schema=False)
|
| 121 |
async def root_health():
|
app/api/v1/router.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
from fastapi import APIRouter
|
| 4 |
|
| 5 |
-
from app.api.v1 import batch, chat, code_executor, convert, database, embeddings, reconcile, scraper, semantic_router, sql_validator, system, token_counter, web_search
|
| 6 |
from app.api.verify import router as verify_router
|
| 7 |
|
| 8 |
api_v1_router = APIRouter()
|
|
@@ -19,4 +19,5 @@ api_v1_router.include_router(web_search.router, tags=["Web Search"])
|
|
| 19 |
api_v1_router.include_router(sql_validator.router, tags=["SQL Validator"])
|
| 20 |
api_v1_router.include_router(semantic_router.router, tags=["Semantic Router"])
|
| 21 |
api_v1_router.include_router(token_counter.router, tags=["Token Counter"])
|
|
|
|
| 22 |
api_v1_router.include_router(chat.router, tags=["Chat"])
|
|
|
|
| 2 |
|
| 3 |
from fastapi import APIRouter
|
| 4 |
|
| 5 |
+
from app.api.v1 import batch, chat, code_executor, convert, database, embeddings, reconcile, scraper, semantic_router, sql_validator, system, token_counter, token_generator, web_search
|
| 6 |
from app.api.verify import router as verify_router
|
| 7 |
|
| 8 |
api_v1_router = APIRouter()
|
|
|
|
| 19 |
api_v1_router.include_router(sql_validator.router, tags=["SQL Validator"])
|
| 20 |
api_v1_router.include_router(semantic_router.router, tags=["Semantic Router"])
|
| 21 |
api_v1_router.include_router(token_counter.router, tags=["Token Counter"])
|
| 22 |
+
api_v1_router.include_router(token_generator.router, tags=["Token Generator"])
|
| 23 |
api_v1_router.include_router(chat.router, tags=["Chat"])
|
app/api/v1/token_generator.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, Depends
|
| 6 |
+
|
| 7 |
+
from app.api.deps import require_auth
|
| 8 |
+
from app.models.schemas import (
|
| 9 |
+
TokenGenerateRequest,
|
| 10 |
+
TokenGenerateResponse,
|
| 11 |
+
TokenValidateRequest,
|
| 12 |
+
TokenValidateResponse,
|
| 13 |
+
)
|
| 14 |
+
from app.services.jwt_service import JWTService
|
| 15 |
+
|
| 16 |
+
router = APIRouter()
|
| 17 |
+
_service = JWTService()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@router.post(
|
| 21 |
+
"/token/generate",
|
| 22 |
+
response_model=TokenGenerateResponse,
|
| 23 |
+
summary="Generate a signed JWT token with custom claims",
|
| 24 |
+
)
|
| 25 |
+
async def generate_token(
|
| 26 |
+
body: TokenGenerateRequest,
|
| 27 |
+
auth: str = Depends(require_auth),
|
| 28 |
+
) -> TokenGenerateResponse:
|
| 29 |
+
start = time.perf_counter()
|
| 30 |
+
try:
|
| 31 |
+
result = _service.generate(
|
| 32 |
+
subject=body.subject,
|
| 33 |
+
role=body.role,
|
| 34 |
+
permissions=body.permissions,
|
| 35 |
+
issuer=body.issuer,
|
| 36 |
+
audience=body.audience,
|
| 37 |
+
expiry_minutes=body.expiry_minutes,
|
| 38 |
+
not_before_minutes=body.not_before_minutes,
|
| 39 |
+
extra_claims=body.extra_claims,
|
| 40 |
+
secret=body.secret,
|
| 41 |
+
algorithm=body.algorithm,
|
| 42 |
+
)
|
| 43 |
+
elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
|
| 44 |
+
return TokenGenerateResponse(
|
| 45 |
+
success=True,
|
| 46 |
+
time_ms=elapsed_ms,
|
| 47 |
+
token=result["token"],
|
| 48 |
+
claims=result["claims"],
|
| 49 |
+
expires_at=result["expires_at"],
|
| 50 |
+
valid_for=result.get("valid_for"),
|
| 51 |
+
secret=result.get("secret"),
|
| 52 |
+
algorithm=result["algorithm"],
|
| 53 |
+
)
|
| 54 |
+
except Exception as exc:
|
| 55 |
+
elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
|
| 56 |
+
return TokenGenerateResponse(
|
| 57 |
+
success=False,
|
| 58 |
+
time_ms=elapsed_ms,
|
| 59 |
+
error=str(exc),
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@router.post(
|
| 64 |
+
"/token/validate",
|
| 65 |
+
response_model=TokenValidateResponse,
|
| 66 |
+
summary="Validate a JWT token and return its claims",
|
| 67 |
+
)
|
| 68 |
+
async def validate_token(
|
| 69 |
+
body: TokenValidateRequest,
|
| 70 |
+
auth: str = Depends(require_auth),
|
| 71 |
+
) -> TokenValidateResponse:
|
| 72 |
+
start = time.perf_counter()
|
| 73 |
+
try:
|
| 74 |
+
result = _service.validate(body.token, body.audience, body.secret, body.algorithm)
|
| 75 |
+
elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
|
| 76 |
+
return TokenValidateResponse(
|
| 77 |
+
success=True,
|
| 78 |
+
time_ms=elapsed_ms,
|
| 79 |
+
valid=result["valid"],
|
| 80 |
+
claims=result["claims"],
|
| 81 |
+
error=result["error"],
|
| 82 |
+
)
|
| 83 |
+
except Exception as exc:
|
| 84 |
+
elapsed_ms = round((time.perf_counter() - start) * 1000, 3)
|
| 85 |
+
return TokenValidateResponse(
|
| 86 |
+
success=False,
|
| 87 |
+
time_ms=elapsed_ms,
|
| 88 |
+
valid=False,
|
| 89 |
+
error=str(exc),
|
| 90 |
+
)
|
app/config.py
CHANGED
|
@@ -61,6 +61,11 @@ class Settings(BaseSettings):
|
|
| 61 |
key_lock_ttl: int = 600
|
| 62 |
rounds_per_model: int = 50
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
@property
|
| 65 |
def max_upload_mb(self) -> int:
|
| 66 |
return self.max_upload_bytes // (1024 * 1024)
|
|
|
|
| 61 |
key_lock_ttl: int = 600
|
| 62 |
rounds_per_model: int = 50
|
| 63 |
|
| 64 |
+
jwt_secret_key: str = "changeme-jwt-secret"
|
| 65 |
+
jwt_algorithm: str = "HS256"
|
| 66 |
+
jwt_default_expiry_minutes: int = 30
|
| 67 |
+
jwt_issuer: str = "all-api-collection"
|
| 68 |
+
|
| 69 |
@property
|
| 70 |
def max_upload_mb(self) -> int:
|
| 71 |
return self.max_upload_bytes // (1024 * 1024)
|
app/models/schemas.py
CHANGED
|
@@ -468,6 +468,46 @@ class WebSearchEngineDescriptionsResponse(BaseModel):
|
|
| 468 |
error: Optional[str] = None
|
| 469 |
|
| 470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
class SqlValidationRequest(BaseModel):
|
| 472 |
query: str = Field(..., min_length=1, max_length=100000, description="SQL query string to validate")
|
| 473 |
dialect: Optional[str] = Field(None, description="SQL dialect (mysql, postgres, bigquery, snowflake, sqlite, etc.)")
|
|
|
|
| 468 |
error: Optional[str] = None
|
| 469 |
|
| 470 |
|
| 471 |
+
class TokenGenerateRequest(BaseModel):
|
| 472 |
+
subject: str = Field(..., min_length=1, max_length=256, description="Token subject (user ID, client ID, etc.)")
|
| 473 |
+
role: Optional[str] = Field(None, max_length=64, description="User role for RBAC")
|
| 474 |
+
permissions: Optional[List[str]] = Field(None, description="List of permission strings")
|
| 475 |
+
issuer: Optional[str] = Field(None, max_length=128, description="Token issuer (overrides default)")
|
| 476 |
+
audience: Optional[str] = Field(None, max_length=128, description="Token audience")
|
| 477 |
+
expiry_minutes: Optional[int] = Field(None, ge=1, le=525600, description="Token lifetime in minutes")
|
| 478 |
+
not_before_minutes: int = Field(default=0, ge=0, le=525600, description="Delay token validity by N minutes")
|
| 479 |
+
extra_claims: Optional[Dict[str, Any]] = Field(None, description="Additional custom claims")
|
| 480 |
+
secret: Optional[str] = Field(None, description="JWT signing secret (auto-generated if not provided)")
|
| 481 |
+
algorithm: Optional[str] = Field(None, pattern="^(HS256|HS384|HS512)$", description="JWT signing algorithm (defaults to HS256)")
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
class TokenGenerateResponse(BaseModel):
|
| 485 |
+
success: bool
|
| 486 |
+
time_ms: float
|
| 487 |
+
token: Optional[str] = None
|
| 488 |
+
claims: Optional[Dict[str, Any]] = None
|
| 489 |
+
expires_at: Optional[str] = None
|
| 490 |
+
valid_for: Optional[str] = None
|
| 491 |
+
secret: Optional[str] = None
|
| 492 |
+
algorithm: Optional[str] = None
|
| 493 |
+
error: Optional[str] = None
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
class TokenValidateRequest(BaseModel):
|
| 497 |
+
token: str = Field(..., min_length=1, description="JWT token string to validate")
|
| 498 |
+
audience: Optional[str] = Field(None, description="Expected audience to verify against")
|
| 499 |
+
secret: Optional[str] = Field(None, description="Signing secret used during generation (uses server default if not provided)")
|
| 500 |
+
algorithm: Optional[str] = Field(None, pattern="^(HS256|HS384|HS512)$", description="Algorithm used during generation (uses server default if not provided)")
|
| 501 |
+
|
| 502 |
+
|
| 503 |
+
class TokenValidateResponse(BaseModel):
|
| 504 |
+
success: bool
|
| 505 |
+
time_ms: float
|
| 506 |
+
valid: bool
|
| 507 |
+
claims: Optional[Dict[str, Any]] = None
|
| 508 |
+
error: Optional[str] = None
|
| 509 |
+
|
| 510 |
+
|
| 511 |
class SqlValidationRequest(BaseModel):
|
| 512 |
query: str = Field(..., min_length=1, max_length=100000, description="SQL query string to validate")
|
| 513 |
dialect: Optional[str] = Field(None, description="SQL dialect (mysql, postgres, bigquery, snowflake, sqlite, etc.)")
|
app/services/jwt_service.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import secrets
|
| 4 |
+
import uuid
|
| 5 |
+
from datetime import datetime, timedelta, timezone
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
|
| 8 |
+
import jwt
|
| 9 |
+
from jwt import PyJWTError
|
| 10 |
+
|
| 11 |
+
from app.config import get_settings
|
| 12 |
+
|
| 13 |
+
_SUPPORTED_ALGORITHMS = frozenset({"HS256", "HS384", "HS512"})
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class JWTService:
|
| 17 |
+
def __init__(self) -> None:
|
| 18 |
+
self._settings = get_settings()
|
| 19 |
+
|
| 20 |
+
@staticmethod
|
| 21 |
+
def _generate_secret() -> str:
|
| 22 |
+
return secrets.token_hex(32)
|
| 23 |
+
|
| 24 |
+
def generate(
|
| 25 |
+
self,
|
| 26 |
+
*,
|
| 27 |
+
subject: str,
|
| 28 |
+
role: Optional[str] = None,
|
| 29 |
+
permissions: Optional[list[str]] = None,
|
| 30 |
+
issuer: Optional[str] = None,
|
| 31 |
+
audience: Optional[str] = None,
|
| 32 |
+
expiry_minutes: Optional[int] = None,
|
| 33 |
+
not_before_minutes: int = 0,
|
| 34 |
+
extra_claims: Optional[dict[str, Any]] = None,
|
| 35 |
+
secret: Optional[str] = None,
|
| 36 |
+
algorithm: Optional[str] = None,
|
| 37 |
+
) -> dict[str, Any]:
|
| 38 |
+
now = datetime.now(timezone.utc)
|
| 39 |
+
expire_min = expiry_minutes if expiry_minutes is not None else self._settings.jwt_default_expiry_minutes
|
| 40 |
+
|
| 41 |
+
payload: dict[str, Any] = {
|
| 42 |
+
"sub": subject,
|
| 43 |
+
"iat": now,
|
| 44 |
+
"exp": now + timedelta(minutes=expire_min),
|
| 45 |
+
"jti": uuid.uuid4().hex,
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if issuer:
|
| 49 |
+
payload["iss"] = issuer
|
| 50 |
+
else:
|
| 51 |
+
payload["iss"] = self._settings.jwt_issuer
|
| 52 |
+
|
| 53 |
+
if audience:
|
| 54 |
+
payload["aud"] = audience
|
| 55 |
+
|
| 56 |
+
if role:
|
| 57 |
+
payload["role"] = role
|
| 58 |
+
|
| 59 |
+
if permissions:
|
| 60 |
+
payload["permissions"] = permissions
|
| 61 |
+
|
| 62 |
+
if not_before_minutes > 0:
|
| 63 |
+
payload["nbf"] = now + timedelta(minutes=not_before_minutes)
|
| 64 |
+
|
| 65 |
+
if extra_claims:
|
| 66 |
+
payload.update(extra_claims)
|
| 67 |
+
|
| 68 |
+
used_secret = secret if secret else self._generate_secret()
|
| 69 |
+
used_algorithm = algorithm if algorithm else self._settings.jwt_algorithm
|
| 70 |
+
|
| 71 |
+
if used_algorithm not in _SUPPORTED_ALGORITHMS:
|
| 72 |
+
raise ValueError(f"Unsupported algorithm '{used_algorithm}'. Supported: {sorted(_SUPPORTED_ALGORITHMS)}")
|
| 73 |
+
|
| 74 |
+
token = jwt.encode(payload, used_secret, algorithm=used_algorithm)
|
| 75 |
+
|
| 76 |
+
valid_minutes = expire_min
|
| 77 |
+
if valid_minutes < 60:
|
| 78 |
+
valid_for = f"{valid_minutes} minute{'s' if valid_minutes != 1 else ''}"
|
| 79 |
+
elif valid_minutes < 1440:
|
| 80 |
+
hours = valid_minutes // 60
|
| 81 |
+
mins = valid_minutes % 60
|
| 82 |
+
valid_for = f"{hours} hour{'s' if hours != 1 else ''}"
|
| 83 |
+
if mins:
|
| 84 |
+
valid_for += f" {mins} minute{'s' if mins != 1 else ''}"
|
| 85 |
+
else:
|
| 86 |
+
days = valid_minutes // 1440
|
| 87 |
+
hrs = (valid_minutes % 1440) // 60
|
| 88 |
+
valid_for = f"{days} day{'s' if days != 1 else ''}"
|
| 89 |
+
if hrs:
|
| 90 |
+
valid_for += f" {hrs} hour{'s' if hrs != 1 else ''}"
|
| 91 |
+
|
| 92 |
+
return {
|
| 93 |
+
"token": token,
|
| 94 |
+
"claims": {
|
| 95 |
+
"sub": subject,
|
| 96 |
+
"iss": payload["iss"],
|
| 97 |
+
"aud": audience,
|
| 98 |
+
"role": role,
|
| 99 |
+
"permissions": permissions,
|
| 100 |
+
"jti": payload["jti"],
|
| 101 |
+
"iat": payload["iat"].isoformat(),
|
| 102 |
+
"exp": payload["exp"].isoformat(),
|
| 103 |
+
"nbf": payload.get("nbf").isoformat() if payload.get("nbf") else None,
|
| 104 |
+
},
|
| 105 |
+
"expires_at": payload["exp"].isoformat(),
|
| 106 |
+
"valid_for": valid_for,
|
| 107 |
+
"secret": used_secret,
|
| 108 |
+
"algorithm": used_algorithm,
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
def validate(
|
| 112 |
+
self,
|
| 113 |
+
token: str,
|
| 114 |
+
audience: Optional[str] = None,
|
| 115 |
+
secret: Optional[str] = None,
|
| 116 |
+
algorithm: Optional[str] = None,
|
| 117 |
+
) -> dict[str, Any]:
|
| 118 |
+
used_secret = secret if secret else self._settings.jwt_secret_key
|
| 119 |
+
used_algorithms = [algorithm] if algorithm else [self._settings.jwt_algorithm]
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
payload = jwt.decode(
|
| 123 |
+
token,
|
| 124 |
+
used_secret,
|
| 125 |
+
algorithms=used_algorithms,
|
| 126 |
+
audience=audience,
|
| 127 |
+
issuer=self._settings.jwt_issuer,
|
| 128 |
+
options={
|
| 129 |
+
"require": ["sub", "exp", "iat", "jti"],
|
| 130 |
+
"verify_signature": True,
|
| 131 |
+
"verify_exp": True,
|
| 132 |
+
"verify_iat": True,
|
| 133 |
+
"verify_aud": audience is not None,
|
| 134 |
+
"verify_iss": True,
|
| 135 |
+
},
|
| 136 |
+
)
|
| 137 |
+
return {
|
| 138 |
+
"valid": True,
|
| 139 |
+
"claims": {
|
| 140 |
+
"sub": payload.get("sub"),
|
| 141 |
+
"iss": payload.get("iss"),
|
| 142 |
+
"aud": payload.get("aud"),
|
| 143 |
+
"role": payload.get("role"),
|
| 144 |
+
"permissions": payload.get("permissions"),
|
| 145 |
+
"jti": payload.get("jti"),
|
| 146 |
+
"iat": datetime.fromtimestamp(payload["iat"], tz=timezone.utc).isoformat() if payload.get("iat") else None,
|
| 147 |
+
"exp": datetime.fromtimestamp(payload["exp"], tz=timezone.utc).isoformat() if payload.get("exp") else None,
|
| 148 |
+
"nbf": datetime.fromtimestamp(payload["nbf"], tz=timezone.utc).isoformat() if payload.get("nbf") else None,
|
| 149 |
+
},
|
| 150 |
+
"error": None,
|
| 151 |
+
}
|
| 152 |
+
except jwt.ExpiredSignatureError:
|
| 153 |
+
return {"valid": False, "claims": None, "error": "Token has expired."}
|
| 154 |
+
except jwt.InvalidAudienceError:
|
| 155 |
+
return {"valid": False, "claims": None, "error": "Token audience does not match."}
|
| 156 |
+
except jwt.InvalidIssuerError:
|
| 157 |
+
return {"valid": False, "claims": None, "error": "Token issuer does not match."}
|
| 158 |
+
except jwt.InvalidTokenError as exc:
|
| 159 |
+
return {"valid": False, "claims": None, "error": f"Invalid token: {exc}"}
|
| 160 |
+
except PyJWTError as exc:
|
| 161 |
+
return {"valid": False, "claims": None, "error": f"JWT validation error: {exc}"}
|
requirements.txt
CHANGED
|
@@ -25,6 +25,7 @@ spacy>=3.7.0
|
|
| 25 |
phonenumbers>=8.13.0
|
| 26 |
sqlglot>=20.0.0
|
| 27 |
tiktoken>=0.9.0
|
|
|
|
| 28 |
|
| 29 |
# Async database drivers
|
| 30 |
aiomysql>=0.3.2
|
|
|
|
| 25 |
phonenumbers>=8.13.0
|
| 26 |
sqlglot>=20.0.0
|
| 27 |
tiktoken>=0.9.0
|
| 28 |
+
PyJWT>=2.9.0
|
| 29 |
|
| 30 |
# Async database drivers
|
| 31 |
aiomysql>=0.3.2
|