Skip to content

Restrict delegated OAuth tokens to scoped messaging - #47

Merged
markmnl merged 1 commit into
mainfrom
oauth-delegated-tokens
Sep 17, 2026
Merged

markmnl merged 1 commit into
mainfrom
oauth-delegated-tokens

Conversation

@markmnl

@markmnl markmnl commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a delegated OAuth token class so an identity provider can issue tokens to third-party clients (for example through an MCP server using RFC 8693 token exchange) that message as the consenting user without the privileges of that user's own session. Today every provider token is an owner session, so such a token could create API keys that outlive the consent that produced it.

  • FMSG_JWT_OAUTH_AUDIENCE enables it. The aud claim alone decides the kind of token: FMSG_JWT_AUDIENCE is an owner session (unchanged), FMSG_JWT_OAUTH_AUDIENCE is delegated. Both audiences, or an owner-audience token carrying act, is rejected. Startup fails if the OAuth audience is set without a different FMSG_JWT_AUDIENCE.
  • Delegated tokens need fmsg:read / fmsg:write for message, attachment and WebSocket routes and are refused every other route, including all sub-account (API key and grant) routes and push subscriptions. Routes are closed by default.
  • A missing, empty or malformed scope is refused with 403; it never falls back to owner privileges.
  • X-FMSG-Act-As is refused for delegated tokens unless the address is in the token's fmsg_identities claim and also passes the existing grant check.
  • A WebSocket opened with a delegated token closes when the token expires.
  • Message permissions, quotas and fmsgid acceptance checks are unchanged. With the variable unset, behaviour is identical to before.

Claims contract for issuers and resource servers such as an MCP server: docs/oauth-claims.md

Tests

  • Messaging works on every scoped route with auth type oauth; read-only tokens cannot write.
  • Every sub-account and push route returns 403 for delegated tokens, including after switching identity and with invented scopes.
  • Nine malformed scope shapes are refused on both a message route and an admin route.
  • Audience: both audiences, another resource's audience, no audience, and act on the owner audience are rejected; delegated tokens are rejected while the feature is off.
  • Act-as: unlisted, malformed claim, and listed-but-unowned are refused; listed and owned works.
  • Owner sessions (including ones carrying a provider scope claim) keep administration and act-as; existing owner and API-key tests pass unchanged.
  • Route registration moved into registerRoutes; a test requires every route to be classified as scoped or deliberately closed.

go build ./..., go vet ./... and go test ./... pass.

🤖 Generated with Claude Code

FMSG_JWT_OAUTH_AUDIENCE adds a delegated token class identified by audience
alone. Delegated tokens need fmsg:read/fmsg:write for message, attachment and
WebSocket routes and are refused every other route, including sub-account
(API key and grant) administration. Missing or malformed scopes are refused
rather than treated as an owner session, X-FMSG-Act-As is limited to
identities named in the token and still subject to the grant check, and
WebSocket connections end when a delegated token expires.

Owner sessions and API-key tokens are unchanged, and behaviour is unchanged
while the variable is unset. docs/oauth-claims.md is the claims contract for
issuers and resource servers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@markmnl
markmnl merged commit d7e3372 into main Sep 17, 2026
1 check passed
@markmnl
markmnl deleted the oauth-delegated-tokens branch September 17, 2026 09:53
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