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