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