auth: add Argon2id hashing and password policy
This commit is contained in:
parent
51b0177f7f
commit
e86985743a
44
containers/clearview/src/clearview_app/auth/security.py
Normal file
44
containers/clearview/src/clearview_app/auth/security.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Password hashing, password-policy validation, and session-id generation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError
|
||||
|
||||
|
||||
class PasswordPolicyError(ValueError):
|
||||
"""Raised when a candidate password does not meet the policy."""
|
||||
|
||||
|
||||
_hasher = PasswordHasher()
|
||||
|
||||
MIN_LENGTH = 12
|
||||
|
||||
|
||||
def validate_password(pw: str) -> None:
|
||||
"""Enforce: length >= 12, at least one letter and one digit."""
|
||||
if len(pw) < MIN_LENGTH:
|
||||
raise PasswordPolicyError(f"Password must be at least {MIN_LENGTH} characters.")
|
||||
if not any(c.isalpha() for c in pw):
|
||||
raise PasswordPolicyError("Password must contain at least one letter.")
|
||||
if not any(c.isdigit() for c in pw):
|
||||
raise PasswordPolicyError("Password must contain at least one digit.")
|
||||
|
||||
|
||||
def hash_password(pw: str) -> str:
|
||||
return _hasher.hash(pw)
|
||||
|
||||
|
||||
def verify_password(pw: str, encoded: str) -> bool:
|
||||
try:
|
||||
return _hasher.verify(encoded, pw)
|
||||
except (VerifyMismatchError, InvalidHashError, VerificationError):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def new_session_id() -> str:
|
||||
"""Opaque 128-bit session identifier rendered as 32 hex chars."""
|
||||
return uuid.uuid4().hex
|
||||
37
containers/clearview/tests/test_security.py
Normal file
37
containers/clearview/tests/test_security.py
Normal file
@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
from clearview_app.auth.security import (
|
||||
PasswordPolicyError,
|
||||
hash_password,
|
||||
new_session_id,
|
||||
validate_password,
|
||||
verify_password,
|
||||
)
|
||||
|
||||
|
||||
def test_hash_and_verify_roundtrip():
|
||||
h = hash_password("CorrectHorse42")
|
||||
assert verify_password("CorrectHorse42", h) is True
|
||||
assert verify_password("wrong", h) is False
|
||||
|
||||
|
||||
def test_verify_returns_false_on_garbage_hash():
|
||||
assert verify_password("anything", "not-a-real-hash") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pw", ["short1A", "alllowercase", "ALLUPPERCASE", "12345678901234"])
|
||||
def test_policy_rejects(pw):
|
||||
with pytest.raises(PasswordPolicyError):
|
||||
validate_password(pw)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pw", ["CorrectHorse42", "abcdefghij12"])
|
||||
def test_policy_accepts(pw):
|
||||
validate_password(pw)
|
||||
|
||||
|
||||
def test_new_session_id_unique_and_hex():
|
||||
a = new_session_id()
|
||||
b = new_session_id()
|
||||
assert a != b
|
||||
assert len(a) == 32 and all(c in "0123456789abcdef" for c in a)
|
||||
Loading…
Reference in New Issue
Block a user