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