Authenticate sockets by header or one-time ticket instead of ?token=

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GAeg4Jo6xjZzjwy9QDWMQ
This commit is contained in:
2026-10-07 08:58:22 +02:00
co-authored by Claude Opus 5.5
parent eb45fb2763
commit 5259b8db0a
21 changed files with 336 additions and 44 deletions
+1
View File
@@ -521,6 +521,7 @@ async def test_the_live_socket_ends_when_its_client_goes(
def __init__(self) -> None:
self.app = client.app
self.headers: dict[str, str] = {}
self.frames = ['{"type": "media", "names": []}']
self.sent: list[str] = []
+12
View File
@@ -892,6 +892,18 @@ def test_a_panels_socket_carries_only_what_it_draws(
# A person's credential is not bounded at all.
assert panel_scope(superuser_token_headers["Authorization"][7:], client.app) is None
# It opens the socket with a ticket like any browser, and the ticket
# carries the panel's bound rather than widening it.
ticket = client.post(
f"{settings.API_V1_STR}/flows/ws-ticket",
headers={"Authorization": f"Bearer {token}"},
)
assert ticket.status_code == 200, ticket.text
with client.websocket_connect(
f"{settings.API_V1_STR}/flows/ws?ticket={ticket.json()['ticket']}"
) as ws:
assert ws.receive_json()["nodes"] == []
def test_a_panel_fetches_only_the_media_its_tiles_are_showing(
client: TestClient, superuser_token_headers: dict[str, str]
@@ -0,0 +1,72 @@
"""A socket's credential comes as a header or a one-time ticket, not in its URL."""
from time import monotonic
import pytest
from fastapi.testclient import TestClient
from starlette.websockets import WebSocketDisconnect
from fluksio.core.config import settings
from fluksio.flow.remote import PROTOCOL
FLOWS = f"{settings.API_V1_STR}/flows"
ATTACH = f"{settings.API_V1_STR}/workers/attach"
def ticket(client: TestClient, headers: dict[str, str]) -> str:
answer = client.post(f"{FLOWS}/ws-ticket", headers=headers)
assert answer.status_code == 200, answer.text
return str(answer.json()["ticket"])
def opens(client: TestClient, url: str, headers: dict[str, str] | None = None) -> bool:
try:
with client.websocket_connect(url, headers=headers or {}) as ws:
assert ws.receive_json()["type"] == "snapshot"
except WebSocketDisconnect:
return False
return True
def test_the_live_socket_takes_a_header_and_still_a_token(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
assert opens(client, f"{FLOWS}/ws", superuser_token_headers)
token = superuser_token_headers["Authorization"][7:]
assert opens(client, f"{FLOWS}/ws?token={token}")
assert not opens(client, f"{FLOWS}/ws")
def test_a_ticket_opens_the_socket_once(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
once = ticket(client, superuser_token_headers)
assert opens(client, f"{FLOWS}/ws?ticket={once}")
assert not opens(client, f"{FLOWS}/ws?ticket={once}")
def test_a_ticket_lapses_after_thirty_seconds(
client: TestClient,
superuser_token_headers: dict[str, str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
stale = ticket(client, superuser_token_headers)
later = monotonic() + 31
monkeypatch.setattr("fluksio.api.socket_auth.monotonic", lambda: later)
assert not opens(client, f"{FLOWS}/ws?ticket={stale}")
def test_a_worker_attaches_with_a_header_and_still_a_token(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
token = client.post(
f"{settings.API_V1_STR}/workers/tokens",
headers=superuser_token_headers,
json={"name": "header-box"},
).json()["token"]
hello = {"op": "hello", "protocol": PROTOCOL}
bearer = {"Authorization": f"Bearer {token}"}
for url, headers in ((ATTACH, bearer), (f"{ATTACH}?token={token}", {})):
with client.websocket_connect(url, headers=headers) as ws:
ws.send_json(hello)
assert ws.receive_json()["op"] == "welcome"
+3 -5
View File
@@ -1059,11 +1059,9 @@ def test_a_duration_reads_the_way_an_age_does() -> None:
def test_the_dashboard_subscribes_where_the_browser_does() -> None:
"""The same socket, and the token where a handshake can carry it."""
"""The same socket, with the token kept out of its URL."""
from fluksio.sdk.stream import socket_url
assert socket_url("http://127.0.0.1:8000", "abc") == (
"ws://127.0.0.1:8000/api/v1/flows/ws?token=abc"
)
assert socket_url("http://127.0.0.1:8000") == "ws://127.0.0.1:8000/api/v1/flows/ws"
# TLS on the one side is TLS on the other.
assert socket_url("https://engine.example.com", "t").startswith("wss://")
assert socket_url("https://engine.example.com").startswith("wss://")
+32
View File
@@ -579,3 +579,35 @@ def test_a_member_the_portal_vouches_for_is_adopted(
"fluksio.cloud.enroll.httpx.get", side_effect=httpx.ConnectError("no route")
):
assert user_from_token(db, stranger) is None
def test_the_tunnel_opens_the_live_socket_with_a_ticket(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
"""A browser under the portal brings a ticket, which the engine redeems."""
import json
from fluksio.cloud.connector import CloudConnector
ticket = client.post(
f"{settings.API_V1_STR}/flows/ws-ticket", headers=superuser_token_headers
).json()["ticket"]
connector = CloudConnector(client.app)
frame = {"id": "s1", "path": "/api/v1/flows/ws", "query": f"ticket={ticket}"}
async def opened() -> list[str]:
sent: list[str] = []
class Socket:
async def send(self, text: str) -> None:
sent.append(text)
task = asyncio.create_task(connector._stream(Socket(), frame))
while len(sent) < 1:
await asyncio.sleep(0.01)
task.cancel()
return [str(json.loads(text)["op"]) for text in sent]
assert asyncio.run(opened())[0] == "ws_open_ok"
# Used once: the same ticket again is refused.
assert asyncio.run(opened()) == ["ws_close"]