Prerequisites
- An Authdog OIDC issuer for the environment that issues access tokens.
- An MCP resource URL, normally the public server URL plus
/mcp. - JWT access tokens and either an issuer or a direct JWKS URI.
- Required MCP scopes chosen for the service and for sensitive tools.
- Bun and Wrangler for the included Cloudflare Worker sample.
Key bindings: AUTHDOG_OIDC_ISSUER, JWKS_URI, AUTHDOG_OIDC_AUDIENCE, MCP_REQUIRED_SCOPES, MCP_PROTECTED_RESOURCE, MCP_JWT_ONLY. Opaque-token introspection is opt-in via MCP_ALLOW_OPAQUE_INTROSPECTION=true.
Implementation
1. The four controls
Authenticate (authenticateRequest) verifies the token against JWKS and serves RFC 9728 protected-resource metadata. Trust store (checkTrustStore) denies unverified non-human identities and caps their scopes (MCP_TRUST_STORE_MODE=off|monitor|enforce, default off). Authorize (withAuthdogToolGuard) asks Authdog's AuthZEN PDP whether the identity may invoke this specific tool (MCP_AUTHZ_MODE=off|monitor|enforce, default monitor). Telemetry emits MCP_TOOL_INVOKED/DENIED/FAILED events feeding audit, SIEM, and the gated MCP threat detectors, raw arguments are never sent, only a digest.
2. Run the sample
cd examples/mcp-auth-sample
bun install
bun run devCopy .dev.vars.example to .dev.vars and set your environment's issuer, JWKS, audience, and required scope (e.g. mcp:invoke).
3. Authenticate before the transport runs
The sample calls authenticateRequest(request, env) before forwarding /mcp. It discovers OIDC metadata, verifies the JWT, and checks issuer, audience, and scopes. Missing or invalid credentials produce 401; missing scopes produce 403.
4. Re-authorize inside privileged tools
Authentication protects the service; it doesn't authorize every side effect. The sample's admin_echo handler applies a narrower check:
requireClaimScopes(auth.claims, ["mcp:admin"])5. Publish protected-resource metadata
buildProtectedResourceMetadata(request, env) returns the resource URL, authorization servers, and supported scopes. Publish it at /.well-known/oauth-protected-resource.
Security considerations
- Prefer bearer tokens in the
Authorizationheader, the query-token fallback (MCP_ALLOW_QUERY_TOKEN) is not recommended. - Use
MCP_JWT_ONLY=truewhen the deployment accepts only JWT access tokens. - Don't enable opaque introspection merely because stale credentials exist, it requires explicit configuration and stays server-side.
- Set
AUTHDOG_OIDC_AUDIENCEto the actual MCP resource URL, this stops a token minted for another resource from being reused. - Distinguish service-wide scopes from tool-level scopes, authentication alone doesn't authorize a tool's side effects.
Validation checklist
- Metadata endpoint returns the intended resource and authorization server.
/mcprejects a request withoutAuthorization: Bearer.- Valid JWT succeeds only with matching issuer and audience.
- Missing baseline scope returns
403. - Sensitive tool rejects a token lacking its tool-specific scope.
MCP_JWT_ONLY=trueprevents introspection.- Logs and error handling never expose bearer tokens or introspection secrets.
Troubleshooting
"Missing token verification settings": neither JWT verification nor introspection is fully configured, set an issuer or JWKS URI.
"Token audience mismatch": match AUTHDOG_OIDC_AUDIENCE, MCP_PROTECTED_RESOURCE, and the public /mcp URL.
403 reporting missing scopes: signature and claims verification succeeded, but a required scope wasn't granted.
Next steps
- Inspect
examples/mcp-auth-sample/src/index.tsfor request ordering and tool checks. - Inspect
packages/mcp-sdk/src/auth.tsfor exact exported configuration. - Read Machine identity for non-human OAuth clients.
- Apply Authorization to each state-changing MCP tool.