Files
library-desk/tests/test_browser_auth.py
T
jpmschweitzerandClaude Fable 5 c5f90cdb4f
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 2m12s
feat(auth): session/proxy auth for Wiki.js buttons; drop browser API key
The wikijs-integration.js embedded a full-privilege API key that was
served to every wiki visitor — it unlocked all 66 authenticated
endpoints, including page/vector deletes and index purges. That key
has been rotated out of service.

The two browser endpoints (/ingest/page, /entity-linking/link-page)
now authenticate via the NPM /library-desk/ proxy location instead of a
key: Authentik forward-auth for external users, LAN bypass for internal,
verified by a trusted proxy marker header. This is safe because
library-desk binds loopback-only, so NPM is the sole path that can set
that header. The browser holds no secret; the script calls same-origin
with credentials. Machine callers (the Scheduler) keep the Bearer key
on the container-network endpoints. verify_api_key now compares in
constant time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 09:02:02 +02:00

58 lines
2.0 KiB
Python

"""
Tests for verify_browser_request — the session/proxy auth used by the
Wiki.js integration endpoints (no secret in the browser).
"""
import pytest
from types import SimpleNamespace
from fastapi import HTTPException
from starlette.requests import Request
from src.core.dependencies import verify_browser_request
def _request(headers: dict) -> Request:
raw = [(k.lower().encode(), v.encode()) for k, v in headers.items()]
return Request({"type": "http", "method": "POST", "path": "/ingest/page", "headers": raw})
SETTINGS = SimpleNamespace(library_api_key="server-secret-key")
@pytest.mark.asyncio
async def test_proxy_marker_with_authentik_identity_is_accepted():
req = _request({"X-Library-Desk-Proxy": "1", "X-Authentik-Email": "user@example.com"})
assert await verify_browser_request(req, SETTINGS) == "user@example.com"
@pytest.mark.asyncio
async def test_proxy_marker_on_lan_bypass_falls_back_to_lan():
# LAN bypass: proxy marker present, no Authentik identity headers.
req = _request({"X-Library-Desk-Proxy": "1"})
assert await verify_browser_request(req, SETTINGS) == "lan"
@pytest.mark.asyncio
async def test_valid_api_key_is_accepted_for_machine_callers():
req = _request({"Authorization": "Bearer server-secret-key"})
assert await verify_browser_request(req, SETTINGS) == "server-secret-key"
@pytest.mark.asyncio
async def test_no_marker_and_no_key_is_rejected():
with pytest.raises(HTTPException) as exc:
await verify_browser_request(_request({}), SETTINGS)
assert exc.value.status_code == 401
@pytest.mark.asyncio
async def test_forged_marker_value_is_rejected():
# Only the exact NPM-set value "1" is trusted.
with pytest.raises(HTTPException):
await verify_browser_request(_request({"X-Library-Desk-Proxy": "yes"}), SETTINGS)
@pytest.mark.asyncio
async def test_wrong_api_key_is_rejected():
with pytest.raises(HTTPException):
await verify_browser_request(_request({"Authorization": "Bearer wrong"}), SETTINGS)