From e6f5ea7f0c4505d9441e65b8703ec567f614c1e1 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Fri, 13 Feb 2026 14:35:32 +0100 Subject: [PATCH] feat: add PasswordService with argon2 hash/verify --- src/fastapi_oidc_op/authn/password.py | 20 +++++++++++++++++ tests/test_authn/test_password.py | 32 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/fastapi_oidc_op/authn/password.py create mode 100644 tests/test_authn/test_password.py diff --git a/src/fastapi_oidc_op/authn/password.py b/src/fastapi_oidc_op/authn/password.py new file mode 100644 index 0000000..737590a --- /dev/null +++ b/src/fastapi_oidc_op/authn/password.py @@ -0,0 +1,20 @@ +from argon2 import PasswordHasher +from argon2.exceptions import InvalidHashError, VerifyMismatchError + + +class PasswordService: + """Argon2 password hashing and verification.""" + + def __init__(self, hasher: PasswordHasher | None = None) -> None: + self._hasher = hasher or PasswordHasher() + + def hash(self, password: str) -> str: + """Hash a password using argon2id. Returns the full hash string.""" + return self._hasher.hash(password) + + def verify(self, password_hash: str, password: str) -> bool: + """Verify a password against an argon2 hash. Returns True if valid.""" + try: + return self._hasher.verify(password_hash, password) + except (VerifyMismatchError, InvalidHashError): + return False diff --git a/tests/test_authn/test_password.py b/tests/test_authn/test_password.py new file mode 100644 index 0000000..4e46233 --- /dev/null +++ b/tests/test_authn/test_password.py @@ -0,0 +1,32 @@ +from argon2 import PasswordHasher + +from fastapi_oidc_op.authn.password import PasswordService + + +def test_hash_returns_argon2_string() -> None: + service = PasswordService(hasher=PasswordHasher(time_cost=1, memory_cost=8192)) + result = service.hash("correcthorse") + assert result.startswith("$argon2id$") + + +def test_verify_correct_password() -> None: + service = PasswordService(hasher=PasswordHasher(time_cost=1, memory_cost=8192)) + hashed = service.hash("correcthorse") + assert service.verify(hashed, "correcthorse") is True + + +def test_verify_wrong_password() -> None: + service = PasswordService(hasher=PasswordHasher(time_cost=1, memory_cost=8192)) + hashed = service.hash("correcthorse") + assert service.verify(hashed, "wrongpassword") is False + + +def test_verify_invalid_hash() -> None: + service = PasswordService(hasher=PasswordHasher(time_cost=1, memory_cost=8192)) + assert service.verify("not-a-hash", "password") is False + + +def test_default_hasher() -> None: + service = PasswordService() + hashed = service.hash("test") + assert service.verify(hashed, "test") is True