[phase9] Auth hardening: bound agent tokens (iat/exp) + JWT jti revocation#323
Merged
Merged
Conversation
… tokens are bounded
…ry so a stolen cookie's exp is fixed
…t revokes the current jti
…w + jti revocation
|
Thanks for opening your first pull request on RoboCo! Quick checklist before review (most of these are enforced by CI, but worth a glance):
See CONTRIBUTING.md for the full workflow and the Code of Conduct for the community standards we follow. Welcome aboard — a maintainer will review shortly. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 9 — Auth hardening (structural)
Final phase of the 2026-07-06 full-codebase-scan fix program. Two findings, 5 commits, stacked on Phase 8 (
fix/scan-llm-providers, PR #322). Merge bottom-up after #322.M35 — Bound agent HMAC tokens with iat/exp (Tasks 1-2)
The agent token was a static
HMAC(secret, "id:role:team")hex with no expiry — a leaked token was valid forever.issue_agent_tokennow mints a{base64url(payload)}.{sig}token carryingiat/exp(TTLagent_token_ttl_seconds, default 7d) when a TTL is passed, and the orchestrator passes one at spawn. Backward-compatible:verify_agent_tokendetects the format by the.separator; a caller passing no TTL still gets the static hex token, which still verifies byte-for-byte (issue_panel_tokenis unchanged). Expired tokens are rejected; thenowparam is the test seam (no global time monkeypatch).M36 — Cloud-auth JWT revocation + sliding re-mint (Tasks 3-4, default-off
ROBOCO_CLOUD_AUTH_ENABLED)jti;_slide_session_cookiere-mints only when the current token is withincloud_auth_remint_threshold_seconds(default 1d) of expiry, so a stolen cookie'sexpstays fixed instead of rolling with the legit user./logoutonly cleared the cookie — a stolen copy stayed valid forcloud_auth_cookie_max_age. A custom/logoutnow adds the cookie'sjtito a Redis revocation set (TTL = remaining life), andread_tokenrejects any token whosejtiis in the set. Fail-OPEN on Redis unavailable — thepwd_fppassword-rotation check remains the strong user-wide revocation;jtiis the per-session logout kill and a Redis hiccup never locks out the CEO. The Redis check is short-circuited behind the pwd_fp check inread_token(a rotated-password token never touches Redis).Task 5 — CHANGELOG
3 bullets appended to
0.19.0 ### Fixed.Gate
ruff format .(1082 unchanged) +ruff check .cleanmypy roboco/— no issues, 359 source filespytest(DB :55432) — 12042 passed / 418 skipped / 95% (+17 over Phase 8: Tasks 1-4 new tests)feccd41d): SHIP, no fix wave. Fail-open and backward-compat invariants verified adversarially;nowtest seam exercised for expired-token rejection; grok.py correctly untouched (delegates to the orchestrator injector).Minor findings (list only, no fix required pre-merge)
/auth/logoutdecodes the cookie without signature verification (plan-mandatedverify_signature: False) — defense-in-depth: an unauthenticated caller can revoke a forged jti, but jtis are 128-bit uuid4 (unguessable) and impact is logout-DoS only.revoke_jtiwrite-error fail-open branch uncovered by tests._should_remintdecode-failure →Truesafe-default branch uncovered._heal_stale_agent_tokenskills it at the next restart — the documented M35 tradeoff.Implementation note
The brief's verbatim test monkeypatch target
revocation.redis.asynciowas wrong —revocationdoesimport redis.asyncio as redis, sorevocation.redisIS theredis.asynciomodule and.asyncioresolved to stdlibasyncio(AttributeError: module 'redis.asyncio' has no attribute 'asyncio'). Corrected torevocation.redis. The Task 4 sonnet implementer 429-failed on the glm-5.2:cloud ollama.com session cap; the controller completed the wiring inline.🤖 Generated with Claude Code