Mint OAuth client_credentials tokens as an umbra value provider, so a hosted upstream needs no static secret #83

Open
opened 2026-08-18 19:14:55 +00:00 by coilyco-ops · 0 comments
Member

The cheap half of the OAuth problem, split out of #82 so it can be built on its own merits. Neither issue blocks the other, and this one has no human in the loop.

What

A token-minting value provider, so {oauth2:<name>} resolves the way {env:VAR} and {file:path} do today:

--upstream-header 'Authorization=Bearer {oauth2:someupstream}'

Config carries the token endpoint, client id, client secret reference, and scopes. The provider posts grant_type=client_credentials, caches the access token to its own expires_in, and renews ahead of expiry.

Why this belongs in umbra rather than the proxy path

#82's first open question was whether an OAuth loop belongs in umbra's value layer instead of mcp-beaver's proxy path, since umbra already owns credential resolution for spec mode. For client_credentials the answer is clearly umbra. It is a value that happens to be minted rather than read, it needs nothing from the MCP session, and putting it in the value registry means spec mode and upstream mode both get it from one implementation.

Why it is cheap, unlike #82

Every cost that makes #82 expensive is absent here:

  • No browser. client_credentials is machine to machine by definition, so nothing needs an attended seeding flow.
  • No refresh-token rotation. Nothing is written back, so no pod needs SSM write access.
  • No personal grant. The credential is a service identity rather than a long-lived grant on a person.
  • No client-registration contention. Nothing to collide with, so no per-deployment clientId dance.

What lands is a POST, a cache with an expiry, and serialized renewal so concurrent calls do not stampede the token endpoint.

Why it is worth building regardless of Moxn

The MCP specification pushed HTTP transports toward OAuth, so hosted third-party MCP servers keep arriving OAuth-shaped rather than API-key-shaped. --upstream-header covers a hosted upstream holding a durable static secret, which is the shape that is becoming less common, not more.

Moxn cannot use this. Its authorization server offers authorization_code and refresh_token only, with no client_credentials grant. That is exactly why this is filed separately: the generic capability should not wait on, or be shaped by, the one upstream that needs the harder version.

Notes for whoever builds it

  • Resolve per request from cache, matching how spec-mode auth and query pins already behave, so a rotated client secret takes effect without a restart.
  • Fail loud. A mint that cannot succeed surfaces as a clear error naming the token endpoint, never a silent unauthenticated request. Same rule #82 records.
  • /admin should report the scheme and never the credential, matching what 62474ac did for static headers.
  • Layer above the bounded transport, not below it, for the same reason 62474ac had to: boundedTransport only recognizes an *http.Transport and the other order drops the time-to-first-byte bound from #79.

Refs #82, coilyco-bridge/deploy#647

The cheap half of the OAuth problem, split out of #82 so it can be built on its own merits. Neither issue blocks the other, and this one has no human in the loop. ## What A token-minting value provider, so `{oauth2:<name>}` resolves the way `{env:VAR}` and `{file:path}` do today: ``` --upstream-header 'Authorization=Bearer {oauth2:someupstream}' ``` Config carries the token endpoint, client id, client secret reference, and scopes. The provider posts `grant_type=client_credentials`, caches the access token to its own `expires_in`, and renews ahead of expiry. ## Why this belongs in umbra rather than the proxy path #82's first open question was whether an OAuth loop belongs in umbra's value layer instead of mcp-beaver's proxy path, since umbra already owns credential resolution for spec mode. For `client_credentials` the answer is clearly umbra. It is a value that happens to be minted rather than read, it needs nothing from the MCP session, and putting it in the value registry means spec mode and upstream mode both get it from one implementation. ## Why it is cheap, unlike #82 Every cost that makes #82 expensive is absent here: * **No browser.** `client_credentials` is machine to machine by definition, so nothing needs an attended seeding flow. * **No refresh-token rotation.** Nothing is written back, so no pod needs SSM write access. * **No personal grant.** The credential is a service identity rather than a long-lived grant on a person. * **No client-registration contention.** Nothing to collide with, so no per-deployment `clientId` dance. What lands is a POST, a cache with an expiry, and serialized renewal so concurrent calls do not stampede the token endpoint. ## Why it is worth building regardless of Moxn The MCP specification pushed HTTP transports toward OAuth, so hosted third-party MCP servers keep arriving OAuth-shaped rather than API-key-shaped. `--upstream-header` covers a hosted upstream holding a durable static secret, which is the shape that is becoming less common, not more. **Moxn cannot use this.** Its authorization server offers `authorization_code` and `refresh_token` only, with no `client_credentials` grant. That is exactly why this is filed separately: the generic capability should not wait on, or be shaped by, the one upstream that needs the harder version. ## Notes for whoever builds it * Resolve per request from cache, matching how spec-mode `auth` and query pins already behave, so a rotated client secret takes effect without a restart. * Fail loud. A mint that cannot succeed surfaces as a clear error naming the token endpoint, never a silent unauthenticated request. Same rule #82 records. * `/admin` should report the scheme and never the credential, matching what `62474ac` did for static headers. * Layer above the bounded transport, not below it, for the same reason `62474ac` had to: `boundedTransport` only recognizes an `*http.Transport` and the other order drops the time-to-first-byte bound from #79. Refs #82, coilyco-bridge/deploy#647
Sign in to join this conversation.
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
coilyco-flight-deck/mcp-beaver#83
No description provided.