security: reduce JWT expiry to 24h and add jti claim

Tokens previously lasted 30 days with no revocation path. Expiry is now
24 hours and every token carries a unique jti (UUID hex) to support future
revocation checks.
This commit is contained in:
ZdenekSrotyr 2026-04-09 06:57:23 +02:00
parent 23ae6a602c
commit 3321d2e266
2 changed files with 35 additions and 1 deletions

View file

@ -1,6 +1,7 @@
"""JWT token creation and verification for API auth.""" """JWT token creation and verification for API auth."""
import os import os
import uuid
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
@ -24,7 +25,7 @@ elif len(SECRET_KEY) < 32 and os.environ.get("TESTING", "").lower() not in ("1",
) )
ALGORITHM = "HS256" ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = 24 * 30 # 30 days ACCESS_TOKEN_EXPIRE_HOURS = 24 # 24 hours
def create_access_token( def create_access_token(
@ -42,6 +43,7 @@ def create_access_token(
"role": role, "role": role,
"exp": expire, "exp": expire,
"iat": datetime.now(timezone.utc), "iat": datetime.now(timezone.utc),
"jti": uuid.uuid4().hex,
} }
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM) return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

View file

@ -111,6 +111,18 @@ class TestScriptSandbox:
# Should still run but without access to dangerous modules # Should still run but without access to dangerous modules
assert resp.status_code == 200 assert resp.status_code == 200
def test_sandbox_cannot_import_httpx(self, client):
"""httpx must be blocked — either by pattern check (400) or
ModuleNotFoundError at runtime due to stripped VIRTUAL_ENV/PYTHONPATH (200 with non-zero exit)."""
c, token = client
resp = c.post("/api/scripts/run", json={
"source": "import httpx\nprint('pwned')",
}, headers=_headers(token))
# Static pattern check should reject it outright
assert resp.status_code == 400 or (
resp.status_code == 200 and resp.json()["exit_code"] != 0
)
# ---- SQL Query Security ---- # ---- SQL Query Security ----
@ -213,6 +225,26 @@ class TestAuthSecurity:
assert resp.status_code == 401 assert resp.status_code == 401
# ---- JWT Claims ----
class TestJwtClaims:
def test_jwt_contains_jti_claim(self):
"""Token payload must include a jti claim with at least 16 hex chars."""
os.environ.setdefault("TESTING", "1")
from app.auth.jwt import create_access_token, verify_token
token = create_access_token("u1", "user@test.com", "analyst")
payload = verify_token(token)
assert payload is not None
assert "jti" in payload
assert len(payload["jti"]) >= 16
def test_jwt_expiry_is_24_hours(self):
"""ACCESS_TOKEN_EXPIRE_HOURS must be 24 (not 30*24)."""
os.environ.setdefault("TESTING", "1")
from app.auth import jwt as jwt_module
assert jwt_module.ACCESS_TOKEN_EXPIRE_HOURS == 24
# ---- JWT Secret Hardening ---- # ---- JWT Secret Hardening ----
class TestJwtSecretHardening: class TestJwtSecretHardening: