security: JWT aud/iss validation, token blacklist, encryption key crash-early
C-2 (CRITICAL): JWT decode_token now validates aud, iss, iat, jti claims. Tokens without these claims are rejected. Audience 'ss-tools-api' and issuer 'ss-tools' prevent cross-service token reuse. H-2 (HIGH): Server-side JWT revocation via token_blacklist table. - New TokenBlacklist model (SHA-256 hash only, never raw token) - blacklist_token() on logout — revokes current session - is_token_blacklisted() check in get_current_user - Expired entries auto-pruned via _prune_blacklist() H-3 (HIGH): Encryption key auto-generation removed — crash-early instead. Previously, ensure_encryption_key() would auto-generate a Fernet key on first run and write it to .env. This made encrypted data unrecoverable when the key changed between deployments. Now raises RuntimeError with a clear command to generate the key explicitly. Also: update orthogonal security test for crash-early behavior.
This commit is contained in:
@@ -13,10 +13,11 @@ from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
import starlette.requests
|
||||
|
||||
from ..core.auth.jwt import blacklist_token
|
||||
from ..core.auth.logger import log_security_event
|
||||
from ..core.auth.oauth import is_adfs_configured, oauth
|
||||
from ..core.database import get_auth_db
|
||||
from ..core.logger import belief_scope
|
||||
from ..core.logger import belief_scope, logger
|
||||
from ..dependencies import get_current_user
|
||||
from ..schemas.auth import Token, User as UserSchema
|
||||
from ..services.auth_service import AuthService
|
||||
@@ -74,18 +75,32 @@ async def read_users_me(current_user: UserSchema = Depends(get_current_user)):
|
||||
|
||||
|
||||
# #region logout [C:4] [TYPE Function]
|
||||
# @BRIEF Logs out the current user (placeholder for session revocation).
|
||||
# @PRE Valid JWT token provided.
|
||||
# @POST Returns success message.
|
||||
# @BRIEF Logs out the current user — blacklists the JWT token server-side.
|
||||
# @PRE Valid JWT token provided in Authorization header.
|
||||
# @POST Token is added to blacklist; subsequent requests with same token are rejected.
|
||||
# @SIDE_EFFECT Writes security event LOGOUT event; writes to token_blacklist table.
|
||||
# @RELATION DEPENDS_ON -> [get_current_user]
|
||||
# @SIDE_EFFECT Writes security event LOGOUT event.
|
||||
# @RELATION CALLS -> [blacklist_token]
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(current_user: UserSchema = Depends(get_current_user)):
|
||||
async def logout(
|
||||
current_user: UserSchema = Depends(get_current_user),
|
||||
request: starlette.requests.Request = None,
|
||||
db: Session = Depends(get_auth_db),
|
||||
):
|
||||
with belief_scope("api.auth.logout"):
|
||||
log_security_event("LOGOUT", current_user.username)
|
||||
# In a stateless JWT setup, client-side token deletion is primary.
|
||||
# Server-side revocation (blacklisting) can be added here if needed.
|
||||
|
||||
# Extract token from Authorization header and blacklist it
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
blacklist_token(token, db)
|
||||
logger.reason(
|
||||
"Token blacklisted on logout",
|
||||
extra={"user": current_user.username},
|
||||
)
|
||||
|
||||
return {"message": "Successfully logged out"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user