180 lines
5.6 KiB
Python
180 lines
5.6 KiB
Python
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
|