first commit
This commit is contained in:
155
services/auth_service.py
Normal file
155
services/auth_service.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
|
||||
from repositories.auth_repository import (
|
||||
create_auth_session_record,
|
||||
get_auth_session_by_token,
|
||||
revoke_auth_session,
|
||||
)
|
||||
|
||||
IDLE_TIMEOUT_SECONDS = 2 * 60 * 60
|
||||
SESSION_TOUCH_THROTTLE_SECONDS = 60
|
||||
PBKDF2_ITERATIONS = 260_000
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
salt = secrets.token_bytes(16)
|
||||
derived = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
password.encode("utf-8"),
|
||||
salt,
|
||||
PBKDF2_ITERATIONS,
|
||||
)
|
||||
return (
|
||||
f"pbkdf2_sha256${PBKDF2_ITERATIONS}$"
|
||||
f"{base64.b64encode(salt).decode()}$"
|
||||
f"{base64.b64encode(derived).decode()}"
|
||||
)
|
||||
|
||||
|
||||
def verify_password(password: str, stored_hash: str) -> bool:
|
||||
try:
|
||||
algorithm, iterations_raw, salt_b64, hash_b64 = stored_hash.split("$", 3)
|
||||
if algorithm != "pbkdf2_sha256":
|
||||
return False
|
||||
iterations = int(iterations_raw)
|
||||
salt = base64.b64decode(salt_b64)
|
||||
expected = base64.b64decode(hash_b64)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
actual = hashlib.pbkdf2_hmac(
|
||||
"sha256",
|
||||
password.encode("utf-8"),
|
||||
salt,
|
||||
iterations,
|
||||
)
|
||||
return hmac.compare_digest(actual, expected)
|
||||
|
||||
|
||||
def create_auth_session(
|
||||
user_id: int,
|
||||
ip_address: str | None = None,
|
||||
user_agent: str | None = None,
|
||||
) -> str:
|
||||
token = secrets.token_urlsafe(48)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(seconds=IDLE_TIMEOUT_SECONDS)
|
||||
create_auth_session_record(
|
||||
user_id=user_id,
|
||||
session_token=token,
|
||||
expires_at=expires_at,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def _is_api_request(request: Request) -> bool:
|
||||
path = request.url.path
|
||||
if request.method in {"POST", "PUT", "PATCH", "DELETE"}:
|
||||
return True
|
||||
if path.endswith("/events") or "/event/" in path or path.endswith("/event"):
|
||||
return True
|
||||
accept = request.headers.get("accept", "")
|
||||
requested_with = request.headers.get("x-requested-with", "")
|
||||
return "application/json" in accept or requested_with.lower() == "xmlhttprequest"
|
||||
|
||||
|
||||
def build_not_authenticated_response(request: Request):
|
||||
if _is_api_request(request):
|
||||
return JSONResponse({"error": "auth_expired"}, status_code=401)
|
||||
return RedirectResponse(url="/login?reason=idle", status_code=303)
|
||||
|
||||
|
||||
def revoke_auth_session_by_request(request: Request):
|
||||
token = request.cookies.get("auth_token")
|
||||
if token:
|
||||
revoke_auth_session(token)
|
||||
|
||||
|
||||
def get_current_user_from_request(request: Request):
|
||||
token = request.cookies.get("auth_token")
|
||||
if not token:
|
||||
return None
|
||||
|
||||
row = get_auth_session_by_token(token)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
(
|
||||
session_id,
|
||||
user_id,
|
||||
session_token,
|
||||
created_at,
|
||||
last_activity_at,
|
||||
expires_at,
|
||||
revoked_at,
|
||||
ip_address,
|
||||
user_agent,
|
||||
username,
|
||||
is_active,
|
||||
role,
|
||||
) = row
|
||||
|
||||
if revoked_at is not None or not is_active:
|
||||
return None
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
if last_activity_at.tzinfo is None:
|
||||
last_activity_at = last_activity_at.replace(tzinfo=timezone.utc)
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
|
||||
idle_seconds = (now - last_activity_at).total_seconds()
|
||||
if idle_seconds > IDLE_TIMEOUT_SECONDS or expires_at < now:
|
||||
revoke_auth_session(token)
|
||||
return None
|
||||
|
||||
new_expires_at = now + timedelta(seconds=IDLE_TIMEOUT_SECONDS)
|
||||
from repositories.auth_repository import touch_auth_session_if_needed
|
||||
touch_auth_session_if_needed(
|
||||
session_token=token,
|
||||
expires_at=new_expires_at,
|
||||
throttle_seconds=SESSION_TOUCH_THROTTLE_SECONDS,
|
||||
)
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"created_at": created_at,
|
||||
"last_activity_at": last_activity_at,
|
||||
"expires_at": expires_at,
|
||||
"ip_address": ip_address,
|
||||
"user_agent": user_agent,
|
||||
"role": role,
|
||||
}
|
||||
Reference in New Issue
Block a user