Switch auth password hashing to Argon2

This commit is contained in:
2026-04-20 15:26:36 +02:00
parent e1aad408ab
commit 3f7c9e43d9
7 changed files with 34 additions and 54 deletions
+11 -45
View File
@@ -1,12 +1,13 @@
from __future__ import annotations
import base64
import hashlib
import logging
import secrets
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError
from sqlalchemy import Select, select
from sqlalchemy.orm import Session
@@ -14,11 +15,7 @@ from app.config import Settings
from app.models.auth import AuthSession, AuthUser
logger = logging.getLogger(__name__)
SCRYPT_N = 2**14
SCRYPT_R = 8
SCRYPT_P = 1
SCRYPT_DKLEN = 64
password_hasher = PasswordHasher()
class AuthBootstrapError(RuntimeError):
@@ -62,52 +59,17 @@ def initialize_auth_schema(session: Session, settings: Settings) -> None:
def hash_password(password: str) -> str:
salt = secrets.token_bytes(16)
derived_key = hashlib.scrypt(
password.encode("utf-8"),
salt=salt,
n=SCRYPT_N,
r=SCRYPT_R,
p=SCRYPT_P,
dklen=SCRYPT_DKLEN,
)
return "$".join(
[
"scrypt",
str(SCRYPT_N),
str(SCRYPT_R),
str(SCRYPT_P),
base64.b64encode(salt).decode("ascii"),
base64.b64encode(derived_key).decode("ascii"),
]
)
return password_hasher.hash(password)
def verify_password(password: str, stored_hash: str) -> bool:
try:
algorithm, n, r, p, encoded_salt, encoded_key = stored_hash.split("$")
except ValueError:
return password_hasher.verify(stored_hash, password)
except VerifyMismatchError:
return False
if algorithm != "scrypt":
except (InvalidHashError, VerificationError):
return False
try:
salt = base64.b64decode(encoded_salt.encode("ascii"))
expected_key = base64.b64decode(encoded_key.encode("ascii"))
derived_key = hashlib.scrypt(
password.encode("utf-8"),
salt=salt,
n=int(n),
r=int(r),
p=int(p),
dklen=len(expected_key),
)
except (ValueError, TypeError):
return False
return secrets.compare_digest(derived_key, expected_key)
def authenticate_user(session: Session, *, username: str, password: str) -> AuthUser | None:
user = session.scalar(select(AuthUser).where(AuthUser.username == username).limit(1))
@@ -156,6 +118,10 @@ def get_authenticated_session(session: Session, *, raw_token: str | None) -> Aut
now = _utc_now()
expires_at = _as_utc(auth_session.expires_at)
revoked_at = _as_utc(auth_session.revoked_at)
if expires_at is None:
logger.warning("Auth session %s has no expires_at; treating it as invalid", auth_session.id)
return None
if revoked_at is not None or expires_at <= now or not user.is_active:
if revoked_at is None and expires_at <= now:
auth_session.revoked_at = now