openclaw-bridge / tests /test_auth_security.py
Andy
feat(saas-wrapper): P3-P11 production wrapper landing on HF
a382c74
Raw
History Blame Contribute Delete
5.01 kB
"""Unit tests for the auth crypto utilities — argon2 + JWT.
Crypto code is small but high-stakes. Tests assert:
- argon2 hashes are not reversible to plaintext
- verify() returns False on tampered hash / wrong password
- rehashing on parameter upgrade is detected
- JWT issuance + decode round-trip
- JWT signature tamper detection
- JWT expiry enforcement
- JWT issued for slot_id matches the regex
"""
from __future__ import annotations
import time
import pytest
def test_argon2_hash_is_not_reversible():
from app.auth.passwords import hash_password, verify_password
h = hash_password("correct horse battery staple")
assert h != "correct horse battery staple"
assert h.startswith("$argon2"), h
def test_argon2_verify_correct_password():
from app.auth.passwords import hash_password, verify_password
h = hash_password("hunter2!")
assert verify_password(h, "hunter2!") is True
def test_argon2_verify_wrong_password():
from app.auth.passwords import hash_password, verify_password
h = hash_password("hunter2!")
assert verify_password(h, "hunter3!") is False
assert verify_password(h, "") is False
assert verify_password(h, "hunter2! ") is False # trailing space matters
def test_argon2_verify_tampered_hash():
from app.auth.passwords import hash_password, verify_password
h = hash_password("hunter2!")
tampered = h[:-3] + "AAA"
assert verify_password(tampered, "hunter2!") is False
def test_argon2_rejects_empty_or_huge_password():
from app.auth.passwords import hash_password, PasswordRejected
with pytest.raises(PasswordRejected):
hash_password("")
# Argon2 rejects very long strings to avoid DoS via long inputs.
with pytest.raises(PasswordRejected):
hash_password("x" * 4096)
def test_jwt_roundtrip(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "x" * 32)
from app.auth.tokens import decode_access_token, issue_access_token
token = issue_access_token(client_id="11111111-1111-1111-1111-111111111111", slot_id="user_slot_001")
claims = decode_access_token(token)
assert claims["sub"] == "11111111-1111-1111-1111-111111111111"
assert claims["aid"] == "user_slot_001"
assert claims["typ"] == "access"
def test_jwt_signature_tamper(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "x" * 32)
from app.auth.tokens import decode_access_token, issue_access_token, JWTInvalid
token = issue_access_token(client_id="u", slot_id="user_slot_001")
# Flip the last byte of the signature segment.
parts = token.split(".")
parts[2] = parts[2][:-2] + ("AA" if parts[2][-2:] != "AA" else "BB")
tampered = ".".join(parts)
with pytest.raises(JWTInvalid):
decode_access_token(tampered)
def test_jwt_wrong_secret(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "x" * 32)
from app.auth.tokens import decode_access_token, issue_access_token, JWTInvalid
token = issue_access_token(client_id="u", slot_id="user_slot_001")
monkeypatch.setenv("JWT_SECRET", "y" * 32)
with pytest.raises(JWTInvalid):
decode_access_token(token)
def test_jwt_expired(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "x" * 32)
monkeypatch.setenv("ACCESS_TOKEN_TTL_SECONDS", "1")
from app.auth.tokens import decode_access_token, issue_access_token, JWTInvalid
token = issue_access_token(client_id="u", slot_id="user_slot_001")
time.sleep(1.2)
with pytest.raises(JWTInvalid):
decode_access_token(token)
def test_jwt_rejects_invalid_slot_id(monkeypatch):
"""Per multi-tenant-auth §5 JWT shape, aid must match user_slot_<digits>."""
monkeypatch.setenv("JWT_SECRET", "x" * 32)
from app.auth.tokens import decode_access_token, JWTInvalid, issue_access_token
# Forge a token with an aid that violates the regex.
token = issue_access_token(client_id="u", slot_id="user_slot_001")
# Decode legit first — should pass.
decode_access_token(token)
# Now build a manual token with bad aid and check decode rejects.
import jwt
import os
bad = jwt.encode(
{"sub": "u", "aid": "../etc/passwd", "typ": "access", "iat": int(time.time()), "exp": int(time.time()) + 60},
os.environ["JWT_SECRET"],
algorithm="HS256",
)
with pytest.raises(JWTInvalid):
decode_access_token(bad)
def test_refresh_token_roundtrip(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "x" * 32)
from app.auth.tokens import decode_refresh_token, issue_refresh_token
token = issue_refresh_token(client_id="u")
claims = decode_refresh_token(token)
assert claims["sub"] == "u"
assert claims["typ"] == "refresh"
def test_refresh_token_rejected_as_access(monkeypatch):
monkeypatch.setenv("JWT_SECRET", "x" * 32)
from app.auth.tokens import decode_access_token, issue_refresh_token, JWTInvalid
rtoken = issue_refresh_token(client_id="u")
with pytest.raises(JWTInvalid):
decode_access_token(rtoken)