Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@
separate audited support flows; `/api/accounts/config` must reject forged or
orgless privileged sessions before credential lookup, and tests must exercise
the real signed bearer path rather than only dev public-header overrides.
- HMAC fallback sessions must not authorize `system_admin` or `platform_admin`
roles. Platform-wide operators require the OIDC/JWKS path or a separately
audited support flow so compromise of an HMAC session secret cannot mint
platform administrator claims.
- Reply-wait task escalation must reuse the server-authoritative pending reply
path, create or update source-linked `reply_sla` ticket tasks by opaque task
id, and sanitize generated task titles from email subjects before persistence.
Expand Down
9 changes: 8 additions & 1 deletion backend/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,14 @@ def _verify_signed_session_payload(authorization: str | None) -> dict[str, Any]:
if not hmac.compare_digest(expected_signature, provided_signature):
raise _authentication_error()

return _json_object_from_base64url_segment(payload_segment)
payload = _json_object_from_base64url_segment(payload_segment)
_reject_hmac_system_admin_payload(payload)
return payload


def _reject_hmac_system_admin_payload(payload: dict[str, Any]) -> None:
if payload.get("role") in SYSTEM_ADMIN_ROLES:
raise _authentication_error()


def _required_string_claim(payload: dict[str, Any], name: str) -> str:
Expand Down
8 changes: 3 additions & 5 deletions backend/tests/test_accounts_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,9 @@ def test_accounts_config_rejects_system_admin_mailbox_owner_session(admin_role:
{"smtp_server": "smtp.example.com", "smtp_port": 587},
)

assert read_response.status_code == 403
assert write_response.status_code == 403
assert read_response.json()["detail"] == (
"Mailbox account settings require a scoped user session"
)
assert read_response.status_code == 401
assert write_response.status_code == 401
assert read_response.json()["detail"] == "Authentication required"
assert session.execute_calls == 0
assert session.configs == {}

Expand Down
25 changes: 19 additions & 6 deletions backend/tests/test_auth_real.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,20 +474,33 @@ async def test_admin_subject_does_not_imply_system_admin_role():


@pytest.mark.asyncio
async def test_system_admin_requires_explicit_signed_role_claim():
async def test_hmac_session_rejects_platform_system_admin_role_claim():
settings.AUTH_SESSION_HMAC_SECRET = SecretStr(TEST_SESSION_HMAC_SECRET)
token = _signed_session_token(
_valid_session_payload(
role="system_admin", org=None, workspace="workspace-root"
)
)

context = await get_auth_context(authorization=f"Bearer {token}")
with pytest.raises(HTTPException) as exc:
await get_auth_context(authorization=f"Bearer {token}")

assert context.user_id == "alice"
assert context.role == "system_admin"
assert context.organization_id is None
assert context.workspace_id == "workspace-root"
assert exc.value.status_code == 401


@pytest.mark.asyncio
async def test_hmac_session_rejects_platform_admin_role_claim():
settings.AUTH_SESSION_HMAC_SECRET = SecretStr(TEST_SESSION_HMAC_SECRET)
token = _signed_session_token(
_valid_session_payload(
role="platform_admin", org=None, workspace="workspace-root"
)
)

with pytest.raises(HTTPException) as exc:
await get_auth_context(authorization=f"Bearer {token}")

assert exc.value.status_code == 401


def test_http_route_accepts_signed_bearer_and_ignores_forged_identity_headers():
Expand Down
29 changes: 29 additions & 0 deletions docs/plans/2026-05-29-hmac-system-admin-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# HMAC System Admin Boundary Roadmap

## Evidence

- Master Strix run `26641767705` reported a critical JWT authentication bypass:
if an attacker learns `AUTH_SESSION_HMAC_SECRET`, they can forge a
`system_admin` token.
- The runtime already validates HMAC secret length, fixture values, issuer,
audience, expiration, role names, and unsupported critical headers.
- The remaining high-impact path is platform-wide role minting through the
legacy HMAC fallback.

## Plan

1. Keep OIDC/JWKS verification authoritative when `OIDC_ISSUER_URL` is
configured.
2. Reject `system_admin` and `platform_admin` role claims on the legacy HMAC
fallback path.
3. Keep tenant-scoped HMAC sessions working for existing workspace flows.
4. Add signed bearer regression tests proving HMAC platform-admin claims fail.
5. Record the anti-pattern in `AGENTS.md` so future admin endpoints do not
reintroduce platform-wide HMAC sessions.

## Non-Goals

- This does not remove HMAC sessions for tenant-scoped local development or
existing non-platform workspace flows.
- This does not weaken OIDC; platform-wide roles should come from an external
IdP or an audited support flow.