linguaielts-api / backend /app /core /security.py
AnhviNguyen
first commit
b1a388c
Raw
History Blame Contribute Delete
3.17 kB
"""
app/core/security.py
─────────────────────
JWT creation/verification and password hashing helpers.
"""
import hashlib
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
# ── Password hashing ─────────────────────────────────────────────────────────
# Default to pbkdf2_sha256 to avoid bcrypt backend issues on some Python builds.
# Keep bcrypt for backward compatibility with already-created hashes.
_pwd_context = CryptContext(
schemes=["pbkdf2_sha256", "bcrypt"],
deprecated="auto",
)
def hash_password(plain: str) -> str:
"""Return secure hash of a plain-text password."""
return _pwd_context.hash(plain)
def verify_password(plain: str, hashed: str) -> bool:
"""Return True if *plain* matches *hashed*."""
try:
return _pwd_context.verify(plain, hashed)
except (ValueError, RuntimeError):
# If legacy bcrypt backend raises unexpectedly, fail safely.
return False
# ── JWT ──────────────────────────────────────────────────────────────────────
def create_access_token(subject: int | str, expires_delta: Optional[timedelta] = None) -> str:
"""
Create a signed JWT token.
Args:
subject: Typically the user id stored in the `sub` claim.
expires_delta: Override the default expiry window.
Returns:
Encoded JWT string.
"""
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
)
payload = {"sub": str(subject), "type": "access", "exp": expire}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def create_refresh_token(subject: int | str, expires_delta: Optional[timedelta] = None) -> str:
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
)
payload = {"sub": str(subject), "type": "refresh", "exp": expire}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> Optional[dict[str, Any]]:
try:
return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
except JWTError:
return None
def decode_access_token(token: str) -> Optional[str]:
"""
Decode and verify a JWT token.
Returns:
The `sub` claim (user id as string) on success, None on failure.
"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
if payload.get("type") != "access":
return None
return payload.get("sub")
except JWTError:
return None
def hash_token(raw_token: str) -> str:
return hashlib.sha256(raw_token.encode("utf-8")).hexdigest()