Switch auth password hashing to Argon2
This commit is contained in:
+11
-45
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user