fix(httpapi): strip relayauth token-class prefix before parsing bearer as JWT#205
Conversation
…r as JWT Relayauth's `/v1/tokens/path` and `/v1/tokens/workspace` mint endpoints wrap the access token they issue with a class prefix (`relay_pa_<jwt>`, `relay_ws_<jwt>`, `relay_id_<jwt>`). The prefix encodes the token class for clients that route by token type, but is NOT part of the RS256 JWS string that follows. `parseBearer` was splitting the raw bearer on `.` without first stripping the prefix. For a `relay_pa_eyJ.eyJ.<sig>` token, `parts[0]` ended up as `relay_pa_eyJ<rest_of_header_b64>`. The underscore character is valid in base64url so `base64.RawURLEncoding. DecodeString` sometimes succeeded; the subsequent JSON unmarshal then failed because the decoded bytes were not valid JSON. Net effect: every request bearing a relayauth-wrapped token returned a 401 "invalid jwt header" (or, depending on how the dots fell in the prefix, "invalid jwt format"). This silently broke the relayfile-mount daemon used by cloud's proactive runtime (agentworkforce/cloud) and the cloud-agent box flow: the mount started, hit 401 on every sync cycle, and writeback drafts that handlers wrote to local disk never reached relayfile cloud or the upstream provider. Verified end-to-end against issue agentworkforce/proactive-agents#105/#106 — handler ran fine, draft files written to local mirror, daemon log filled with `mount sync cycle failed: http 401 unauthorized: invalid jwt header`. Fix: introduce `stripRelayauthTokenPrefix` and call it inside `parseBearer` immediately after the `Bearer ` strip and before the dot-split. Falls back to the raw input if no known prefix is present, so bare JWTs minted via `/v1/tokens` (with no class wrapper) continue to parse unchanged. Tests: new `TestParseBearerStripsRelayauthTokenPrefixes` exercises all three known prefixes (`relay_pa_`, `relay_ws_`, `relay_id_`) against a real RS256 JWT, plus a bare-JWT sanity case and a `relay_pa_not-a-jwt` rejection case. Existing `TestParseBearer*` cases all still pass. Full `go test ./...` clean (8 packages, no regressions in mountsync/mountfuse/writeback/digest/relayfile/schema). If relayauth adds new token classes in the future, append the prefix to `relayauthTokenPrefixes` in `internal/httpapi/auth.go`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Relayfile Eval ReviewRun: Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0 Human Review CasesNo reviewable human-review cases captured Relayfile output. |
Summary
Relayauth's
/v1/tokens/pathand/v1/tokens/workspacemint endpoints wrap the access token they issue with a class prefix:relay_pa_<jwt>,relay_ws_<jwt>,relay_id_<jwt>. The prefix encodes the token class for clients that route by type; it is NOT part of the RS256 JWS that follows.parseBearerininternal/httpapi/auth.gowas splitting the raw bearer on.without first stripping the prefix. For arelay_pa_eyJ.eyJ.<sig>token,parts[0]ended up asrelay_pa_eyJ<rest_of_header_b64>. The underscore is valid in base64url, sobase64.RawURLEncoding.DecodeStringsometimes succeeded; the subsequent JSON unmarshal then failed because the decoded bytes weren't valid JSON. Net effect: every request bearing a relayauth-wrapped token returned 401 "invalid jwt header" (or, depending on how dots fell in the prefix, "invalid jwt format").Production impact
The cloud repo's proactive runtime (
agentworkforce/cloud) starts arelayfile-mountdaemon inside per-fire Daytona sandboxes so handler-side writebacks (ctx.github.commentetc.) flush to relayfile cloud. Mint succeeded, daemon launched, and then:Verified end-to-end against
agentworkforce/proactive-agents#105/#106:runner.handler.ok durationMs=3)/workspace/github/repos/AgentWorkforce/proactive-agents/issues/106/comments/create comment <uuid>.jsonrunScripteventually 524'd from Daytona's proxy after the cleanup trap's--onceflush also hammered 401sSame flow underpins cloud-agent boxes (
packages/web/.../box/box-manager.ts), which use the same mint helper.Fix
Falls back to the raw input if no known prefix is present, so bare JWTs minted via
/v1/tokens(no class wrapper) continue to parse unchanged.Test plan
TestParseBearerStripsRelayauthTokenPrefixes— exercises all three prefixes (relay_pa_,relay_ws_,relay_id_) against a real RS256 JWT, plus a bare-JWT sanity case + arelay_pa_not-a-jwtrejection case (header decode must still fail AFTER the strip)TestParseBearer*cases pass unchangedgo test ./...clean (8 packages, no regressions in mountsync/mountfuse/writeback/digest/schema)issues.openedevent inAgentWorkforce/proactive-agents. Expected: mount daemon polls successfully (200 from/v1/workspaces/.../fs/events),ctx.github.commentdraft file pushes to relayfile cloud, adapter writeback POSTs comment to GitHub, comment lands on the issue.Future-proofing
If relayauth adds new token classes (
relay_<class>_*), append the prefix torelayauthTokenPrefixes. The list lives ininternal/httpapi/auth.gonext to its sole consumer.Possible follow-up
Independent investigation (cloud-side) surfaced a second potential issue:
scopeMatchesreturns a 403 "missing required scope: fs:read" when checking a token with path-scoped grants (relayfile:fs:read:/github/*) against a coarsefs:readrequirement. The Go-side code actually handles this correctly (path is the OPTIONAL 4th segment, plane/res/act match), so I believe the 403 came from a different curl test against a path-required endpoint viascopeMatchesPath. Confirming this after the prefix-strip is merged + deployed; will file a separate PR if a second issue surfaces.🤖 Generated with Claude Code