jcp-bridge: an OAuth access token narrows to the grant's caveats or is not minted, and a delegated grant needs its chain #321

Merged
dcharlot merged 1 commit from fix/jcp-oauth-projects-caveats into main 2026-10-18 19:02:29 -04:00
Owner

The defect

grant_to_access_token projects a JCP grant into an RFC 9068 access token, so an MCP or OAuth resource server can enforce it with ordinary JWT tooling. It carried the capability, the expiry and the joule budget, and dropped everything else. The resource server enforced more authority than the grant conveyed:

  • Time window: a NotAfter narrower than the expiry was ignored, and so was any NotBefore.
  • Actions: an ActionIn did not narrow scope.
  • Silently dropped: PayloadIs, MaxCostUj, ModelIn, a flow ceiling and a money ceiling.
  • Delegation: a delegated grant was minted from its leaf's signature alone, with the chain that shows it narrows never checked.
  • Verification: verify_access_token never checked nbf.

The fix

A projection may narrow, never widen.

  • Narrowed into standard claims:
    • NotAfter lowers exp.
    • NotBefore raises nbf.
    • ActionIn intersects scope.
    • A CallerIs naming the subject adds nothing, since sub already names it.
  • Refused:
    • Unprojectable for a condition no claim a resource server is known to enforce can carry: PayloadIs, MaxCostUj, ModelIn, a flow ceiling, a money ceiling.
    • PermitsNothing for a grant whose conditions leave nothing to permit: no action, an empty window, or a caller other than the subject.
  • Delegated grants: grant_to_access_token refuses them (Delegated). chain_to_access_token projects a chain, verified hop by hop. Every link bounds exp, and every link's caveats apply.
  • Verification: verify_access_token enforces nbf as well as exp.

The only caller in the repository is the mint example, which still runs.

Checked

On the build host:

  • Tests: jcp-bridge passes 18, three of them new. The full jcp-rs workspace passes 517.
  • Clippy is clean.
  • Mutations: each fails tests:
    • dropping NotAfter
    • dropping PayloadIs
    • minting a lone delegated grant
    • not enforcing nbf
  • Repository checks: tests-in-CI, spec references, vanity metrics and orphan sources pass locally.

🤖 Generated with Claude Code

## The defect `grant_to_access_token` projects a JCP grant into an RFC 9068 access token, so an MCP or OAuth resource server can enforce it with ordinary JWT tooling. It carried the capability, the expiry and the joule budget, and **dropped everything else**. The resource server enforced more authority than the grant conveyed: - **Time window:** a `NotAfter` narrower than the expiry was ignored, and so was any `NotBefore`. - **Actions:** an `ActionIn` did not narrow `scope`. - **Silently dropped:** `PayloadIs`, `MaxCostUj`, `ModelIn`, a flow ceiling and a money ceiling. - **Delegation:** a delegated grant was minted from its leaf's signature alone, with the chain that shows it narrows never checked. - **Verification:** `verify_access_token` never checked `nbf`. ## The fix A projection may narrow, never widen. - **Narrowed into standard claims:** - `NotAfter` lowers `exp`. - `NotBefore` raises `nbf`. - `ActionIn` intersects `scope`. - A `CallerIs` naming the subject adds nothing, since `sub` already names it. - **Refused:** - `Unprojectable` for a condition no claim a resource server is known to enforce can carry: `PayloadIs`, `MaxCostUj`, `ModelIn`, a flow ceiling, a money ceiling. - `PermitsNothing` for a grant whose conditions leave nothing to permit: no action, an empty window, or a caller other than the subject. - **Delegated grants:** `grant_to_access_token` refuses them (`Delegated`). `chain_to_access_token` projects a chain, verified hop by hop. Every link bounds `exp`, and every link's caveats apply. - **Verification:** `verify_access_token` enforces `nbf` as well as `exp`. The only caller in the repository is the mint example, which still runs. ## Checked On the build host: - **Tests:** jcp-bridge passes 18, three of them new. The full jcp-rs workspace passes 517. - **Clippy** is clean. - **Mutations:** each fails tests: - dropping `NotAfter` - dropping `PayloadIs` - minting a lone delegated grant - not enforcing `nbf` - **Repository checks:** tests-in-CI, spec references, vanity metrics and orphan sources pass locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jcp-bridge: an OAuth access token narrows to the grant's caveats or is not minted, and a delegated grant needs its chain
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 19s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m51s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m38s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m15s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m13s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m34s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m52s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m38s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 2m11s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 2m43s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 56s
2e37fe2e50
`grant_to_access_token` projects a JCP grant into an RFC 9068 access
token that a resource server enforces with ordinary JWT tooling. It
carried the capability, the expiry and the joule budget, and dropped
everything else. The resource server therefore enforced more authority
than the grant conveyed:
- A `NotAfter` narrower than the expiry was ignored, and so was any
  `NotBefore`.
- An `ActionIn` did not narrow `scope`.
- `PayloadIs`, `MaxCostUj`, `ModelIn`, a flow ceiling and a money
  ceiling were dropped silently.
- A delegated grant was minted from its leaf's signature alone, with the
  chain that shows it narrows never checked.
- `verify_access_token` never checked `nbf`.

A projection may narrow, never widen:
- **Narrowed.** `NotAfter` lowers `exp`, `NotBefore` raises `nbf`,
  `ActionIn` intersects `scope`, and a `CallerIs` naming the subject
  adds nothing.
- **Refused.** A condition no claim a resource server is known to
  enforce can carry gets no token (`Unprojectable`): `PayloadIs`,
  `MaxCostUj`, `ModelIn`, a flow ceiling, a money ceiling. So does a
  grant whose conditions leave nothing to permit (`PermitsNothing`): no
  action, an empty window, or a caller other than the subject.
- **Chains.** `grant_to_access_token` refuses a delegated grant
  (`Delegated`). `chain_to_access_token` projects a chain, verified hop
  by hop, with every link bounding `exp` and every link's caveats
  applied.
- **Verification.** `verify_access_token` enforces `nbf` as well as
  `exp`.

Checked on the build host:
- jcp-bridge: 18 tests pass, three of them new. jcp-rs: 517 pass.
- The mint example runs, and clippy is clean.
- Mutations each fail tests: dropping NotAfter, dropping PayloadIs,
  minting a lone delegated grant, and not enforcing nbf.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!321
No description provided.