feat: harden single-user account authentication
This commit is contained in:
@@ -141,3 +141,104 @@ def test_browser_admin_can_read_other_clients_operation(client, app, service):
|
||||
assert operation_id in [
|
||||
op["operation_id"] for op in client.get("/api/v1/operations").json()
|
||||
]
|
||||
|
||||
|
||||
def test_password_minimum_and_https_cookie(client, app):
|
||||
response = login(client, app)
|
||||
client.headers["X-CSRF-Token"] = response.json()["csrf_token"]
|
||||
assert (
|
||||
client.put(
|
||||
"/api/v1/auth/password",
|
||||
json={"current_password": "fixture-password", "new_password": "1234567"},
|
||||
).status_code
|
||||
== 422
|
||||
)
|
||||
assert (
|
||||
client.put(
|
||||
"/api/v1/auth/password",
|
||||
json={"current_password": "fixture-password", "new_password": "12345678"},
|
||||
).status_code
|
||||
== 204
|
||||
)
|
||||
response = client.post(
|
||||
"https://testserver/api/v1/auth/login",
|
||||
json={"username": "test-user", "password": "12345678"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert "Secure" in response.headers["set-cookie"]
|
||||
assert response.headers["cache-control"] == "no-store"
|
||||
|
||||
|
||||
def test_login_rejects_cross_origin(client, app):
|
||||
account(app)
|
||||
assert (
|
||||
client.post(
|
||||
"/api/v1/auth/login",
|
||||
headers={"Origin": "https://evil.example"},
|
||||
json={"username": "test-user", "password": "fixture-password"},
|
||||
).status_code
|
||||
== 403
|
||||
)
|
||||
|
||||
|
||||
def test_account_throttle_across_source_addresses(client, app):
|
||||
account(app)
|
||||
for _ in range(5):
|
||||
assert (
|
||||
client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "test-user", "password": "wrong"},
|
||||
).status_code
|
||||
== 401
|
||||
)
|
||||
app.state.service.db.execute(
|
||||
"DELETE FROM login_attempts WHERE identity NOT LIKE 'account:%'"
|
||||
)
|
||||
assert (
|
||||
client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "test-user", "password": "fixture-password"},
|
||||
).status_code
|
||||
== 429
|
||||
)
|
||||
|
||||
|
||||
def test_legacy_hash_upgrade(client, app):
|
||||
import hmac
|
||||
|
||||
from pigway_frame_flow.core.accounts import verify_password
|
||||
|
||||
salt = "12" * 16
|
||||
digest = hashlib.scrypt(
|
||||
b"fixture-password", salt=bytes.fromhex(salt), n=16384, r=8, p=1
|
||||
).hex()
|
||||
legacy = f"scrypt${salt}${digest}"
|
||||
app.state.service.db.execute(
|
||||
"INSERT INTO users VALUES(?,?,1)", ("test-user", legacy)
|
||||
)
|
||||
assert verify_password("fixture-password", legacy)
|
||||
assert (
|
||||
client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"username": "test-user", "password": "fixture-password"},
|
||||
).status_code
|
||||
== 200
|
||||
)
|
||||
stored = app.state.service.db.one("SELECT password_hash FROM users")[
|
||||
"password_hash"
|
||||
]
|
||||
assert stored.startswith("scrypt-v2$") and not hmac.compare_digest(stored, legacy)
|
||||
assert verify_password("fixture-password", stored)
|
||||
|
||||
|
||||
def test_only_one_browser_account(app):
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
account(app)
|
||||
with pytest.raises(sqlite3.IntegrityError):
|
||||
app.state.service.db.execute(
|
||||
"INSERT INTO users VALUES(?,?,1)",
|
||||
("other", password_hash("another-password")),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user