69 lines
2.1 KiB
Python
69 lines
2.1 KiB
Python
"""Authentication, CSRF, token, and redaction matrices."""
|
|
|
|
import pytest
|
|
from starlette.responses import Response
|
|
|
|
from app.security.auth import (
|
|
AuthenticationError,
|
|
csrf_token,
|
|
hash_secret,
|
|
issue_ticket,
|
|
parse_api_token,
|
|
redact_secrets,
|
|
set_ticket_cookie,
|
|
verify_csrf,
|
|
verify_secret,
|
|
verify_ticket,
|
|
)
|
|
|
|
KEY = b"test-signing-key-with-at-least-32-bytes"
|
|
|
|
|
|
def test_password_and_token_hashes_do_not_store_plaintext() -> None:
|
|
encoded = hash_secret("correct horse", salt=b"0123456789abcdef")
|
|
|
|
assert "correct horse" not in encoded
|
|
assert verify_secret("correct horse", encoded)
|
|
assert not verify_secret("wrong", encoded)
|
|
assert not verify_secret("correct horse", "unknown$format")
|
|
|
|
|
|
def test_signed_ticket_expiry_and_csrf() -> None:
|
|
ticket = issue_ticket("root@pam", KEY, now=100, ttl=60)
|
|
|
|
assert verify_ticket(ticket, KEY, now=120).principal == "root@pam"
|
|
token = csrf_token(ticket, KEY)
|
|
assert verify_csrf(ticket, token, KEY)
|
|
assert not verify_csrf(ticket, token + "x", KEY)
|
|
with pytest.raises(AuthenticationError, match="expired"):
|
|
verify_ticket(ticket, KEY, now=161)
|
|
with pytest.raises(AuthenticationError, match="invalid"):
|
|
verify_ticket(ticket + "x", KEY, now=120)
|
|
|
|
|
|
def test_ticket_cookie_is_http_only_and_secure() -> None:
|
|
response = Response()
|
|
set_ticket_cookie(response, "ticket")
|
|
|
|
header = response.headers["set-cookie"]
|
|
assert "PVEAuthCookie=ticket" in header
|
|
assert "HttpOnly" in header
|
|
assert "Secure" in header
|
|
assert "SameSite=strict" in header
|
|
|
|
|
|
def test_api_token_parsing_and_log_redaction() -> None:
|
|
token = parse_api_token("PVEAPIToken=user@pve!automation=supersecret")
|
|
|
|
assert token.principal == "user@pve"
|
|
assert token.token_id == "automation"
|
|
assert token.secret == "supersecret"
|
|
redacted = redact_secrets(
|
|
"PVEAPIToken=user@pve!automation=supersecret password=hunter2 token=abc"
|
|
)
|
|
assert "supersecret" not in redacted
|
|
assert "hunter2" not in redacted
|
|
assert "token=abc" not in redacted
|
|
with pytest.raises(AuthenticationError):
|
|
parse_api_token("Bearer secret")
|