feat(dashboard): persist administrator login
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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:
|
||||
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()
|
||||
Reference in New Issue
Block a user