Skip to content

Upstreams that need a minted bearer: oauth2-client-credentials auth block + CIPP preset - #21

Merged
selic merged 1 commit into
mainfrom
feat/upstream-oauth2-auth
Jul 29, 2026
Merged

Upstreams that need a minted bearer: oauth2-client-credentials auth block + CIPP preset#21
selic merged 1 commit into
mainfrom
feat/upstream-oauth2-auth

Conversation

@selic

@selic selic commented Jul 29, 2026

Copy link
Copy Markdown
Member

Why

CIPP's MCP endpoint (/api/ExecMcp, behind App Service Easy Auth) wants a finished Authorization: Bearer <access token>, not a static key — verified against the NDR instance: 401 + PRM advertising the tenant's Entra as AS, and Easy Auth's allowed audiences include the CIPP API clients, so app-only tokens are accepted. Pasting such a token into an upstream spec works for exactly one hour.

Planner-style upstreams don't have this problem because our servers accept long-lived credentials in headers and mint internally. For third-party servers the caller must mint — so the gateway learns to.

What

Optional upstream spec block:

"auth": { "kind": "oauth2-client-credentials",
          "tokenUrl": "https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token",
          "clientId": "<app id>", "clientSecret": "kv:cipp-mcp-secret",
          "scope": "api://<app id>/.default" }
  • Mint at connect: the secret resolves through the usual ${VAR}/bao:/kv: machinery (so only a reference is stored), the header defaults to Authorization: Bearer … and is overridable.
  • Refresh: a minted token is fixed for the transport's lifetime, so connect() tears down and rebuilds the connection once the token is within 60s of expiry — the next call transparently rides a fresh token.
  • Failures at the token endpoint surface as connect errors (never a silent unauthenticated connect); neither secret nor token is logged.
  • Per-user upstream links inherit the block automatically (the manager clones the spec).

Plus a CIPP preset (url / tenant / client id / secret ref) with deliberately restrictive grants — viewer: none, editor: none, admins only. Reason: CIPP exports its tools annotated read-only even for ExecGetLocalAdminPassword, ExecGetRecoveryKey, ExecBreachSearch — so tier-derived access would hand LAPS passwords and BitLocker keys to every viewer.

Tests

218 passing (8 new). New upstream/oauth2-auth.test.ts runs a fake server that plays both the token endpoint and a bearer-protected MCP upstream, asserting: the grant shape (client_credentials, scope, secret pulled from the store), calls succeed with the minted token, a near-expiry token is re-minted and the call still succeeds (the fake rejects superseded tokens), and a broken token endpoint fails the connect.

Release v0.10.0. After deploy: install CIPP via the preset (secret written on the Secrets tab first) so prod stops relying on the hand-pasted token.

🤖 Generated with Claude Code

Some third-party MCP servers (CIPP behind App Service Easy Auth) expect an
Authorization: Bearer <access token> rather than a static key — so a pasted
token dies within the hour. New optional spec block

  auth: { kind: "oauth2-client-credentials", tokenUrl, clientId,
          clientSecret: "kv:…", scope, header?, prefix? }

makes the gateway do the client-credentials exchange itself at connect time:
the secret is resolved from the store/env like any injected value, the token
is cached with the connection, and connect() rebuilds the transport once the
token is within 60s of expiry (headers are fixed per transport). Neither the
secret nor the token is logged. Also a CIPP preset — admin-only grants by
design: its tools arrive annotated read-only even when they surface LAPS
passwords and BitLocker keys.

Release v0.10.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@selic
selic merged commit e490b5c into main Jul 29, 2026
@selic
selic deleted the feat/upstream-oauth2-auth branch July 29, 2026 15:18
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