Skip to content

Latest commit

 

History

History
41 lines (28 loc) · 2.75 KB

File metadata and controls

41 lines (28 loc) · 2.75 KB

oauth-client-credentials

OAuth 2.0 client_credentials grant — machine-to-machine MCP auth, fully self-verifying with no browser.

client_credentials is the grant a backend service uses to authenticate as itself (not on behalf of a user): it presents a pre-registered client_id/client_secret directly to the Authorization Server's token endpoint and receives a Bearer access token. There is no redirect, no authorization code, no user consent screen.

The interactive authorization-code flow (the one that opens a browser and asks a human to sign in) lives under ../oauth/; the runner drives it headlessly via the demo AS's OAUTH_DEMO_AUTO_CONSENT=1 auto-approve mode.

What runs

  • server.ts starts two listeners in one process:
    • the MCP resource server on --portcreateMcpHandler behind requireBearerAuth from @modelcontextprotocol/express, advertising the AS via mcpAuthMetadataRouter (RFC 9728 + RFC 8414).
    • a minimal client_credentials-only Authorization Server on --port + 1 (createClientCredentialsAuthServer from @mcp-examples/shared). The repo's full better-auth/OIDC demo AS only implements authorization_code, so this story ships its own purpose-built AS.
  • client.ts first asserts a bare request is 401 with a WWW-Authenticate challenge, then connects with a ClientCredentialsProvider on the transport. The SDK auth driver discovers the AS from the challenge, posts grant_type=client_credentials (HTTP Basic auth) to /token, attaches the returned Bearer token, and the whoami tool's ctx.authInfo carries the granted clientId and scopes end to end.

Run it

pnpm --filter @mcp-examples/oauth-client-credentials server -- --http --port 3000
pnpm --filter @mcp-examples/oauth-client-credentials client -- --http http://127.0.0.1:3000/mcp

HTTP-only; runs on both protocol eras (the client honours --legacy via parseExampleArgs().era).

private_key_jwt client authentication

To authenticate the client_credentials grant with a signed JWT assertion (RFC 7523 §2.2) instead of a shared secret, swap ClientCredentialsProvider for PrivateKeyJwtProvider:

import { PrivateKeyJwtProvider } from '@modelcontextprotocol/client';

const authProvider = new PrivateKeyJwtProvider({
    clientId: 'my-service',
    privateKey: pemEncodedKey,
    algorithm: 'RS256'
});

The full snippet lives in Machine auth › Sign with a private key instead of a secret (guides/clients/machine-auth.examples.tsprivateKeyJwt_provider). There is no runnable leg for it in this story — the in-repo client_credentials AS only implements client_secret_basic/client_secret_post.