Files
jarca-ledger-mcp/tests/test_dashboard_auth_server.py
T

192 lines
5.9 KiB
Python

from __future__ import annotations
import sys
from http.cookies import SimpleCookie
from pathlib import Path
import pytest
from starlette.testclient import TestClient
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
from dashboard.auth_server import create_auth_routes
ADMIN_ID = "admin"
ADMIN_PASSWORD = "correct horse battery staple"
SIGNING_SECRET = "s" * 64
@pytest.fixture(autouse=True)
def _configured_auth(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(authentication.ADMIN_ID_ENV, ADMIN_ID)
monkeypatch.setenv(authentication.ADMIN_PASSWORD_ENV, ADMIN_PASSWORD)
monkeypatch.setenv(authentication.SIGNING_SECRET_ENV, SIGNING_SECRET)
monkeypatch.setattr(authentication.time, "sleep", lambda _seconds: None)
@pytest.fixture
def client() -> TestClient:
from starlette.applications import Starlette
return TestClient(
Starlette(routes=create_auth_routes()), base_url="https://testserver"
)
def _cookie_from(response) -> SimpleCookie[str]:
cookie: SimpleCookie[str] = SimpleCookie()
cookie.load(response.headers["set-cookie"])
return cookie
def test_successful_login_sets_hardened_24_hour_cookie(client: TestClient) -> None:
response = client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": ADMIN_PASSWORD},
headers={"origin": "https://testserver"},
follow_redirects=False,
)
assert response.status_code == 303
assert response.headers["location"] == "/"
header = response.headers["set-cookie"]
assert f"{authentication.AUTH_COOKIE_NAME}=" in header
assert "HttpOnly" in header
assert "Secure" in header
assert "SameSite=lax" in header
assert "Path=/" in header
assert f"Max-Age={authentication.DEFAULT_TOKEN_TTL_SECONDS}" in header
assert ADMIN_PASSWORD not in header
assert SIGNING_SECRET not in header
def test_remember_me_sets_30_day_cookie(client: TestClient) -> None:
response = client.post(
"/api/auth/login",
data={
"admin_id": ADMIN_ID,
"admin_password": ADMIN_PASSWORD,
"remember_me": "on",
},
headers={"origin": "https://testserver"},
follow_redirects=False,
)
assert response.status_code == 303
assert (
f"Max-Age={authentication.REMEMBER_ME_TOKEN_TTL_SECONDS}"
in response.headers["set-cookie"]
)
def test_invalid_credentials_do_not_set_cookie_and_use_general_error(
client: TestClient,
) -> None:
response = client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": "wrong"},
headers={"origin": "https://testserver"},
follow_redirects=False,
)
assert response.status_code == 303
assert response.headers["location"] == "/?auth_error=invalid"
assert "set-cookie" not in response.headers
assert "password" not in response.headers["location"]
assert ADMIN_ID not in response.headers["location"]
def test_missing_configuration_fails_closed(client: TestClient, monkeypatch) -> None:
monkeypatch.delenv(authentication.SIGNING_SECRET_ENV)
response = client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": ADMIN_PASSWORD},
headers={"origin": "https://testserver"},
follow_redirects=False,
)
assert response.status_code == 303
assert response.headers["location"] == "/?auth_error=unavailable"
assert "set-cookie" not in response.headers
def test_cross_site_login_is_rejected(client: TestClient) -> None:
response = client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": ADMIN_PASSWORD},
headers={"origin": "https://attacker.example"},
follow_redirects=False,
)
assert response.status_code == 403
assert "set-cookie" not in response.headers
def test_different_port_is_not_same_origin(client: TestClient) -> None:
response = client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": ADMIN_PASSWORD},
headers={"origin": "https://testserver:444"},
follow_redirects=False,
)
assert response.status_code == 403
assert "set-cookie" not in response.headers
def test_plain_http_is_rejected_even_with_spoofed_https_origin() -> None:
from starlette.applications import Starlette
with TestClient(
Starlette(routes=create_auth_routes()), base_url="http://testserver"
) as http_client:
response = http_client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": ADMIN_PASSWORD},
headers={
"origin": "https://testserver",
"x-forwarded-proto": "https",
},
follow_redirects=False,
)
assert response.status_code == 403
assert "set-cookie" not in response.headers
def test_logout_clears_cookie_with_matching_scope(client: TestClient) -> None:
response = client.post(
"/api/auth/logout",
headers={"origin": "https://testserver"},
follow_redirects=False,
)
assert response.status_code == 303
header = response.headers["set-cookie"]
assert f"{authentication.AUTH_COOKIE_NAME}=" in header
assert "Max-Age=0" in header
assert "HttpOnly" in header
assert "Secure" in header
assert "SameSite=lax" in header
assert "Path=/" in header
def test_cookie_token_round_trip(client: TestClient) -> None:
response = client.post(
"/api/auth/login",
data={"admin_id": ADMIN_ID, "admin_password": ADMIN_PASSWORD},
headers={"origin": "https://testserver"},
follow_redirects=False,
)
cookie = _cookie_from(response)
token = cookie[authentication.AUTH_COOKIE_NAME].value
assert authentication.verify_persistent_token(
authentication.load_auth_settings(), token
) is not None