All checks were successful
Deploy to Production / deploy (push) Successful in 1m31s
- Hash passwords with SHA-256 on frontend before transmission to prevent raw password exposure in network traffic and server logs - Switch login endpoint from OAuth2 form-data to JSON body - Auto-create admin user on startup from ADMIN_USERNAME/ADMIN_PASSWORD env vars, solving login failure after registration was disabled - Update auth tests to match new SHA-256 + JSON login flow Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
59 lines
1.7 KiB
Python
59 lines
1.7 KiB
Python
"""
|
|
Security utilities for authentication.
|
|
"""
|
|
import hashlib
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Optional
|
|
|
|
import jwt
|
|
from jwt.exceptions import PyJWTError
|
|
from passlib.context import CryptContext
|
|
|
|
from app.core.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
|
|
def sha256_hash(password: str) -> str:
|
|
"""SHA-256 hash a raw password (matches client-side hashing)."""
|
|
return hashlib.sha256(password.encode()).hexdigest()
|
|
|
|
|
|
def verify_password(sha256_password: str, hashed_password: str) -> bool:
|
|
"""Verify a SHA-256 hashed password against its bcrypt hash."""
|
|
return pwd_context.verify(sha256_password, hashed_password)
|
|
|
|
|
|
def get_password_hash(raw_password: str) -> str:
|
|
"""Hash a raw password: SHA-256 then bcrypt."""
|
|
return pwd_context.hash(sha256_hash(raw_password))
|
|
|
|
|
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
|
"""Create a JWT access token."""
|
|
to_encode = data.copy()
|
|
if expires_delta:
|
|
expire = datetime.now(timezone.utc) + expires_delta
|
|
else:
|
|
expire = datetime.now(timezone.utc) + timedelta(
|
|
minutes=settings.access_token_expire_minutes
|
|
)
|
|
to_encode.update({"exp": expire})
|
|
encoded_jwt = jwt.encode(
|
|
to_encode, settings.jwt_secret, algorithm=settings.jwt_algorithm
|
|
)
|
|
return encoded_jwt
|
|
|
|
|
|
def decode_access_token(token: str) -> Optional[dict]:
|
|
"""Decode and verify a JWT access token."""
|
|
try:
|
|
payload = jwt.decode(
|
|
token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]
|
|
)
|
|
return payload
|
|
except PyJWTError:
|
|
return None
|