feat: add setup, browser login, and an install homepage

setup registers or logs in and writes ~/.verae/auth.properties
without printing the JWT. A caller bearer is used for stamp and
verify instead of the host login. VERAE_MCP_OAUTH=1 serves the
browser account page. site/index.html is the public install guide.
This commit is contained in:
George Lambert 2026-09-23 12:04:15 -04:00
commit c3e0cd3abb
2 changed files with 974 additions and 105 deletions

102
tests/test_oauth_login.py Normal file
View file

@ -0,0 +1,102 @@
"""Browser account page issues a VeraeTime JWT without storing it on the host."""
import pytest
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyUrl
from starlette.requests import Request
from verae_mcp_timestamping_server.global_client import GlobalClient, request_token
from verae_mcp_timestamping_server.ledger import SHA256_EMPTY
from verae_mcp_timestamping_server.oauth_login import VeraeOAuthProvider
from test_global_client import _auth_http
pytestmark = pytest.mark.anyio
def _request(method: str, body: bytes = b"", query: bytes = b"") -> Request:
async def receive():
return {"type": "http.request", "body": body, "more_body": False}
scope = {
"type": "http",
"http_version": "1.1",
"method": method,
"scheme": "http",
"path": "/account",
"raw_path": b"/account",
"query_string": query,
"headers": [(b"content-type", b"application/x-www-form-urlencoded")],
"client": ("127.0.0.1", 1234),
"server": ("127.0.0.1", 8000),
}
return Request(scope, receive)
async def test_account_form_issues_code_and_leaves_host_token(tmp_path):
http = _auth_http()
api = GlobalClient(
"https://api.veraetime.net",
token="host-token",
http=http,
auth_path=tmp_path / "auth.properties",
)
provider = VeraeOAuthProvider(api, "http://127.0.0.1:8000")
client = OAuthClientInformationFull(
client_id="assistant",
redirect_uris=[AnyUrl("http://127.0.0.1/callback")],
)
await provider.register_client(client)
params = AuthorizationParams(
state="st",
scopes=["stamp"],
code_challenge="challenge",
redirect_uri=AnyUrl("http://127.0.0.1/callback"),
redirect_uri_provided_explicitly=True,
)
url = await provider.authorize(client, params)
txn = url.split("txn=", 1)[1]
response = await provider.handle_account(
_request(
"POST",
body=f"txn={txn}&username=newcomer&password=secret-pass&mode=signup".encode(),
)
)
assert response.status_code == 302
location = response.headers["location"]
assert location.startswith("http://127.0.0.1/callback?")
assert "code=" in location
assert "fresh-token" not in location
assert api.token == "host-token"
assert not (tmp_path / "auth.properties").exists()
code = location.split("code=", 1)[1].split("&", 1)[0]
loaded = await provider.load_authorization_code(client, code)
assert loaded is not None
issued = await provider.exchange_authorization_code(client, loaded)
assert issued.access_token == "fresh-token"
assert issued.refresh_token
seen = await provider.load_access_token("fresh-token")
assert seen is not None
assert seen.subject == "admin"
async def test_request_token_does_not_refresh_host_login(tmp_path):
path = tmp_path / "auth.properties"
path.write_text(
"access_token=old-token\n"
"expires_at=2020-01-01T00:00:00Z\n"
"username=admin\n"
"password=secret-pass\n"
)
http = _auth_http()
client = GlobalClient("https://api.veraetime.net", http=http, auth_path=path)
reset = request_token.set("caller-token")
try:
rec = client.submit(SHA256_EMPTY)
finally:
request_token.reset(reset)
assert rec.receipt_id == "QmFresh"
assert http.calls[0][1].endswith("/api/timestamp")
assert http.calls[0][3]["Authorization"] == "Bearer caller-token"
assert client.token == "old-token"

39
tests/test_setup.py Normal file
View file

@ -0,0 +1,39 @@
"""First-run setup stores a login and does not print the JWT."""
from verae_mcp_timestamping_server.global_client import GlobalClient
from verae_mcp_timestamping_server.setup_cmd import login_hint, run_setup
from test_global_client import FakeHttp, _auth_http
def test_setup_signup_writes_token_file_without_printing_it(tmp_path, capsys):
path = tmp_path / "auth.properties"
http = _auth_http()
client = GlobalClient("https://api.veraetime.net", http=http, auth_path=path)
code = run_setup(
["--signup", "--username", "newcomer", "--password", "secret-pass"],
client=client,
)
assert code == 0
text = capsys.readouterr().out
assert "fresh-token" not in text
assert "username=admin" in text
assert "role=admin" in text
assert f"Wrote {path}" in text
stored = path.read_text()
assert "access_token=fresh-token" in stored
assert path.stat().st_mode & 0o777 == 0o600
assert any(url.endswith("/auth/signup") for _, url, _, _ in http.calls)
assert any(url.endswith("/auth/login") for _, url, _, _ in http.calls)
def test_login_hint_only_when_missing(tmp_path):
empty = GlobalClient("https://api.veraetime.net", http=FakeHttp(), auth_path=tmp_path / "missing.properties")
assert "uvx verae-mcp-timestamping-server setup" in (login_hint(empty) or "")
signed = GlobalClient(
"https://api.veraetime.net",
token="secret",
http=FakeHttp(),
auth_path=tmp_path / "auth.properties",
)
assert login_hint(signed) is None