Files
Zakaria El OrcheandClaude Opus 5 f28fc43150 feat(ext-security): add an OAuth 2.1 authorization server
flash-ext-security-oauth-server issues RFC 9068 access tokens (code + PKCE S256,
CIMD and DCR clients, RFC 8707 resources, rotating refresh tokens) for resources on
the application's own origin. Around it: SecurityExtension resolves a configured
origin instead of X-Forwarded-* headers, mechanisms expose schemes() and a route can
be restricted to some of them, McpConfig.mechanisms(...) uses that, OIDC bearers must
be typed at+jwt, and PublicUrl guards outbound fetches against internal addresses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 11:39:09 +00:00

2.9 KiB

Security

The MCP endpoint is secured by flash-ext-security-core: whatever mechanisms the application registers — OAuth2 bearer tokens, API keys, custom ones — authenticate /mcp exactly as they authenticate every other route.

McpConfig.security(...)
REQUIRED (default) every call must be authenticated; boot fails without a SecurityExtension
NONE a public endpoint; a tool carrying security annotations fails the boot

OAuth2 protected resource

When a registered mechanism publishes an OAuth2 issuer — flash-ext-security-oidc does — the endpoint behaves as the MCP authorization spec requires, with nothing to configure:

  • GET /.well-known/oauth-protected-resource/mcp serves RFC 9728 metadata: the resource (the application's SecurityExtension.origin(...) plus the path), every issuer as authorization_servers, and scopes_supported when McpConfig.scopesSupported(...) is set;
  • an anonymous call gets 401 with WWW-Authenticate: Bearer resource_metadata="…";
  • a token whose aud does not include the resource is 403 (RFC 8707) and logged at WARN. Credentials that are not audience-bound, such as API keys, are unaffected.

For Keycloak, the audience comes from an Audience protocol mapper whose included custom audience is the resource URL, attached to a client scope every MCP client receives (the built-in basic scope is the one that needs no client cooperation). Clients that register dynamically need Keycloak's anonymous client registration policies relaxed for the trusted hosts.

McpConfig.requireTokenAudience(false) drops that last check for an authorization server that cannot mint a resource audience at all — Keycloak ignores RFC 8707's resource parameter, so a deployment that cannot add the mapper has no other way in. Every token a registered issuer signs is then accepted on the endpoint, and the boot logs say so.

Which credentials

By default every mechanism in the chain authenticates /mcp, and the session cookie too. McpConfig.mechanisms(...) narrows that to the ones named: nothing else is a credential on the endpoint, and only their issuers are published — so a client is sent to exactly the authorization server the endpoint trusts.

McpConfig.builder("app").toolsPackage("com.example.tools").mechanisms(authorizationServer).build();

Tool policies

The core annotations work on tools as on handlers, checked per tools/call against the caller the route authenticated:

@Tool(name = "approve", description = "Approves a pending proposal")
@RolesAllowed(value = "REVIEWER", on = {"project", "locale"})   // read from the tool's arguments
public class ApproveTool extends McpTool {  }

A denial is a tool result with isError: true — the call reached the server, the tool did not run.

McpConfig.middleware(...) runs after authentication, for rate limiting, auditing or tracing.