Files
endurain/backend/app/auth/utils.py
João Vitória Silva f6e06fb3e6 Add session idle and absolute timeout enforcement
Implements optional session idle and absolute timeout logic, including new environment variables for configuration. Adds last_activity_at to sessions, enforces timeouts on token refresh, and introduces a scheduler job to clean up idle sessions. Also introduces progressive lockout for failed logins and updates documentation and examples accordingly.
2025-12-18 10:28:22 +00:00

202 lines
6.8 KiB
Python

import os
from datetime import datetime, timedelta, timezone
from typing import Tuple
from fastapi import (
HTTPException,
status,
Response,
Request,
)
from uuid import uuid4
from sqlalchemy.orm import Session
import auth.constants as auth_constants
import session.utils as session_utils
import auth.password_hasher as auth_password_hasher
import auth.token_manager as auth_token_manager
import users.user.crud as users_crud
import users.user.schema as users_schema
def authenticate_user(
username: str,
password: str,
password_hasher: auth_password_hasher.PasswordHasher,
db: Session,
) -> users_schema.UserRead:
"""
Authenticates a user by verifying the provided username and password.
Args:
username (str): The username of the user attempting to authenticate.
password (str): The plaintext password provided by the user.
password_hasher (auth_password_hasher.PasswordHasher): An instance of the password hasher for verifying and updating password hashes.
db (Session): The database session used for querying and updating user data.
Returns:
users_schema.UserRead: The authenticated user object if authentication is successful.
Raises:
HTTPException: If the username does not exist or the password is invalid.
"""
# Get the user from the database
user = users_crud.authenticate_user(username, db)
# Check if the user exists and if the password is correct
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username",
headers={"WWW-Authenticate": "Bearer"},
)
# Verify password and get updated hash if applicable
is_password_valid, updated_hash = password_hasher.verify_and_update(
password, user.password
)
if not is_password_valid:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid password",
headers={"WWW-Authenticate": "Bearer"},
)
# Update user hash if applicable
if updated_hash:
users_crud.edit_user_password(
user.id, updated_hash, password_hasher, db, is_hashed=True
)
# Return the user if the password is correct
return user
def create_tokens(
user: users_schema.UserRead,
token_manager: auth_token_manager.TokenManager,
session_id: str | None = None,
) -> Tuple[str, datetime, str, datetime, str, str]:
"""
Generates session tokens for a user, including access token, refresh token, and CSRF token.
Args:
user (users_schema.UserRead): The user object for whom the tokens are being created.
token_manager (auth_token_manager.TokenManager): The token manager responsible for token creation.
session_id (str | None, optional): An optional session ID. If not provided, a new unique session ID is generated.
Returns:
Tuple[str, datetime, str, datetime, str, str]:
A tuple containing:
- session_id (str): The session identifier.
- access_token_exp (datetime): Expiration datetime of the access token.
- access_token (str): The access token string.
- refresh_token_exp (datetime): Expiration datetime of the refresh token.
- refresh_token (str): The refresh token string.
- csrf_token (str): The CSRF token string.
"""
if session_id is None:
# Generate a unique session ID
session_id = str(uuid4())
# Create the access, refresh tokens and csrf token
access_token_exp, access_token = token_manager.create_token(
session_id, user, auth_token_manager.TokenType.ACCESS
)
refresh_token_exp, refresh_token = token_manager.create_token(
session_id, user, auth_token_manager.TokenType.REFRESH
)
csrf_token = token_manager.create_csrf_token()
return (
session_id,
access_token_exp,
access_token,
refresh_token_exp,
refresh_token,
csrf_token,
)
def complete_login(
response: Response,
request: Request,
user: users_schema.UserRead,
client_type: str,
password_hasher: auth_password_hasher.PasswordHasher,
token_manager: auth_token_manager.TokenManager,
db: Session,
) -> dict:
"""
Handles the completion of the login process by generating session and authentication tokens,
storing the session in the database, and returning tokens in response body.
OAuth 2.1 compliant: Returns tokens in response body for all clients.
- Access token and CSRF token: Returned in body (for in-memory storage)
- Refresh token: Set as httpOnly cookie with SameSite=Strict
This unified model works for both username/password and SSO login flows.
Args:
response (Response): The HTTP response object to set refresh cookie.
request (Request): The HTTP request object containing client information.
user (users_schema.UserRead): The authenticated user object.
client_type (str): The type of client ("web" or "mobile").
password_hasher (auth_password_hasher.PasswordHasher): Utility for password hashing.
token_manager (auth_token_manager.TokenManager): Utility for token generation and management.
db (Session): Database session for storing session information.
Returns:
dict: Contains session_id, access_token, csrf_token, token_type, and expires_in.
Raises:
HTTPException: If the client type is invalid, raises a 403 Forbidden error.
"""
if client_type not in ["web", "mobile"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid client type",
headers={"WWW-Authenticate": "Bearer"},
)
# Create the tokens
(
session_id,
access_token_exp,
access_token,
_refresh_token_exp,
refresh_token,
csrf_token,
) = create_tokens(user, token_manager)
# Create the session and store it in the database
session_utils.create_session(
session_id, user, request, refresh_token, password_hasher, db
)
# Access token and CSRF token returned in body for in-memory storage
secure = os.environ.get("FRONTEND_PROTOCOL") == "https"
response.set_cookie(
key="endurain_refresh_token",
value=refresh_token,
expires=datetime.now(timezone.utc)
+ timedelta(days=auth_constants.JWT_REFRESH_TOKEN_EXPIRE_DAYS),
httponly=True,
path="/",
secure=secure,
samesite="strict", # OAuth 2.1: Strict for defense-in-depth
)
# Return tokens in response body for all clients (unified OAuth 2.1 model)
return {
"session_id": session_id,
"access_token": access_token,
"csrf_token": csrf_token,
"token_type": "bearer",
"expires_in": int(access_token_exp.timestamp()),
}