Files
treg/tests/test_mcp_oauth.py
SToneX 52998b475a test(cli): prune help-text, argparse and duplicate MCP/CLI tests
Replace the audit busy-wait in agent capture tests with audit.drain().
2026-09-25 21:09:53 +08:00

1370 lines
70 KiB
Python

"""treg as an OAuth authorization server — the metadata, the refusals, and client registration.
Nothing issues tokens yet. This file covers the half that says NO, deliberately built first: there is
never a window where the server accepts tokens it has not learned to check.
The assertion that matters most is the audience one. Everything else here — signature, expiry, type —
is ordinary token hygiene that any login system needs. `aud` is the one that exists because we are a
RESOURCE server: a user grants a client access to a named resource, and a token addressed to some
other MCP server must not spend a treg balance just because it happens to be validly signed by us.
"""
from __future__ import annotations
from conftest import verified_signup
import json
import pytest
from treg import mcp
from treg.domain.identity import mcp_oauth, session
from treg.routers import auth as auth_routes
from treg.config import Settings, get_settings
# The MCP transport helpers live with the MCP tests; a token is only interesting here because it can
# drive a tool, so reuse them rather than keeping a second copy that can drift.
from test_mcp import _call_tool, mcp_session
# ---- metadata: what we tell a client we support --------------------------------------------
async def test_protected_resource_metadata_names_the_mcp_endpoint(clients):
r = await clients.get("/.well-known/oauth-protected-resource")
assert r.status_code == 200
body = r.json()
assert body["resource"] == mcp_oauth.mcp_resource_url()
assert mcp_oauth.DIRECTORY_SCOPE not in body["scopes_supported"]
assert body["resource"].endswith("/mcp/"), "the trailing slash is part of the identifier"
assert body["authorization_servers"], "a client must be told who issues tokens for us"
async def test_the_metadata_is_served_at_BOTH_lookup_paths(clients):
"""The spec has clients look this up either at the host root or under the resource's own path,
and which one a given client tries is not ours to choose."""
a = await clients.get("/.well-known/oauth-protected-resource")
b = await clients.get("/.well-known/oauth-protected-resource/mcp")
assert a.status_code == b.status_code == 200
assert a.json() == b.json()
async def test_v2_metadata_names_only_the_directory_resource(clients):
response = await clients.get("/.well-known/oauth-protected-resource/mcp/v2")
assert response.status_code == 200
assert response.json()["resource"] == mcp_oauth.mcp_resource_url("v2")
assert response.json()["resource"].endswith("/mcp/v2/")
assert response.json()["resource"] != mcp_oauth.mcp_resource_url("v1")
assert mcp_oauth.DIRECTORY_SCOPE in response.json()["scopes_supported"]
async def test_authorization_server_metadata_offers_only_safe_choices(clients):
"""OAuth 2.1 drops the implicit grant, and `plain` PKCE makes the challenge equal to the secret —
anyone who sees the authorization request could redeem the code. Offering either would be a
downgrade a client is entitled to take us up on."""
body = (await clients.get("/.well-known/oauth-authorization-server")).json()
assert body["response_types_supported"] == ["code"]
assert body["code_challenge_methods_supported"] == ["S256"]
assert "plain" not in body["code_challenge_methods_supported"]
assert body["authorization_endpoint"].endswith("/oauth/authorize")
assert body["token_endpoint"].endswith("/oauth/token")
assert mcp_oauth.DIRECTORY_SCOPE in body["scopes_supported"]
async def test_metadata_advertises_BOTH_ways_for_a_client_to_identify_itself(clients):
"""ChatGPT sends a client-id metadata document; Claude Code and most others register
dynamically. Advertising only one would lock the other out — and we would not notice, because
the client we test with is the one that does not need registration."""
body = (await clients.get("/.well-known/oauth-authorization-server")).json()
assert body["client_id_metadata_document_supported"] is True
assert body["registration_endpoint"].endswith("/oauth/register")
# ---- the refusals ---------------------------------------------------------------------------
async def test_a_token_for_ANOTHER_resource_is_refused():
"""The load-bearing check. A user consented to some other MCP server; that grant must not spend
a treg balance merely because we signed the token."""
ours = mcp_oauth.mcp_resource_url()
elsewhere = mcp_oauth.make_access_token(user_id=7, org_id=3,
audience="https://evil.example.com/mcp/")
assert mcp_oauth.read_access_token(elsewhere, expected_audience=ours) is None
assert mcp._oauth_claims(elsewhere) is None
async def test_a_session_cookie_is_not_an_access_token():
"""treg mints session cookies and identity tokens with the same HMAC construction. Without a type
marker one class of credential would silently validate as another — a browser session becoming an
MCP grant, which nobody consented to."""
cookie = session.make_session(7)
assert mcp._oauth_claims(cookie) is None
assert mcp_oauth.read_access_token(cookie, expected_audience=mcp_oauth.mcp_resource_url()) is None
async def test_a_tampered_token_is_refused():
ours = mcp_oauth.mcp_resource_url()
tok = mcp_oauth.make_access_token(user_id=7, org_id=3, audience=ours)
payload, sig = tok.split(".", 1)
# same signature, different claims: the forgery a signature exists to stop
forged = mcp_oauth.make_access_token(user_id=99, org_id=99, audience=ours).split(".", 1)[0]
assert mcp_oauth.read_access_token(f"{forged}.{sig}", expected_audience=ours) is None
@pytest.mark.parametrize("junk", ["", "not-a-token", "a.b", "....", "Bearer x"])
async def test_malformed_input_is_simply_not_a_token(junk):
assert mcp_oauth.read_access_token(junk, expected_audience=mcp_oauth.mcp_resource_url()) is None
async def test_the_audience_check_cannot_be_skipped_by_accident():
"""`expected_audience` has no default, and an empty one refuses rather than matching everything.
The single thing that must never happen quietly is not performing this check."""
tok = mcp_oauth.make_access_token(user_id=7, org_id=3, audience="")
assert mcp_oauth.read_access_token(tok, expected_audience="") is None
with pytest.raises(TypeError):
mcp_oauth.read_access_token(tok) # type: ignore[call-arg]
# ---- PKCE ------------------------------------------------------------------------------------
async def test_pkce_accepts_the_right_verifier_and_nothing_else():
import base64
import hashlib
verifier = "a-random-high-entropy-string-from-the-client"
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
assert mcp_oauth.verify_pkce(verifier, challenge)
assert not mcp_oauth.verify_pkce("some-other-verifier", challenge)
assert not mcp_oauth.verify_pkce("", challenge)
assert not mcp_oauth.verify_pkce(verifier, "")
# ---- step 2: client registration — two doors in, one row shape out --------------------------
async def test_dynamic_registration_mints_a_client(clients):
"""RFC 7591, unauthenticated by design: registering grants nothing on its own, because every
token still needs a human to approve at the consent screen."""
r = await clients.post("/oauth/register", json={
"client_name": "Claude Code",
"redirect_uris": ["http://127.0.0.1:8976/callback"]})
assert r.status_code == 201, r.text
body = r.json()
assert body["client_id"].startswith("treg-client-")
assert body["redirect_uris"] == ["http://127.0.0.1:8976/callback"]
assert body["token_endpoint_auth_method"] == "none" # public client + PKCE
async def test_registration_requires_a_usable_redirect_uri(clients):
"""An authorization code is delivered to the redirect URI. A client with none, or with one an
eavesdropper could read, has nowhere safe to receive it."""
for uris in ([], ["http://evil.example.com/cb"], ["ftp://x/cb"], ["https://a.test/cb#frag"]):
r = await clients.post("/oauth/register",
json={"client_name": "x", "redirect_uris": uris})
assert r.status_code == 400, f"{uris} should be refused, got {r.status_code}"
assert r.json()["error"] == "invalid_redirect_uri"
async def test_loopback_http_is_allowed_because_a_CLI_cannot_hold_a_certificate(clients):
for uri in ("http://127.0.0.1:1234/cb", "http://localhost:9999/cb"):
r = await clients.post("/oauth/register", json={"client_name": "cli", "redirect_uris": [uri]})
assert r.status_code == 201, uri
async def test_redirect_matching_is_EXACT_not_prefix():
"""The classic defeat of this check. `https://good.test/cb.evil` starts with the registered URI,
and an open redirect under a registered host turns one sloppy page into stolen codes."""
from treg.domain.identity.mcp_oauth import redirect_uri_allowed
class C:
redirect_uris = ["https://good.test/cb"]
assert redirect_uri_allowed(C(), "https://good.test/cb")
for evil in ("https://good.test/cb.evil", "https://good.test/cb/../x", "https://good.test/CB",
"https://good.test.evil/cb", ""):
assert not redirect_uri_allowed(C(), evil), evil
async def test_loopback_redirect_allows_ANY_port_but_not_other_drift():
"""RFC 8252 §7.3: a native/CLI client (Claude Code, Codex, Cursor) registers a PORTLESS loopback
URI and sends the ephemeral port it actually bound at authorize time. The AS MUST allow the port
to vary — treg's pure exact-match rejected `http://localhost:3118/callback` against a registered
`http://localhost/callback` and broke every native client. Port varies; scheme, host and path do
NOT, and the loopback exception must not leak to public hosts."""
from treg.domain.identity.mcp_oauth import redirect_uri_allowed
class C: # exactly what Claude Code's client-id metadata document registers
redirect_uris = ["http://localhost/callback", "http://127.0.0.1/callback"]
# the real failing case + variants that MUST now pass
assert redirect_uri_allowed(C(), "http://localhost:3118/callback")
assert redirect_uri_allowed(C(), "http://127.0.0.1:52713/callback")
assert redirect_uri_allowed(C(), "http://localhost/callback") # portless still fine
# but the exception is loopback-only and path-exact — these MUST still be refused
for bad in ("http://localhost:3118/evil", # wrong path
"http://localhost.evil/callback", # not actually loopback
"http://evil.test:3118/callback", # public host — no port flexibility
"https://localhost:3118/callback", # scheme drift (registered is http)
"http://[::1]:9/other"): # loopback but wrong path
assert not redirect_uri_allowed(C(), bad), bad
# ---- the CIMD fetch: a URL the caller chose, so it must be fenced ---------------------------
@pytest.mark.parametrize("url", [
"http://example.com/cimd.json", # not https
"https://127.0.0.1/cimd.json", # loopback
"https://169.254.169.254/latest/meta", # cloud metadata — the classic SSRF target
"https://10.0.0.5/cimd.json", # private
"https://localhost/cimd.json",
"not-a-url",
])
async def test_cimd_refuses_unsafe_urls(url):
"""`client_id` is a URL our SERVER fetches, which makes it a request-forgery primitive unless
fenced. Reuses the same guard the webhook and tool base_url paths already use."""
from treg.domain.identity.mcp_oauth import fetch_client_id_metadata
assert await fetch_client_id_metadata(url) is None
async def test_cimd_document_must_claim_its_own_url(monkeypatch):
"""Without this, a document hosted anywhere could assert somebody else's client_id and inherit
the consent users granted them."""
import httpx
from treg import health
from treg.domain.identity import mcp_oauth
monkeypatch.setattr(health, "safe_webhook_url", lambda u: True)
monkeypatch.setattr(health, "host_is_public", lambda h: True)
async def fake_get(self, url, **kw):
return httpx.Response(200, json={"client_id": "https://someone-else.test/cimd.json",
"client_name": "impostor",
"redirect_uris": ["https://impostor.test/cb"]},
request=httpx.Request("GET", url))
monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
assert await mcp_oauth.fetch_client_id_metadata("https://real.test/cimd.json") is None
async def test_cimd_accepts_a_well_formed_document(monkeypatch):
"""Not vacuous: the refusals above only mean something if a good document DOES load."""
import httpx
from treg import health
from treg.domain.identity import mcp_oauth
monkeypatch.setattr(health, "safe_webhook_url", lambda u: True)
monkeypatch.setattr(health, "host_is_public", lambda h: True)
async def fake_get(self, url, **kw):
return httpx.Response(200, json={"client_id": url, "client_name": "ChatGPT",
"redirect_uris": ["https://chatgpt.com/connector/oauth/x",
"http://evil.test/cb"]},
request=httpx.Request("GET", url))
monkeypatch.setattr(httpx.AsyncClient, "get", fake_get)
doc = await mcp_oauth.fetch_client_id_metadata("https://chatgpt.com/cimd.json")
assert doc is not None
assert doc["client_name"] == "ChatGPT"
# the unsafe redirect in the document is dropped rather than accepted alongside the good one
assert doc["redirect_uris"] == ["https://chatgpt.com/connector/oauth/x"]
# ---- step 3: the authorization code flow ----------------------------------------------------
def _pkce():
import base64
import hashlib
import secrets
verifier = secrets.token_urlsafe(48)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
return verifier, challenge
async def _register(clients, redirect="https://client.test/cb"):
r = await clients.post("/oauth/register",
json={"client_name": "Test Client", "redirect_uris": [redirect]})
assert r.status_code == 201, r.text
return r.json()["client_id"]
async def _signed_in(clients, email="oauth-user@superdesign.dev"):
"""A browser session plus the org it belongs to. The consent step is a HUMAN action, so it needs
a session cookie rather than a token."""
r = await verified_signup(clients, json={"email": email})
assert r.status_code == 200, r.text
token = r.json()["token"]
prev = clients.headers.get("X-Treg-Token")
clients.headers["X-Treg-Token"] = token
org_id = (await clients.get("/orgs")).json()[0]["org_id"]
me = (await clients.get("/auth/me")).json()
if prev:
clients.headers["X-Treg-Token"] = prev
from sqlmodel import select
from treg.domain.identity import session as _sess
from treg.infra.db import session_maker
from treg.models import User
async with session_maker() as db:
user = (await db.execute(select(User).where(User.email == me["email"]))).scalar_one()
cookie = _sess.make_session(user.id, token_version=user.token_version)
clients.cookies.set("treg_session", cookie) # on the client: httpx deprecates per-request
return cookie, org_id
async def test_the_whole_flow_end_to_end(clients):
"""Register, authorize, approve, exchange — and the token that comes out is one our MCP server
accepts, for the team the human chose."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients)
verifier, challenge = _pkce()
params = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "state": "xyz",
"resource": mcp_oauth.mcp_resource_url(), "scope": "treg:call"}
shown = await clients.get("/oauth/authorize", params=params,
headers={"Accept": "application/json"})
assert shown.status_code == 200, shown.text
assert shown.json()["client"]["name"] == "Test Client"
assert any(t["org_id"] == org_id for t in shown.json()["teams"]), "the team picker must offer it"
approved = await clients.post("/oauth/authorize", data={**params, "org_id": org_id}, follow_redirects=False)
assert approved.status_code == 302
loc = approved.headers["location"]
assert loc.startswith("https://client.test/cb?") and "state=xyz" in loc
code = loc.split("code=")[1].split("&")[0]
tok = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier, "resource": mcp_oauth.mcp_resource_url()})
assert tok.status_code == 200, tok.text
assert tok.headers["cache-control"] == "no-store"
assert tok.headers["pragma"] == "no-cache"
access = tok.json()["access_token"]
assert tok.json()["token_type"] == "Bearer"
claims = mcp._oauth_claims(access)
assert claims is not None, "the MCP server must accept what we just issued"
assert claims["org"] == org_id, "the token spends from the team the human picked"
async def test_v2_dcr_flow_mints_only_a_v2_audience(clients):
"""The directory resource reuses DCR, PKCE and the team picker, but not the legacy audience."""
client_id = await _register(clients)
_, org_id = await _signed_in(clients, "oauth-v2@superdesign.dev")
verifier, challenge = _pkce()
resource = mcp_oauth.mcp_resource_url("v2")
params = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "state": "v2", "resource": resource,
"scope": "treg:call"}
shown = await clients.get("/oauth/authorize", params=params,
headers={"Accept": "application/json"})
assert shown.status_code == 200
assert any(team["org_id"] == org_id for team in shown.json()["teams"])
approved = await clients.post("/oauth/authorize", data={**params, "org_id": org_id},
follow_redirects=False)
assert approved.status_code == 302
code = approved.headers["location"].split("code=")[1].split("&")[0]
token = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier, "resource": resource,
})
assert token.status_code == 200, token.text
access = token.json()["access_token"]
assert mcp_oauth.read_access_token_any(access, "v2") is not None
assert mcp_oauth.read_access_token_any(access, "v1") is None
async def test_v2_scope_selects_v2_when_claude_omits_the_resource(clients):
"""Hosted Claude omitted RFC 8707 resource but kept the V2 challenge scopes in production."""
client_id = await _register(clients)
_, org_id = await _signed_in(clients, "oauth-v2-no-resource@superdesign.dev")
verifier, challenge = _pkce()
scope = " ".join(mcp_oauth.scopes_for_resource("v2"))
params = {
"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "state": "v2-no-resource", "scope": scope,
}
shown = await clients.get("/oauth/authorize", params=params,
headers={"Accept": "application/json"})
assert shown.status_code == 200
approved = await clients.post("/oauth/authorize", data={**params, "org_id": org_id},
follow_redirects=False)
assert approved.status_code == 302
code = approved.headers["location"].split("code=")[1].split("&")[0]
token = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier,
})
assert token.status_code == 200, token.text
access = token.json()["access_token"]
assert mcp_oauth.read_access_token_any(access, "v2") is not None
assert mcp_oauth.read_access_token_any(access, "v1") is None
def test_explicit_resource_wins_over_the_v2_scope_marker():
v1 = mcp_oauth.mcp_resource_url("v1")
assert auth_routes._effective_mcp_resource(v1, mcp_oauth.DIRECTORY_SCOPE) == v1
async def test_a_code_can_be_redeemed_only_ONCE(clients):
"""The row is deleted on redemption rather than flagged, so a replay finds nothing. A used code
that still exists is a race waiting for two redemptions to read it before either writes."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "once@superdesign.dev")
verifier, challenge = _pkce()
params = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "resource": mcp_oauth.mcp_resource_url()}
r = await clients.post("/oauth/authorize", data={**params, "org_id": org_id}, follow_redirects=False)
code = r.headers["location"].split("code=")[1].split("&")[0]
body = {"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier}
assert (await clients.post("/oauth/token", data=body)).status_code == 200
second = await clients.post("/oauth/token", data=body)
assert second.status_code == 400 and second.json()["error"] == "invalid_grant"
async def test_the_WRONG_verifier_cannot_redeem_a_stolen_code(clients):
"""The whole point of PKCE: a code intercepted in the browser redirect is worthless without the
verifier, which never left the client."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "pkce@superdesign.dev")
_, challenge = _pkce()
other_verifier, _ = _pkce()
r = await clients.post("/oauth/authorize", data={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "org_id": org_id,
"resource": mcp_oauth.mcp_resource_url()}, follow_redirects=False)
code = r.headers["location"].split("code=")[1].split("&")[0]
tok = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": other_verifier})
assert tok.status_code == 400 and tok.json()["error"] == "invalid_grant"
async def test_an_unregistered_redirect_is_refused_WITHOUT_redirecting(clients):
"""Bouncing an error to an unvalidated URI would be an open redirect, and would hand `state` to
whoever asked for it. So this is a flat 400, not a 302."""
client_id = await _register(clients)
r = await clients.get("/oauth/authorize", params={
"client_id": client_id, "redirect_uri": "https://attacker.test/steal",
"response_type": "code", "code_challenge": "x", "code_challenge_method": "S256"},
follow_redirects=False)
assert r.status_code == 400
assert r.json()["error"] == "invalid_request"
async def test_pkce_is_mandatory(clients):
"""A client that omits the challenge, or offers `plain`, is refused — otherwise a downgrade is
available to anyone who asks for it."""
client_id = await _register(clients)
for extra in ({}, {"code_challenge": "abc", "code_challenge_method": "plain"}):
r = await clients.get("/oauth/authorize", params={
"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", **extra}, follow_redirects=False)
assert r.status_code == 302 and "error=invalid_request" in r.headers["location"]
async def test_you_cannot_approve_for_a_team_you_are_not_in(clients):
"""`org_id` arrives from the browser, so it is client-supplied. Trusting it would let anyone
grant a client access to any team by editing one form field."""
client_id = await _register(clients)
cookie, _ = await _signed_in(clients, "outsider@superdesign.dev")
other = await clients.post("/users", json={"email": "stranger@superdesign.dev"})
prev = clients.headers.get("X-Treg-Token")
clients.headers["X-Treg-Token"] = other.json()["token"]
foreign_org = (await clients.get("/orgs")).json()[0]["org_id"]
if prev:
clients.headers["X-Treg-Token"] = prev
_, challenge = _pkce()
r = await clients.post("/oauth/authorize", data={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "org_id": foreign_org}, follow_redirects=False)
assert r.status_code == 302 and "error=access_denied" in r.headers["location"]
async def test_a_GET_never_grants_anything(clients):
"""Approval is a POST. A GET that granted access could be triggered by any page able to make the
browser navigate — even with `org_id` supplied, as here."""
from sqlmodel import select
from treg.infra.db import session_maker
from treg.models import OAuthCode
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "getonly@superdesign.dev")
_, challenge = _pkce()
r = await clients.get("/oauth/authorize", params={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "org_id": org_id},
follow_redirects=False)
assert r.status_code == 200, "it renders the question"
assert "location" not in r.headers, "and never redirects back with a code"
async with session_maker() as db:
codes = (await db.execute(select(OAuthCode).where(
OAuthCode.client_id == client_id))).scalars().all()
assert codes == [], "a GET must not have minted anything"
# ---- step 4: the consent screen — the only place a human sees what they grant ----------------
async def test_the_consent_page_says_what_it_costs_in_WORDS(clients):
"""A screen that lists `treg:call` and calls that informed consent is a formality. The two things
that actually cost money or expose data have to be legible: this client can spend the team's
balance, and can use keys the team registered."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "consent@superdesign.dev")
_, challenge = _pkce()
r = await clients.get("/oauth/authorize", params={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256"})
assert r.status_code == 200
page = r.text
assert "text/html" in r.headers["content-type"]
assert "Test Client" in page, "the human must see WHICH application is asking"
assert "consent@superdesign.dev" in page, "and which account they are granting from"
assert "spends the team's balance" in page
assert "without seeing them" in page, "keys are used, never revealed — say so"
assert 'name="org_id"' in page, "the team picker belongs here"
# Dynamic registration is open by design, so anyone can appear here with any name. The user is
# the only one who can tell whether they started this, and they can only judge if we say so.
assert "registered itself" in page
assert "only continue if you recognise it" in page
async def test_CANCEL_tells_the_client_no_rather_than_hanging(clients):
"""Declining is a real answer. A client left waiting on a redirect that never comes is a worse
outcome than a clean refusal."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "declines@superdesign.dev")
_, challenge = _pkce()
r = await clients.post("/oauth/authorize", data={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "org_id": org_id,
"state": "s1", "decision": "deny"}, follow_redirects=False)
assert r.status_code == 302
assert "error=access_denied" in r.headers["location"] and "state=s1" in r.headers["location"]
assert "code=" not in r.headers["location"]
async def test_a_CROSS_ORIGIN_submission_is_refused(clients):
"""The consent form is the security boundary. Without this, a page anywhere could auto-submit and
grant itself a team's balance — the user is signed in, so their cookie rides along."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "csrf@superdesign.dev")
_, challenge = _pkce()
r = await clients.post("/oauth/authorize", data={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "org_id": org_id,
"decision": "allow"},
headers={"Origin": "https://attacker.test"}, follow_redirects=False)
assert r.status_code == 403
async def test_the_consent_page_cannot_be_framed(clients):
"""Clickjacking: an invisible frame over a decoy page turns "Allow" into a click the user thought
was something else. treg sets this globally; asserting it here is what notices if that changes."""
client_id = await _register(clients)
cookie, _ = await _signed_in(clients, "framed@superdesign.dev")
_, challenge = _pkce()
r = await clients.get("/oauth/authorize", params={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256"})
assert r.headers.get("X-Frame-Options") == "DENY"
# ---- the token has to WORK, not merely validate ---------------------------------------------
async def _grant(clients, email):
"""Run the whole flow and return the access token, plus the team it was granted for."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, email)
verifier, challenge = _pkce()
params = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "resource": mcp_oauth.mcp_resource_url()}
r = await clients.post("/oauth/authorize", data={**params, "org_id": org_id, "decision": "allow"},
follow_redirects=False)
code = r.headers["location"].split("code=")[1].split("&")[0]
tok = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier})
assert tok.status_code == 200, tok.text
return tok.json()["access_token"], org_id
async def test_an_oauth_token_actually_CALLS_the_tools(clients):
"""The gap running it found and the unit tests missed: `_oauth_claims` validated a token
perfectly while every tool still forwarded the raw bearer to the internal API, which has never
heard of an OAuth token — so all three authenticated tools answered "not signed in".
Validating a credential and being able to USE it are different claims, and only this one matters
to a user.
"""
access, org_id = await _grant(clients, "usable@superdesign.dev")
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=access)
assert "balance_usd" in out, out
assert "not signed in" not in json.dumps(out)
async def test_the_token_spends_from_the_team_ON_THE_GRANT(clients):
"""A person in several teams answered "which one" at the consent screen. That answer travels on
the token, and nothing downstream may re-derive it — re-deriving is how the wrong team's balance
gets spent, and `balance` used to refuse and ask precisely because it had no answer."""
access, granted_org = await _grant(clients, "multi@superdesign.dev")
# a second team for the same person, deliberately created AFTER the grant
made = await clients.post("/orgs", json={"name": "second-team-after-grant"})
assert made.status_code == 200, made.text
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=access)
assert "balance_usd" in out, out
teams = (await clients.get("/orgs")).json()
granted_slug = next(t["slug"] for t in teams if t["org_id"] == granted_org)
assert out["team"] == granted_slug, "the grant's team, not a freshly-guessed one"
async def test_revoking_your_tokens_kills_an_existing_grant(clients):
"""`token_version` is treg's kill switch for a leaked credential. An OAuth grant must honour it
too, or the one button a user has for "make it stop" would quietly miss this door."""
access, _ = await _grant(clients, "revoker@superdesign.dev")
async with mcp_session(clients) as c:
before = await _call_tool(c, "balance", {}, token=access)
assert "balance_usd" in before
from sqlmodel import select
from treg.infra.db import session_maker
from treg.models import User
async with session_maker() as db:
user = (await db.execute(
select(User).where(User.email == "revoker@superdesign.dev"))).scalar_one()
user.token_version += 1
db.add(user)
await db.commit()
async with mcp_session(clients) as c:
after = await _call_tool(c, "balance", {}, token=access)
assert "balance_usd" not in after, "a revoked user's grant must stop working"
# ---- step 5: refresh tokens, rotation, and catching a replay --------------------------------
async def _grant_full(clients, email):
"""The whole flow, returning the token response so the refresh token is visible."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, email)
verifier, challenge = _pkce()
params = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "resource": mcp_oauth.mcp_resource_url()}
r = await clients.post("/oauth/authorize", data={**params, "org_id": org_id, "decision": "allow"},
follow_redirects=False)
code = r.headers["location"].split("code=")[1].split("&")[0]
tok = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier})
assert tok.status_code == 200, tok.text
return tok.json(), client_id, org_id
async def test_refreshing_ROTATES_the_token_and_the_new_one_works(clients):
"""The old token is spent, the new one carries on, and the access token that comes out still
drives the tools — a refresh that returned an unusable token would fail silently an hour later."""
body, client_id, _ = await _grant_full(clients, "rotate@superdesign.dev")
first = body["refresh_token"]
assert first and body["expires_in"] == mcp_oauth.ACCESS_TTL_SECONDS
r = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": first, "client_id": client_id})
assert r.status_code == 200, r.text
second = r.json()["refresh_token"]
assert second and second != first, "a refresh must MINT a replacement, not hand back the same one"
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=r.json()["access_token"])
assert "balance_usd" in out, out
async def test_replaying_a_SPENT_refresh_token_kills_the_whole_family(clients):
"""The reason the retired row is kept rather than deleted. A spent token being presented again
means either a client retried after a dropped response or somebody else has a copy — and those
are indistinguishable from here. Assume the worse one: the cost of being wrong is a sign-in, and
the cost of the other mistake is somebody's balance."""
body, client_id, _ = await _grant_full(clients, "replay@superdesign.dev")
first = body["refresh_token"]
ok = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": first, "client_id": client_id})
assert ok.status_code == 200
second = ok.json()["refresh_token"]
replay = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": first, "client_id": client_id})
assert replay.status_code == 400
assert "already used" in replay.json()["error_description"]
# and the descendant is dead too — containment is the point, not just refusing the replay
after = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": second, "client_id": client_id})
assert after.status_code == 400, "the whole family must be revoked, not only the replayed token"
async def test_a_refresh_cannot_quietly_change_teams(clients):
"""The team was chosen by a human at consent. A refresh renews that grant; it is not a second
chance to pick, and nothing in the request may influence it."""
body, client_id, org_id = await _grant_full(clients, "steady@superdesign.dev")
made = await clients.post("/orgs", json={"name": "team-created-after"})
assert made.status_code == 200
r = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert r.status_code == 200
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=r.json()["access_token"])
teams = (await clients.get("/orgs")).json()
granted_slug = next(t["slug"] for t in teams if t["org_id"] == org_id)
assert out["team"] == granted_slug
async def test_a_refresh_token_is_bound_to_its_client(clients):
"""Otherwise one client's stolen refresh token is every client's."""
body, _, _ = await _grant_full(clients, "bound@superdesign.dev")
other = await _register(clients, "https://other.test/cb")
r = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"], "client_id": other})
assert r.status_code == 400 and r.json()["error"] == "invalid_grant"
async def test_revoking_ends_the_family_and_never_leaks_whether_a_token_existed(clients):
"""RFC 7009: always 200. Distinguishing a known token from an unknown one would turn this into
an oracle for guessing valid ones."""
body, client_id, _ = await _grant_full(clients, "revoke@superdesign.dev")
assert (await clients.post("/oauth/revoke", data={"token": "never-existed"})).status_code == 200
assert (await clients.post("/oauth/revoke",
data={"token": body["refresh_token"]})).status_code == 200
dead = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert dead.status_code == 400
async def test_refresh_tokens_are_stored_HASHED(clients):
"""A database copy is a database leak, and the refresh token is the long-lived half — the one
worth stealing."""
from sqlmodel import select
from treg.infra.db import session_maker
from treg.models import OAuthRefresh
body, _, _ = await _grant_full(clients, "hashed@superdesign.dev")
async with session_maker() as db:
rows = (await db.execute(select(OAuthRefresh))).scalars().all()
assert rows, "expected a stored refresh token"
assert all(body["refresh_token"] not in (r.token_hash or "") for r in rows)
assert all(len(r.token_hash) == 64 for r in rows), "sha256 hex, as everywhere else in treg"
# ---- step 6: what an INDEPENDENT client found ------------------------------------------------
async def test_a_resource_we_do_not_serve_is_refused_UP_FRONT(clients):
"""Found by driving the flow with the MCP SDK's own client instead of my curl.
It sent the URL it had dialled (`http://127.0.0.1:18790/mcp/`) rather than the canonical
identifier our metadata declares, which is a reasonable thing for a client to do. treg accepted
it and minted a token that was valid, well-formed and silently useless — the audience did not
match, so the first tool call answered "not signed in" and pointed the reader at authentication
when the real problem was the resource.
Refusing at authorize time turns a confusing failure an hour downstream into a clear one
immediately, and names the fix.
"""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "wrongres@superdesign.dev")
_, challenge = _pkce()
r = await clients.get("/oauth/authorize", params={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256",
"resource": "https://somewhere-else.test/mcp/"}, follow_redirects=False)
assert r.status_code == 302
assert "error=invalid_target" in r.headers["location"]
assert "well-known" in r.headers["location"], "and it must say where the right value lives"
async def test_omitting_the_resource_is_fine(clients):
"""A client that sends no `resource` gets our canonical one — which is what it would have
discovered anyway, so refusing would be pedantry rather than safety."""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "nores@superdesign.dev")
verifier, challenge = _pkce()
r = await clients.post("/oauth/authorize", data={
"client_id": client_id, "redirect_uri": "https://client.test/cb", "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "org_id": org_id,
"decision": "allow"}, follow_redirects=False)
assert r.status_code == 302 and "code=" in r.headers["location"]
code = r.headers["location"].split("code=")[1].split("&")[0]
tok = await clients.post("/oauth/token", data={
"grant_type": "authorization_code", "code": code,
"redirect_uri": "https://client.test/cb", "client_id": client_id,
"code_verifier": verifier})
assert tok.status_code == 200
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=tok.json()["access_token"])
assert "balance_usd" in out, "the default audience must be the one the MCP server accepts"
async def test_a_null_origin_from_a_redirect_chain_is_not_treated_as_cross_site(clients):
"""The intermittent failure Unclecode hit: approve worked on one attempt and was refused on
another. `Origin: null` is a browser reporting an OPAQUE origin, which happens after certain
redirect chains — a consent page reached by way of a sign-in bounce through GitHub, say. It is
not evidence of a cross-site request.
`Sec-Fetch-Site` is the corroboration that makes accepting it safe: the browser sets it and script
cannot, so another site cannot forge `same-origin`.
"""
client_id = await _register(clients)
cookie, org_id = await _signed_in(clients, "nullorigin@superdesign.dev")
_, challenge = _pkce()
body = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256", "org_id": org_id, "decision": "allow"}
ok = await clients.post("/oauth/authorize", data=body, follow_redirects=False,
headers={"Origin": "null", "Sec-Fetch-Site": "same-origin"})
assert ok.status_code == 302 and "code=" in ok.headers["location"]
# ...but `null` WITHOUT that corroboration is still refused: the guard is narrowed, not removed.
nope = await clients.post("/oauth/authorize", data=body, follow_redirects=False,
headers={"Origin": "null", "Sec-Fetch-Site": "cross-site"})
assert nope.status_code == 403
assert "Sec-Fetch-Site: cross-site" in nope.json()["detail"], "the refusal must say what it saw"
# ---- signing in mid-authorization must RETURN you to the authorization ----------------------
async def test_a_signed_out_user_is_returned_to_the_consent_screen(clients):
"""The bug ChatGPT found on its first real connect. A signed-out user clicking "Sign in with
treg" landed on the dashboard and the authorization was silently dropped.
The cause was a `?next=` query I invented and nothing implemented, so the sign-in doors simply
ignored it. The destination is now parked in a cookie and resumed at the dashboard, which is the
one point every browser door ends at.
"""
client_id = await _register(clients)
_, challenge = _pkce()
params = {"client_id": client_id, "redirect_uri": "https://client.test/cb",
"response_type": "code", "code_challenge": challenge,
"code_challenge_method": "S256"}
clients.cookies.clear()
sent_away = await clients.get("/oauth/authorize", params=params, follow_redirects=False)
assert sent_away.status_code == 302
assert sent_away.headers["location"] == "/?signin=oauth"
parked = (sent_away.cookies.get("treg_oauth_return") or "").strip('"')
assert parked.startswith("/oauth/authorize?"), f"nothing was parked: {parked!r}"
assert params["client_id"] in parked, "the parked destination must be THIS request"
# now sign in and land on the dashboard, as every browser door does
cookie, _ = await _signed_in(clients, "returner@superdesign.dev")
clients.cookies.set("treg_oauth_return", parked)
back = await clients.get("/app", follow_redirects=False)
assert back.status_code == 302, "the dashboard must resume the parked authorization"
assert back.headers["location"].startswith("/oauth/authorize?")
async def test_email_sign_in_on_the_homepage_resumes_the_parked_authorization(clients):
"""The OAuth sign-in modal opens on `/`, and the email-code door reloads that page once the code
is accepted, so the plain homepage must resume the authorization too. Only `/app` did, and an
email sign-in from an MCP client ended on the marketing page with the connection abandoned."""
clients.cookies.set("treg_oauth_return", "/oauth/authorize?client_id=x")
signed_out = await clients.get("/", follow_redirects=False)
assert signed_out.status_code == 200, "a signed-out visitor sees the homepage, not a loop"
await _signed_in(clients, "homepage-returner@superdesign.dev")
clients.cookies.set("treg_oauth_return", "/oauth/authorize?client_id=x")
back = await clients.get("/", follow_redirects=False)
assert back.status_code == 302
assert back.headers["location"] == "/oauth/authorize?client_id=x"
clients.cookies.set("treg_oauth_return", "/anything-else")
assert (await clients.get("/", follow_redirects=False)).status_code == 200
async def test_connect_demo_is_explicitly_enabled_and_never_displays_token_prefixes(
monkeypatch, clients,
):
monkeypatch.delenv("TREG_CONNECT_DEMO_ENABLED", raising=False)
assert Settings(_env_file=None).connect_demo_enabled is False, "the demo is off by default"
monkeypatch.setenv("TREG_CONNECT_DEMO_ENABLED", "false")
get_settings.cache_clear()
try:
assert (await clients.get("/connect-demo")).status_code == 404
assert (await clients.get("/connect-demo/callback")).status_code == 404
monkeypatch.setenv("TREG_CONNECT_DEMO_ENABLED", "true")
get_settings.cache_clear()
page = await clients.get("/connect-demo")
callback = await clients.get("/connect-demo/callback")
assert page.status_code == callback.status_code == 200
assert 'access_token: "[received]"' in page.text
assert 'refresh_token: "[received]"' in page.text
assert "body.access_token.slice" not in page.text
assert "body.refresh_token.slice" not in page.text
finally:
get_settings.cache_clear()
async def test_the_parked_destination_cannot_be_used_as_an_open_redirect(clients):
"""The cookie is only honoured for /oauth/authorize. Accepting any path would make it a general
"send me anywhere after login" primitive — a phishing aid rather than a feature."""
cookie, _ = await _signed_in(clients, "openredir@superdesign.dev")
for evil in ("https://attacker.test/", "//attacker.test/", "/app/../oauth/authorize?x=1",
"/anything-else"):
clients.cookies.set("treg_oauth_return", evil)
r = await clients.get("/app", follow_redirects=False)
assert r.status_code == 200, f"{evil!r} must NOT be honoured, got {r.status_code}"
clients.cookies.delete("treg_oauth_return")
async def test_a_signed_out_visitor_is_not_bounced_in_a_loop(clients):
"""Resuming only once signed in. Otherwise the dashboard would send them to /oauth/authorize,
which would send them back here, forever."""
clients.cookies.clear()
clients.cookies.set("treg_oauth_return", "/oauth/authorize?client_id=x")
r = await clients.get("/app", follow_redirects=False)
assert r.status_code == 200
# ---- moving a live grant to another team --------------------------------------------------------
async def _as(email: str) -> dict:
"""Headers that act as a given user — an identity token, minted the way `treg login` does."""
from sqlmodel import select
from treg.domain.identity import session as _sess
from treg.infra.db import session_maker
from treg.models import User
async with session_maker() as db:
user = (await db.execute(select(User).where(User.email == email))).scalar_one()
return {"X-Treg-Token": _sess.make_identity(user.id, token_version=user.token_version)}
async def test_the_team_on_a_grant_can_be_moved_without_reconnecting(clients):
"""The consent screen's choice was invisible and permanent: an agent reported a slug, `treg org
ls` listed the teams of whoever was logged in THERE, and those need not be the same account.
Money left a team nobody had opened. The team lives on the refresh family, so moving it is a row
update the next refresh picks up — no tearing down a working connector to fix which balance it
spends."""
email = "mover@superdesign.dev"
body, client_id, org_id = await _grant_full(clients, email)
# As the person who authorised it — the whole point of the bug is that this is NOT necessarily
# whoever the CLI on some other machine happens to be signed in as.
me = await _as(email)
other = (await clients.post("/orgs", json={"name": "the other team"}, headers=me)).json()
grants = (await clients.get("/oauth/grants", headers=me)).json()
assert len(grants) == 1 and grants[0]["team_name"], "and named, not just slugged"
grant = grants[0]["grant"]
moved = await clients.post(f"/oauth/grants/{grant}/team", json={"team": other["org"]}, headers=me)
assert moved.status_code == 200, moved.text
assert moved.json()["team"] == other["org"]
r = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert r.status_code == 200
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=r.json()["access_token"])
assert out["team"] == other["org"], "the client's very next refresh spends from the new team"
async def test_a_rolling_deploy_family_without_authority_can_be_listed_and_moved(clients):
"""A35 is a startup snapshot, not a fence around old instances. During a rolling deploy an old
binary can consent a valid family after the new binary ran A35, leaving only OAuthRefresh. The
repair must happen on both inspection paths: listing cannot show null authority, and a direct
team move cannot 404 merely because nobody listed the family first."""
from datetime import datetime
from sqlmodel import select
from treg import crypto
from treg.infra.db import session_maker
from treg.models import OAuthGrant, OAuthRefresh
email = "rolling-gap@superdesign.dev"
body, client_id, original_org_id = await _grant_full(clients, email)
me = await _as(email)
other = (await clients.post("/orgs", json={"name": "rolling gap destination"},
headers=me)).json()
original = next(team for team in (await clients.get("/orgs", headers=me)).json()
if team["org_id"] == original_org_id)
rotated = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert rotated.status_code == 200, rotated.text
body = rotated.json()
consented_at = datetime(2026, 1, 2, 3, 4, 5)
async with session_maker() as db:
live = (await db.execute(select(OAuthRefresh).where(
OAuthRefresh.token_hash == crypto.hash_token(body["refresh_token"])))).scalar_one()
family_id = live.family_id
rows = (await db.execute(select(OAuthRefresh).where(
OAuthRefresh.family_id == family_id).order_by(OAuthRefresh.id))).scalars().all()
assert len(rows) == 2 and rows[0].retired_at is not None
rows[0].created_at = consented_at
rows[1].created_at = datetime(2026, 2, 2, 3, 4, 5)
# A different VALID team makes a newest-row reconstruction observably wrong. Both fixture
# rows used the same org in the first version, so choosing newest still passed.
rows[1].org_id = other["org_id"]
db.add_all(rows)
grant = await db.get(OAuthGrant, family_id)
await db.delete(grant)
await db.commit()
listed = (await clients.get("/oauth/grants", headers=me)).json()
assert listed[0]["grant"] == family_id
assert listed[0]["team"] == original["slug"]
assert listed[0]["granted"] == "2026-01-02T03:04:05"
# Remove it again so the setter itself — not the GET above — has to heal the rolling gap.
async with session_maker() as db:
await db.delete(await db.get(OAuthGrant, family_id))
await db.commit()
moved = await clients.post(f"/oauth/grants/{family_id}/team",
json={"team": other["org"]}, headers=me)
assert moved.status_code == 200, moved.text
async with session_maker() as db:
grant = await db.get(OAuthGrant, family_id)
assert grant.current_org_id == other["org_id"]
assert grant.granted_at == consented_at
async def test_refresh_repairs_missing_authority_with_the_original_consent_time(clients):
"""Refresh already fell back to OAuthRefresh.org_id, which hid the missing row until rotation.
Rotation then created OAuthGrant with the rotation time, making an old consent look new. Repair
before issuing the replacement and preserve the oldest row's actual consent timestamp."""
from datetime import datetime
from sqlmodel import select
from treg import crypto
from treg.infra.db import session_maker
from treg.models import OAuthGrant, OAuthRefresh
body, client_id, _ = await _grant_full(clients, "rolling-refresh@superdesign.dev")
consented_at = datetime(2026, 2, 3, 4, 5, 6)
async with session_maker() as db:
token = (await db.execute(select(OAuthRefresh).where(
OAuthRefresh.token_hash == crypto.hash_token(body["refresh_token"])))).scalar_one()
family_id = token.family_id
token.created_at = consented_at
db.add(token)
await db.delete(await db.get(OAuthGrant, family_id))
await db.commit()
refreshed = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert refreshed.status_code == 200, refreshed.text
async with session_maker() as db:
grant = await db.get(OAuthGrant, family_id)
assert grant is not None and grant.granted_at == consented_at
async def test_only_the_grants_own_user_can_move_it(clients):
"""The grant belongs to the person who authorised it, not to anyone holding its id."""
await _grant_full(clients, "owner@superdesign.dev")
grant = (await clients.get("/oauth/grants",
headers=await _as("owner@superdesign.dev"))).json()[0]["grant"]
stranger = (await clients.post("/users", json={"email": "stranger@superdesign.dev"})).json()
theirs = (await clients.get("/orgs", headers={"X-Treg-Token": stranger["token"]})).json()[0]
r = await clients.post(f"/oauth/grants/{grant}/team", json={"team": theirs["slug"]},
headers={"X-Treg-Token": stranger["token"]})
assert r.status_code == 404, "and it must not confirm the grant exists"
async def test_balance_tells_an_oauth_caller_how_to_move_the_team(clients):
"""The label is only half of report #5 — the other half is knowing the choice is reversible."""
body, _, _ = await _grant_full(clients, "labelled@superdesign.dev")
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=body["access_token"])
assert out["identity"] == "labelled@superdesign.dev" and out["team_name"]
assert "use-team" in out["hint"]
async def test_the_listed_grant_id_is_the_one_use_team_accepts(clients, monkeypatch):
"""The id printed by `treg mcp grants` is an ARGUMENT, not prose. It is 22 characters and the
table clipped it to 13 plus an ellipsis, so the one command the table exists to feed answered
404 for anything a human copied off their screen — report #5's fix, broken end to end, with a
test suite that never once went through the CLI."""
import io
from contextlib import redirect_stdout
from treg import cli
email = "roundtrip@superdesign.dev"
await _grant_full(clients, email)
me = await _as(email)
listed = (await clients.get("/oauth/grants", headers=me)).json()
grant = listed[0]["grant"]
assert len(grant) > 14, "a shorter id would make this test pass for the wrong reason"
# what the human actually sees. The CLI is synchronous httpx, so its client is stubbed with the
# response the server just gave us — the rendering is what is under test, not the transport.
class _Stub:
status_code, headers = 200, {"content-type": "application/json"}
def json(self): return listed
def __enter__(self): return self
def __exit__(self, *a): return False
def get(self, *a, **k): return self
buf = io.StringIO()
with redirect_stdout(buf):
monkeypatch.setattr(cli, "_client", lambda cfg: _Stub())
cli.cmd_mcp_grants(type("A", (), {})(), {})
printed = buf.getvalue()
assert grant in printed, f"the full id must be on screen, got:\n{printed}"
# and it round-trips through the command it is printed for
moved = await clients.post(f"/oauth/grants/{grant}/team",
json={"team": listed[0]["team"]}, headers=me)
assert moved.status_code == 200, moved.text
async def test_a_grant_dies_with_the_membership_it_was_consented_under(clients):
"""The grant is consent to spend a TEAM's balance; leaving that team ends the standing it was
given with. Refresh checked only that the user and org still existed, so a grant kept minting
tokens forever — every call refused by `require_member`, but the grant lying dormant and
springing back to life, with no new consent, the day membership was restored."""
from sqlmodel import select
from treg.infra.db import session_maker
from treg.models import Membership, User
body, client_id, org_id = await _grant_full(clients, "departing@superdesign.dev")
async with session_maker() as db:
user = (await db.execute(select(User).where(
User.email == "departing@superdesign.dev"))).scalar_one()
m = (await db.execute(select(Membership).where(
Membership.user_id == user.id, Membership.org_id == org_id))).scalar_one()
await db.delete(m)
await db.commit()
r = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert r.status_code == 400, "a grant must not outlive the membership it was granted under"
assert "member" in r.json()["error_description"]
async def test_a_rotation_started_before_a_move_cannot_drag_the_team_back(clients):
"""A rotation that began before a team move must not resurrect the old team when it lands.
The first version of this test proved nothing: it completed the move and THEN refreshed, which
passes whether or not the fix exists, because `set_team` had already rewritten the very row the
refresh reads. The race that matters is the other order — a rotation holding a row whose
`org_id` still says the OLD team — so this test creates that state directly, by writing the
stale team back onto the live row after the move (exactly what an in-flight rotation would have
read). Authority lives on the family's OLDEST row, so the stale row must lose."""
email = "racer@superdesign.dev"
body, client_id, original_org_id = await _grant_full(clients, email)
me = await _as(email)
other = (await clients.post("/orgs", json={"name": "destination team"}, headers=me)).json()
grant = (await clients.get("/oauth/grants", headers=me)).json()[0]["grant"]
# rotate once, so the consent row (the authority) is no longer the live row
rotated = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert rotated.status_code == 200
body = rotated.json()
assert (await clients.post(f"/oauth/grants/{grant}/team",
json={"team": other["org"]}, headers=me)).status_code == 200
# The state the race actually leaves behind: a rotation that read the OLD team before the move
# commits its replacement AFTER it, so the family's NEWEST row carries the old team while the
# oldest — the authority — carries the new one. Reading the newest row (the first attempt at
# this fix) reverts the move permanently right here.
from sqlmodel import select as _select
from treg.infra.db import session_maker
from treg.models import OAuthRefresh as _RT
async with session_maker() as db:
newest = (await db.execute(_select(_RT).where(_RT.family_id == grant)
.order_by(_RT.id.desc()).limit(1))).scalars().first()
oldest = (await db.execute(_select(_RT).where(_RT.family_id == grant)
.order_by(_RT.id.asc()).limit(1))).scalars().first()
assert newest.id != oldest.id, "the family must have rotated, or newest IS the authority"
newest.org_id = original_org_id
db.add(newest)
await db.commit()
listed = (await clients.get("/oauth/grants", headers=me)).json()
assert listed[0]["team"] == other["org"], \
"the grant list must show the same family authority refresh will use"
r = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert r.status_code == 200, r.text
async with mcp_session(clients) as c:
out = await _call_tool(c, "balance", {}, token=r.json()["access_token"])
assert out["team"] == other["org"]
# and the NEXT rotation stays there too, rather than reverting to the row's old value
again = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": r.json()["refresh_token"],
"client_id": client_id})
async with mcp_session(clients) as c:
out2 = await _call_tool(c, "balance", {}, token=again.json()["access_token"])
assert out2["team"] == other["org"]
async def test_a_team_you_cannot_use_is_indistinguishable_from_one_that_does_not_exist(clients):
"""Told apart, this route reports whether an arbitrary slug exists on treg — an oracle any
signed-in account could walk."""
await _grant_full(clients, "prober@superdesign.dev")
me = await _as("prober@superdesign.dev")
grant = (await clients.get("/oauth/grants", headers=me)).json()[0]["grant"]
outsider = (await clients.post("/users", json={"email": "elsewhere@superdesign.dev"})).json()
theirs = (await clients.get("/orgs", headers={"X-Treg-Token": outsider["token"]})).json()[0]
real_but_not_mine = await clients.post(f"/oauth/grants/{grant}/team",
json={"team": theirs["slug"]}, headers=me)
pure_fiction = await clients.post(f"/oauth/grants/{grant}/team",
json={"team": "no-such-team-anywhere"}, headers=me)
assert real_but_not_mine.status_code == pure_fiction.status_code == 404
assert real_but_not_mine.json()["detail"] == pure_fiction.json()["detail"].replace(
"no-such-team-anywhere", theirs["slug"])
async def test_an_expired_grant_is_not_presented_as_authorized(clients):
"""The token endpoint already refuses an expired refresh token. Listing the same family as an
active authorization gives the operator two contradictory answers about one credential."""
from datetime import datetime, timedelta, timezone
from sqlmodel import select
from treg import crypto
from treg.infra.db import session_maker
from treg.models import OAuthRefresh
body, client_id, _ = await _grant_full(clients, "expired-list@superdesign.dev")
me = await _as("expired-list@superdesign.dev")
async with session_maker() as db:
row = (await db.execute(select(OAuthRefresh).where(
OAuthRefresh.token_hash == crypto.hash_token(body["refresh_token"])
))).scalar_one()
row.expires_at = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=1)
db.add(row)
await db.commit()
assert (await clients.get("/oauth/grants", headers=me)).json() == []
refused = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert refused.status_code == 400 and "expired" in refused.json()["error_description"]
async def test_an_expired_grant_cannot_be_moved(clients):
"""A team move is an operation on usable authority. Accepting it on a dead family says the
grant is live even though its next token exchange is guaranteed to fail."""
from datetime import datetime, timedelta, timezone
from sqlmodel import select
from treg import crypto
from treg.infra.db import session_maker
from treg.models import OAuthRefresh
body, _, _ = await _grant_full(clients, "expired-move@superdesign.dev")
me = await _as("expired-move@superdesign.dev")
other = (await clients.post("/orgs", json={"name": "expired destination"}, headers=me)).json()
family_id = (await clients.get("/oauth/grants", headers=me)).json()[0]["grant"]
async with session_maker() as db:
row = (await db.execute(select(OAuthRefresh).where(
OAuthRefresh.token_hash == crypto.hash_token(body["refresh_token"])
))).scalar_one()
row.expires_at = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=1)
db.add(row)
await db.commit()
moved = await clients.post(f"/oauth/grants/{family_id}/team",
json={"team": other["org"]}, headers=me)
assert moved.status_code == 404
async def test_rotation_does_not_change_the_grant_date(clients):
"""`granted` means when the human consented, not when the connector last refreshed. Rotation
creates a token row, but it must not make an old authorization appear newly granted."""
from datetime import timedelta
from sqlmodel import select
from treg.infra.db import session_maker
from treg.models import OAuthRefresh
body, client_id, _ = await _grant_full(clients, "grant-date@superdesign.dev")
me = await _as("grant-date@superdesign.dev")
before = (await clients.get("/oauth/grants", headers=me)).json()[0]
rotated = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert rotated.status_code == 200
async with session_maker() as db:
newest = (await db.execute(select(OAuthRefresh).where(
OAuthRefresh.family_id == before["grant"]
).order_by(OAuthRefresh.id.desc()).limit(1))).scalar_one()
newest.created_at = newest.created_at + timedelta(days=1)
db.add(newest)
await db.commit()
after = (await clients.get("/oauth/grants", headers=me)).json()[0]
assert after["granted"] == before["granted"]
async def test_a_moved_grant_keeps_retired_token_team_provenance(clients, monkeypatch):
"""A replay audit is evidence about the token that was copied. Moving the live grant later
must not rewrite history and attribute a team-A token to team B."""
from treg import audit
email = "provenance@superdesign.dev"
body, client_id, original_org_id = await _grant_full(clients, email)
me = await _as(email)
grant = (await clients.get("/oauth/grants", headers=me)).json()[0]["grant"]
rotated = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert rotated.status_code == 200
other = (await clients.post("/orgs", json={"name": "provenance destination"}, headers=me)).json()
moved = await clients.post(f"/oauth/grants/{grant}/team",
json={"team": other["org"]}, headers=me)
assert moved.status_code == 200
recorded = {}
monkeypatch.setattr(audit, "record_call", lambda **kwargs: recorded.update(kwargs))
replay = await clients.post("/oauth/token", data={
"grant_type": "refresh_token", "refresh_token": body["refresh_token"],
"client_id": client_id})
assert replay.status_code == 400 and "already used" in replay.json()["error_description"]
assert recorded["org_id"] == original_org_id