fix(backend): resolve refresh-token expiry deadlock, missing Jira on normal campaign approval, discarded threat-actor campaign start_date
- /auth/refresh now allows a short grace window past expiry and checks the blacklist, so an active session's silent refresh no longer fails the instant its own token expires - normal manager /approve flow now creates Jira tickets for the campaign and its already-linked tests, matching the admin-only /activate path - GenerateFromActorPayload now accepts start_date and threads it through to the new campaign instead of silently discarding it
This commit is contained in:
@@ -16,6 +16,9 @@ from fastapi import APIRouter, Cookie, Depends, Request, Response
|
||||
# Import OAuth2PasswordRequestForm from fastapi.security
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
# Import datetime, timezone from datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Import jwt (PyJWT)
|
||||
import jwt
|
||||
from jwt.exceptions import PyJWTError as JWTError
|
||||
@@ -24,7 +27,7 @@ from jwt.exceptions import PyJWTError as JWTError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import blacklist_token, create_access_token, verify_pa... from app.auth
|
||||
from app.auth import blacklist_token, create_access_token, verify_password
|
||||
from app.auth import blacklist_token, create_access_token, is_token_blacklisted, verify_password
|
||||
|
||||
# Import settings from app.config
|
||||
from app.config import settings
|
||||
@@ -272,19 +275,28 @@ def logout(
|
||||
return {"detail": "Logged out"}
|
||||
|
||||
|
||||
# A token that has *just* expired can still be refreshed within this window.
|
||||
# Without this grace period, `/auth/refresh` decodes the same token with the
|
||||
# same strict expiry check as every other endpoint — so by the time a 401
|
||||
# triggers a refresh attempt, the refresh call itself has also expired and
|
||||
# the session is unrecoverable. The grace window only widens the refresh
|
||||
# check; a brand-new token is always issued with the full expiry.
|
||||
_REFRESH_GRACE_SECONDS = 15 * 60
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
def refresh_token(
|
||||
response: Response,
|
||||
aegis_token: str | None = Cookie(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Issue a new access token if the current one is valid.
|
||||
"""Issue a new access token if the current one is valid or recently expired.
|
||||
|
||||
Called automatically by the frontend when it detects an expired
|
||||
session while the user is actively using the app. If the current
|
||||
cookie token is still valid (not blacklisted, not expired), a fresh
|
||||
token is issued and the cookie is renewed — keeping the session alive
|
||||
without requiring re-authentication.
|
||||
session while the user is actively using the app. Expiry is checked
|
||||
manually (with `_REFRESH_GRACE_SECONDS` of leeway) instead of relying on
|
||||
PyJWT's built-in check, so a token that expired moments ago can still
|
||||
be refreshed. Revoked (blacklisted) tokens are never refreshable.
|
||||
"""
|
||||
if not aegis_token:
|
||||
raise PermissionViolation("No active session")
|
||||
@@ -294,10 +306,20 @@ def refresh_token(
|
||||
aegis_token,
|
||||
settings.SECRET_KEY,
|
||||
algorithms=[settings.ALGORITHM],
|
||||
options={"verify_exp": False},
|
||||
)
|
||||
except JWTError:
|
||||
raise PermissionViolation("Session expired — please log in again")
|
||||
|
||||
jti: str | None = payload.get("jti")
|
||||
if jti and is_token_blacklisted(jti):
|
||||
raise PermissionViolation("Session expired — please log in again")
|
||||
|
||||
exp = payload.get("exp")
|
||||
now = datetime.now(timezone.utc).timestamp()
|
||||
if exp is None or now - exp > _REFRESH_GRACE_SECONDS:
|
||||
raise PermissionViolation("Session expired — please log in again")
|
||||
|
||||
username: str | None = payload.get("sub")
|
||||
if not username:
|
||||
raise PermissionViolation("Invalid session")
|
||||
|
||||
Reference in New Issue
Block a user