Files
pad/cmd
xarmian 5c53aae306 feat(oauth): a second canonical resource, with every token bound to one (TASK-3321 U2a) (#1715)
* feat(oauth): a second canonical resource, with every token bound to one (TASK-3321 U2a)

Ruling (i) on TASK-3321: the ChatGPT catalog is served at its own MCP
URL, which is a canonical OAuth resource of its own, and its tokens are
not interchangeable with /mcp's.

- config derives ChatGPTResourceURL from the /mcp resource: its MCP path
  plus /chatgpt, so https://mcp.getpad.dev becomes
  https://mcp.getpad.dev/mcp/chatgpt.
- The authorization server issues tokens for AllowedAudience plus
  AdditionalAudiences. A request must name exactly ONE of them, and every
  requested audience must be the same resource. A request for both, or for
  a foreign one, is refused before a code is issued, and the client's
  hydrated audience must include the one requested. A request naming no
  resource is still bound to /mcp's, the documented default.
- MCPBearerAuth checks the MOUNT's canonical. A route stamps it with
  WithMCPResource (never from the request); an unstamped route is /mcp's.
  A /mcp token is refused at the ChatGPT mount and the reverse.

Nothing is mounted yet (U2b). The cross-refusal test covers both
directions and the no-resource token, and goes red when the middleware
ignores the mount or the strategy allows two resources.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC

* test(oauth): a refreshed token keeps its one resource (TASK-3321 U2a review)

Codex confirmed fosite's refresh flow restores the original granted
audience; this pins it end to end: refreshing a ChatGPT token (with no
resource, or asking for /mcp's) yields a token refused at /mcp and accepted
at its own mount, and the test fails if no refresh succeeded at all.

Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
2026-10-02 02:06:09 -04:00
..