from __future__ import annotations import json import sys from dataclasses import replace from pathlib import Path import pytest ROOT_DIR = Path(__file__).resolve().parents[1] if str(ROOT_DIR) not in sys.path: sys.path.insert(0, str(ROOT_DIR)) from dashboard import authentication ADMIN_ID = "admin" ADMIN_PASSWORD = "correct horse battery staple" SIGNING_SECRET = "s" * 64 def _settings() -> authentication.AuthSettings: return authentication.AuthSettings( admin_id=ADMIN_ID, admin_password=ADMIN_PASSWORD, signing_secret=SIGNING_SECRET, ) def test_load_auth_settings_requires_signing_secret(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv(authentication.ADMIN_ID_ENV, ADMIN_ID) monkeypatch.setenv(authentication.ADMIN_PASSWORD_ENV, ADMIN_PASSWORD) monkeypatch.delenv(authentication.SIGNING_SECRET_ENV, raising=False) assert authentication.load_auth_settings() is None monkeypatch.setenv(authentication.SIGNING_SECRET_ENV, SIGNING_SECRET) assert authentication.load_auth_settings() == _settings() def test_issue_and_verify_default_token_without_refresh() -> None: settings = _settings() issued = authentication.issue_persistent_token( settings, remember_me=False, now=1_700_000_000, token_id="fixed-token-id", ) verified = authentication.verify_persistent_token( settings, issued.value, now=1_700_000_001, ) assert verified is not None assert verified.issued_at == 1_700_000_000 assert verified.expires_at == 1_700_086_400 assert issued.max_age == authentication.DEFAULT_TOKEN_TTL_SECONDS assert verified.token_id == "fixed-token-id" assert ADMIN_PASSWORD not in issued.value assert SIGNING_SECRET not in issued.value def test_remember_me_token_uses_30_day_absolute_expiry() -> None: issued = authentication.issue_persistent_token( _settings(), remember_me=True, now=1_700_000_000, token_id="fixed-token-id", ) assert issued.expires_at == 1_702_592_000 assert issued.max_age == authentication.REMEMBER_ME_TOKEN_TTL_SECONDS def test_token_expiry_is_absolute_and_not_sliding() -> None: settings = _settings() issued = authentication.issue_persistent_token( settings, remember_me=False, now=1_700_000_000, token_id="fixed-token-id", ) assert authentication.verify_persistent_token( settings, issued.value, now=issued.expires_at - 1 ) is not None assert authentication.verify_persistent_token( settings, issued.value, now=issued.expires_at ) is None def test_tampering_and_rotations_invalidate_token() -> None: settings = _settings() issued = authentication.issue_persistent_token( settings, remember_me=True, now=1_700_000_000, token_id="fixed-token-id", ) replacement = "A" if issued.value[-1] != "A" else "B" tampered = issued.value[:-1] + replacement assert authentication.verify_persistent_token( settings, tampered, now=1_700_000_001 ) is None assert authentication.verify_persistent_token( replace(settings, signing_secret="n" * 64), issued.value, now=1_700_000_001, ) is None assert authentication.verify_persistent_token( replace(settings, admin_password="rotated password"), issued.value, now=1_700_000_001, ) is None assert authentication.verify_persistent_token( replace(settings, admin_id="rotated-admin"), issued.value, now=1_700_000_001, ) is None @pytest.mark.parametrize( "payload", [ {}, {"v": 2, "sub": ADMIN_ID, "iat": 1, "exp": 2, "jti": "x"}, {"v": 1, "sub": "", "iat": 1, "exp": 2, "jti": "x"}, {"v": 1, "sub": ADMIN_ID, "iat": "1", "exp": 2, "jti": "x"}, {"v": 1, "sub": ADMIN_ID, "iat": 2, "exp": 2, "jti": "x"}, {"v": 1, "sub": ADMIN_ID, "iat": 1, "exp": 2, "jti": ""}, {"v": 1, "sub": ADMIN_ID, "iat": 1, "exp": 2, "jti": "x", "extra": 1}, ], ) def test_claim_schema_is_strict(payload: dict[str, object]) -> None: token = authentication._serializer_for(_settings()).dumps(payload) assert authentication.verify_persistent_token( _settings(), token, now=1 ) is None def test_oversized_and_malformed_tokens_are_rejected() -> None: settings = _settings() assert authentication.verify_persistent_token(settings, "x" * 10_000, now=1) is None assert authentication.verify_persistent_token(settings, "not-a-token", now=1) is None def test_payload_does_not_encode_secrets() -> None: settings = replace(_settings(), admin_id="configured-admin-identity") issued = authentication.issue_persistent_token( settings, remember_me=False, now=1_700_000_000, token_id="fixed-token-id", ) payload = authentication._serializer_for(settings).loads(issued.value) assert set(payload) == {"v", "sub", "iat", "exp", "jti"} assert "password" not in json.dumps(payload).lower() assert "secret" not in json.dumps(payload).lower() assert settings.admin_id not in json.dumps(payload) def test_remember_me_token_expires_at_exact_30_day_boundary() -> None: settings = _settings() issued = authentication.issue_persistent_token( settings, remember_me=True, now=1_700_000_000, token_id="boundary-token-id" ) assert authentication.verify_persistent_token( settings, issued.value, now=issued.expires_at - 1 ) is not None assert authentication.verify_persistent_token( settings, issued.value, now=issued.expires_at ) is None