Skip to content

feat(api): require a live session for writes and add a write rate bucket - #2091

Open
ka1kqi wants to merge 6 commits into
feat/write-dashboard-createfrom
feat/write-session-guard
Open

feat(api): require a live session for writes and add a write rate bucket#2091
ka1kqi wants to merge 6 commits into
feat/write-dashboard-createfrom
feat/write-session-guard

Conversation

@ka1kqi

@ka1kqi ka1kqi commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part 5 of the public write paths. Stacked on the dashboard/widget PR.

Writes check that the caller's session is still live before proceeding — reads stay offline-verifiable on the JWT alone, but a revoked session must not keep writing until its token expires. Adds a dedicated internal liveness route and a separate rate-limit bucket for writes.

Note: _PLAN_LIMITS_WRITE declares starter/pro/enterprise tiers that are currently unreachable — the account stamper hardcodes the free plan, so every caller caps at the free limit. Worth fixing separately.

Part of #2010.

🤖 Generated with Claude Code

https://claude.ai/code/session_012CLSaVXhLYJxH6DS9mDjiB


Summary by cubic

Part of the public write paths (#2010). Writes now verify the caller's session is still live before proceeding — reads stay offline-verifiable on the JWT alone, but a revoked session must not keep writing until its token expires.

  • JWT verification surfaces the token's sid claim and rejects any token without a usable one, so the write path always has a session to check. This also applies to reads, since both paths share the verifier; safe because the only issuer sets sid on every token it mints.
  • A new internal validate-session-live route answers {live: boolean}; any ambiguity (network error, non-200, malformed body) fails closed with a 503.
  • Session-token credentials and API keys skip the extra hop since their introspection already proved liveness.
  • Writes get their own rate-limit bucket so bursts throttle independently of reads; the write tier launches with the read numbers.
  • Note: _PLAN_LIMITS_WRITE declares starter/pro/enterprise tiers that are currently unreachable — the account stamper hardcodes the free plan, so every caller caps at the free limit.

Written for commit bd7bd78. Summary will update on new commits.

Review in cubic


Update: access tokens must now carry a sid

A review bot found a fail-open on the revocation control: a JWT with no sid claim
degraded to None, and the liveness check returns early when there is no session to
check — so a token without one could write past a revoked session. _verify_access_jwt
now rejects an absent, empty, or non-string sid with a 401 instead of degrading.

This tightening applies to reads as well as writes, because both paths share the
verifier. That is safe rather than a behavior risk: the only issuer sets sid from the
session's primary key on every token it mints, and access tokens are short-lived, so no
legitimate caller can be holding a sid-less one for long. It fails closed.


Update: adversarial-review fixes

Test fixtures for CLI-JWT reads now mint a sid, keeping this PR green at its own tip after the sid-required verification change.

ka1kqi and others added 2 commits August 26, 2026 15:58
Reads keep the CLI JWT's offline verification, but writes are
higher-stakes: surface the JWT's sid claim through _verify_access_jwt
onto AuthResult.session_id and add a require_live_session dependency
that checks the sid against the live session row via a new internal
validate-session-live route — revoking the session now takes effect
instantly on writes. Session-token credentials skip the hop (their
introspection already proved liveness), and any introspection ambiguity
fails closed with a 503. The liveness route speaks a {live: boolean}
envelope, so it gets a dedicated fail-closed caller instead of the
valid-discriminator helper. Also adds the write rate bucket (key_write +
a write tier launching with the read numbers) for the upcoming public
write routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CLSaVXhLYJxH6DS9mDjiB
@ka1kqi
ka1kqi requested a review from a team as a code owner September 1, 2026 17:20
Comment thread backend/rest/routers/public/deps.py Outdated
@trident-sentinel

trident-sentinel Bot commented Sep 1, 2026

Copy link
Copy Markdown

PR overview

This pull request requires access tokens to carry a session ID, checks JWT session liveness before account writes, and adds a separate write rate-limit bucket and internal liveness endpoint.

No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Scanned with Semgrep · TruffleHog · Trident review. View in Trident

Fixed/addressed: 1 · PR risk: 0/10

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files

Confidence score: 3/5

  • backend/rest/routers/public/deps.py: The new liveness guard is not attached to any write route, so revoked JWT sessions can still perform writes. Apply require_live_session to every intended user-authenticated write route and verify the protected route coverage.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="backend/rest/routers/public/deps.py">

<violation number="1" location="backend/rest/routers/public/deps.py:973">
P1: The new liveness guard is dead in production: no write route depends on it, so revoked JWT sessions are not checked before writes. Attach `require_live_session` to every intended user-authenticated write route and ensure its auth dependency carries the JWT session id.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Public API Client
    participant API as Public REST API
    participant Auth as Auth Dependencies
    participant JWKS as JWKS Endpoint
    participant UI as Traceroot UI Internal API
    participant DB as Session Database
    participant Redis as Rate Limit Store

    Note over Client,Redis: Public request runtime flow

    alt Read request with CLI access JWT
        Client->>API: GET read route with Authorization: Bearer JWT
        API->>Auth: Verify JWT offline
        Auth->>JWKS: Resolve signing key by kid
        JWKS-->>Auth: EdDSA public key
        Auth-->>API: AuthResult with user identity
        API->>Redis: Check read bucket
        Redis-->>API: Allow or rate-limit decision
        API-->>Client: Read response or 429 with Retry-After and X-RateLimit-* headers
    else Write request with CLI access JWT
        Client->>API: POST/PUT/DELETE write route with Authorization: Bearer JWT
        API->>Auth: Verify JWT offline and extract sub plus sid
        Auth->>JWKS: Resolve signing key by kid
        JWKS-->>Auth: EdDSA public key
        Auth->>UI: POST /api/internal/validate-session-live with sessionId and X-Internal-Secret
        UI->>DB: Find session by id and select expiresAt
        DB-->>UI: Session row or no row
        UI-->>Auth: 200 {live: true} or {live: false}
        alt Session is live
            Auth->>Redis: Check dedicated write bucket
            Redis-->>API: Allow or rate-limit decision
            API-->>Client: Write response or 429 with Retry-After and X-RateLimit-* headers
        else Session revoked or expired
            Auth-->>API: 401 Session revoked or expired
            API-->>Client: 401 Unauthorized
        else Liveness response is unavailable or malformed
            Auth-->>API: 503 Authentication service error
            API-->>Client: 503 Service Unavailable
        end
    else Session-token or API-key credential
        Client->>API: Write request with session token or API key
        API->>Auth: Introspect or validate credential
        Auth-->>API: AuthResult with no session_id
        Note over Auth: Existing introspection has already established liveness, no liveness hop
        API->>Redis: Check dedicated write bucket
        Redis-->>API: Allow or rate-limit decision
        API-->>Client: Write response or 429 with rate-limit headers
    end

    Note over API,Redis: Rate-limit identity is stamped from workspace, user, and plan.
    Note over API,Redis: Write bucket keys use rl:write:<plan>:<workspace/user scope>, account stamping currently resolves callers to free.
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

return live


async def require_live_session(auth: _AccountAuth) -> None:

@cubic-dev-ai cubic-dev-ai Bot Sep 1, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new liveness guard is dead in production: no write route depends on it, so revoked JWT sessions are not checked before writes. Attach require_live_session to every intended user-authenticated write route and ensure its auth dependency carries the JWT session id.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/rest/routers/public/deps.py, line 973:

<comment>The new liveness guard is dead in production: no write route depends on it, so revoked JWT sessions are not checked before writes. Attach `require_live_session` to every intended user-authenticated write route and ensure its auth dependency carries the JWT session id.</comment>

<file context>
@@ -879,3 +906,98 @@ async def authenticate_and_stamp_account_caller(request: Request, auth: _Account
+    return live
+
+
+async def require_live_session(auth: _AccountAuth) -> None:
+    """Block a write when the JWT's minting session has been revoked or expired.
+
</file context>
Fix with cubic

Comment thread backend/rest/routers/public/deps.py Outdated
ka1kqi and others added 4 commits September 1, 2026 13:14
The write path checks the token's sid against the live session row and skips
the check when it is absent, so a signed token carrying no sid wrote past a
revoked session until it expired. Verification now rejects a token without a
usable sid instead of degrading it to None: the sole issuer sets it on every
token it mints, so nothing legitimate loses access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CLSaVXhLYJxH6DS9mDjiB
…quires it

Rejecting sid-less access JWTs left the read-path test fixtures minting
tokens the verifier no longer accepts, failing three JWT-read tests on
this branch. Stamp a sid in both read fixtures' _mint_jwt helpers,
matching what the issuer actually mints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CLSaVXhLYJxH6DS9mDjiB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant