From 8b0e50e10e8568d805aa55e66cce9da9ebca7812 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 2 Oct 2025 16:08:24 +0100 Subject: [PATCH] Add MCP security (wip) ref docs Signed-off-by: Christian Tzolov --- .../src/main/antora/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/api/mcp/mcp-security.adoc | 645 ++++++++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc index 90467805eec..e7d5614c5dd 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/nav.adoc @@ -87,6 +87,7 @@ **** xref:api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc[Streamable-HTTP MCP Servers] **** xref:api/mcp/mcp-stateless-server-boot-starter-docs.adoc[Stateless Streamable-HTTP MCP Servers] // *** xref:api/mcp/mcp-helpers.adoc[MCP Utilities] +*** xref:api/mcp/mcp-security.adoc[MCP Security (WIP)] *** xref:api/mcp/mcp-annotations-overview.adoc[MCP Annotations] **** xref:api/mcp/mcp-annotations-client.adoc[Client Annotations] **** xref:api/mcp/mcp-annotations-server.adoc[Server Annotations] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc new file mode 100644 index 00000000000..289de9ebd42 --- /dev/null +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-security.adoc @@ -0,0 +1,645 @@ += MCP Security + +NOTE: This is still work in progress. The documentation and APIs may change in future releases. + +The Spring AI MCP Security module provides comprehensive OAuth 2.0 and API key-based security support for Model Context Protocol implementations in Spring AI. This community-driven project enables developers to secure both MCP servers and clients with industry-standard authentication and authorization mechanisms. + +NOTE: This module is part of the link:https://github.com/spring-ai-community/mcp-security[spring-ai-community/mcp-security] project and currently works with Spring AI's 1.1.x branch only. +This is a community-driven project and is not officially endorsed yet by Spring AI or the MCP project. + +== Overview + +The MCP Security module provides three main components: + +* *MCP Server Security* - OAuth 2.0 resource server and API key authentication for Spring AI MCP servers +* *MCP Client Security* - OAuth 2.0 client support for Spring AI MCP clients +* *MCP Authorization Server* - Enhanced Spring Authorization Server with MCP-specific features + +The project enables developers to: + +* Secure MCP servers with OAuth 2.0 authentication and API key-based access +* Configure MCP clients with OAuth 2.0 authorization flows +* Set up authorization servers specifically designed for MCP workflows +* Implement fine-grained access control for MCP tools and resources + +== MCP Server Security + +The MCP Server Security module provides OAuth 2.0 resource server capabilities for xref:api/mcp/mcp-server-boot-starter-docs.adoc[Spring AI's MCP servers]. +It also provides basic support for API-key based authentication. + +IMPORTANT: This module is compatible with Spring WebMVC-based servers only. + +=== Dependencies + +Add the following dependencies to your project: + +[tabs] +====== +Maven:: ++ +[source,xml] +---- + + + org.springaicommunity + mcp-server-security + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + +---- + +Gradle:: ++ +[source,groovy] +---- +implementation 'org.springaicommunity:mcp-server-security' +implementation 'org.springframework.boot:spring-boot-starter-security' + +// OPTIONAL: For OAuth2 support +implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' +---- +====== + +=== OAuth 2.0 Configuration + +==== Basic OAuth 2.0 Setup + +First, enable the MCP server in your `application.properties`: + +[source,properties] +---- +spring.ai.mcp.server.name=my-cool-mcp-server +# Supported protocols: STREAMABLE, STATELESS +spring.ai.mcp.server.protocol=STREAMABLE +---- + +Then, configure security using Spring Security's standard APIs with the provided MCP configurer: + +[source,java] +---- +@Configuration +@EnableWebSecurity +class McpServerConfiguration { + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUrl; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + // Enforce authentication with token on EVERY request + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + // Configure OAuth2 on the MCP server + .with( + McpServerOAuth2Configurer.mcpServerOAuth2(), + (mcpAuthorization) -> { + // REQUIRED: the issuerURI + mcpAuthorization.authorizationServer(issuerUrl); + // OPTIONAL: enforce the `aud` claim in the JWT token. + // Not all authorization servers support resource indicators, + // so it may be absent. Defaults to `false`. + // See RFC 8707 Resource Indicators for OAuth 2.0 + // https://www.rfc-editor.org/rfc/rfc8707.html + mcpAuthorization.validateAudienceClaim(true); + } + ) + .build(); + } +} +---- + +==== Securing Tool Calls Only + +You can configure the server to secure only tool calls while leaving other MCP operations (like `initialize` and `tools/list`) public: + +[source,java] +---- +@Configuration +@EnableWebSecurity +@EnableMethodSecurity // Enable annotation-driven security +class McpServerConfiguration { + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUrl; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + // Open every request on the server + .authorizeHttpRequests(auth -> { + auth.requestMatcher("/mcp").permitAll(); + auth.anyRequest().authenticated(); + }) + // Configure OAuth2 on the MCP server + .with( + McpResourceServerConfigurer.mcpServerOAuth2(), + (mcpAuthorization) -> { + // REQUIRED: the issuerURI + mcpAuthorization.authorizationServer(issuerUrl); + } + ) + .build(); + } +} +---- + +Then, secure your tool calls using the `@PreAuthorize` annotation with link:https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html[method security]: + +[source,java] +---- +@Service +public class MyToolsService { + + @PreAuthorize("isAuthenticated()") + @McpTool(name = "greeter", description = "A tool that greets you, in the selected language") + public String greet( + @ToolParam(description = "The language for the greeting (example: english, french, ...)") String language + ) { + if (!StringUtils.hasText(language)) { + language = ""; + } + return switch (language.toLowerCase()) { + case "english" -> "Hello you!"; + case "french" -> "Salut toi!"; + default -> "I don't understand language \"%s\". So I'm just going to say Hello!".formatted(language); + }; + } +} +---- + +You can also access the current authentication directly from the tool method using `SecurityContextHolder`: + +[source,java] +---- +@McpTool(name = "greeter", description = "A tool that greets the user by name, in the selected language") +@PreAuthorize("isAuthenticated()") +public String greet( + @ToolParam(description = "The language for the greeting (example: english, french, ...)") String language +) { + if (!StringUtils.hasText(language)) { + language = ""; + } + var authentication = SecurityContextHolder.getContext().getAuthentication(); + var name = authentication.getName(); + return switch (language.toLowerCase()) { + case "english" -> "Hello, %s!".formatted(name); + case "french" -> "Salut %s!".formatted(name); + default -> ("I don't understand language \"%s\". " + + "So I'm just going to say Hello %s!").formatted(language, name); + }; +} +---- + +=== API Key Authentication + +The MCP Server Security module also supports API key-based authentication. You need to provide your own implementation of `ApiKeyEntityRepository` for storing `ApiKeyEntity` objects. + +A sample implementation is available with `InMemoryApiKeyEntityRepository` along with a default `ApiKeyEntityImpl`: + +WARNING: The `InMemoryApiKeyEntityRepository` uses bcrypt for storing API keys, which is computationally expensive. It is not suited for high-traffic production use. For production, implement your own `ApiKeyEntityRepository`. + +[source,java] +---- +@Configuration +@EnableWebSecurity +class McpServerConfiguration { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http.authorizeHttpRequests(authz -> authz.anyRequest().authenticated()) + .with( + mcpServerApiKey(), + (apiKey) -> { + // REQUIRED: the repo for API keys + apiKey.apiKeyRepository(apiKeyRepository()); + + // OPTIONAL: name of the header containing the API key. + // Here for example, api keys will be sent with "CUSTOM-API-KEY: " + // Replaces .authenticationConverter(...) (see below) + // + // apiKey.headerName("CUSTOM-API-KEY"); + + // OPTIONAL: custom converter for transforming an http request + // into an authentication object. Useful when the header is + // "Authorization: Bearer ". + // Replaces .headerName(...) (see above) + // + // apiKey.authenticationConverter(request -> { + // var key = extractKey(request); + // return ApiKeyAuthenticationToken.unauthenticated(key); + // }); + } + ) + .build(); + } + + /** + * Provide a repository of {@link ApiKeyEntity}. + */ + private ApiKeyEntityRepository apiKeyRepository() { + var apiKey = ApiKeyEntityImpl.builder() + .name("test api key") + .id("api01") + .secret("mycustomapikey") + .build(); + + return new InMemoryApiKeyEntityRepository<>(List.of(apiKey)); + } +} +---- + +With this configuration, you can call your MCP server with a header `X-API-key: api01.mycustomapikey`. + +=== Known Limitations + +[IMPORTANT] +==== + +* The deprecated SSE transport is not supported. Use xref:api/mcp/mcp-streamable-http-server-boot-starter-docs.adoc[Streamable HTTP] or xref:api/mcp/mcp-stateless-server-boot-starter-docs.adoc[stateless transport]. +* WebFlux-based servers are not supported. +* Opaque tokens are not supported. Use JWT. + +==== + +== MCP Client Security + +The MCP Client Security module provides OAuth 2.0 support for xref:api/mcp/mcp-client-boot-starter-docs.adoc[Spring AI's MCP clients], supporting both HttpClient-based clients (from `spring-ai-starter-mcp-client`) and WebClient-based clients (from `spring-ai-starter-mcp-client-webflux`). + +IMPORTANT: This module supports `McpSyncClient` only. + +=== Dependencies + +[tabs] +====== +Maven:: ++ +[source,xml] +---- + + org.springaicommunity + mcp-client-security + +---- + +Gradle:: ++ +[source,groovy] +---- +implementation 'org.springaicommunity:mcp-client-security' +---- +====== + +=== Authorization Flows + +Three OAuth 2.0 flows are available for obtaining tokens: + +* *Authorization Code Flow* - For user-level permissions when every MCP request is made within the context of a user request +* *Client Credentials Flow* - For machine-to-machine use cases where no human is in the loop +* *Hybrid Flow* - Combines both flows for scenarios where some operations (like `initialize` or `tools/list`) happen without a user present, but tool calls require user-level permissions + +TIP: Use authorization code flow when you have user-level permissions and all MCP requests occur within user context. Use client credentials for machine-to-machine communication. Use hybrid flow when using Spring Boot properties for MCP client configuration, as tool discovery happens at startup without a user present. + +=== Common Setup + +For all flows, activate Spring Security's OAuth2 client support in your `application.properties`: + +[source,properties] +---- +# Ensure MCP clients are sync +spring.ai.mcp.client.type=SYNC + +# For authorization_code or hybrid flow +spring.security.oauth2.client.registration.authserver.client-id= +spring.security.oauth2.client.registration.authserver.client-secret= +spring.security.oauth2.client.registration.authserver.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.authserver.provider=authserver + +# For client_credentials or hybrid flow +spring.security.oauth2.client.registration.authserver-client-credentials.client-id= +spring.security.oauth2.client.registration.authserver-client-credentials.client-secret= +spring.security.oauth2.client.registration.authserver-client-credentials.authorization-grant-type=client_credentials +spring.security.oauth2.client.registration.authserver-client-credentials.provider=authserver + +# Authorization server configuration +spring.security.oauth2.client.provider.authserver.issuer-uri= +---- + +Then, create a configuration class activating OAuth2 client capabilities: + +[source,java] +---- +@Configuration +@EnableWebSecurity +class SecurityConfiguration { + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + // in this example, the client app has no security on its endpoints + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + // turn on OAuth2 support + .oauth2Client(Customizer.withDefaults()) + .build(); + } +} +---- + +=== HttpClient-Based Clients + +When using `spring-ai-starter-mcp-client`, configure a `McpSyncHttpClientRequestCustomizer` bean: + +[source,java] +---- +@Configuration +class McpConfiguration { + + @Bean + McpSyncClientCustomizer syncClientCustomizer() { + return (name, syncSpec) -> + syncSpec.transportContextProvider( + new AuthenticationMcpTransportContextProvider() + ); + } + + @Bean + McpSyncHttpClientRequestCustomizer requestCustomizer( + OAuth2AuthorizedClientManager clientManager + ) { + // The clientRegistration name, "authserver", + // must match the name in application.properties + return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer( + clientManager, + "authserver" + ); + } +} +---- + +Available customizers: + +* `OAuth2AuthorizationCodeSyncHttpRequestCustomizer` - For authorization code flow +* `OAuth2ClientCredentialsSyncHttpRequestCustomizer` - For client credentials flow +* `OAuth2HybridSyncHttpRequestCustomizer` - For hybrid flow + +=== WebClient-Based Clients + +When using `spring-ai-starter-mcp-client-webflux`, configure a `WebClient.Builder` with an MCP `ExchangeFilterFunction`: + +[source,java] +---- +@Configuration +class McpConfiguration { + + @Bean + McpSyncClientCustomizer syncClientCustomizer() { + return (name, syncSpec) -> + syncSpec.transportContextProvider( + new AuthenticationMcpTransportContextProvider() + ); + } + + @Bean + WebClient.Builder mcpWebClientBuilder(OAuth2AuthorizedClientManager clientManager) { + // The clientRegistration name, "authserver", must match the name in application.properties + return WebClient.builder().filter( + new McpOAuth2AuthorizationCodeExchangeFilterFunction( + clientManager, + "authserver" + ) + ); + } +} +---- + +Available filter functions: + +* `McpOAuth2AuthorizationCodeExchangeFilterFunction` - For authorization code flow +* `McpOAuth2ClientCredentialsExchangeFilterFunction` - For client credentials flow +* `McpOAuth2HybridExchangeFilterFunction` - For hybrid flow + +=== Working Around Spring AI Autoconfiguration + +Spring AI's autoconfiguration initializes MCP clients at startup, which can cause issues with user-based authentication. To avoid this: + +==== Option 1: Disable @Tool Auto-configuration + +Disable Spring AI's `@Tool` autoconfiguration by publishing an empty `ToolCallbackResolver` bean: + +[source,java] +---- +@Configuration +public class McpConfiguration { + + @Bean + ToolCallbackResolver resolver() { + return new StaticToolCallbackResolver(List.of()); + } +} +---- + +==== Option 2: Programmatic Client Configuration + +Configure MCP clients programmatically instead of using Spring Boot properties. For HttpClient-based clients: + +[source,java] +---- +@Bean +McpSyncClient client( + ObjectMapper objectMapper, + McpSyncHttpClientRequestCustomizer requestCustomizer, + McpClientCommonProperties commonProps +) { + var transport = HttpClientStreamableHttpTransport.builder(mcpServerUrl) + .clientBuilder(HttpClient.newBuilder()) + .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .httpRequestCustomizer(requestCustomizer) + .build(); + + var clientInfo = new McpSchema.Implementation("client-name", commonProps.getVersion()); + + return McpClient.sync(transport) + .clientInfo(clientInfo) + .requestTimeout(commonProps.getRequestTimeout()) + .transportContextProvider(new AuthenticationMcpTransportContextProvider()) + .build(); +} +---- + +For WebClient-based clients: + +[source,java] +---- +@Bean +McpSyncClient client( + WebClient.Builder mcpWebClientBuilder, + ObjectMapper objectMapper, + McpClientCommonProperties commonProperties +) { + var builder = mcpWebClientBuilder.baseUrl(mcpServerUrl); + var transport = WebClientStreamableHttpTransport.builder(builder) + .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .build(); + + var clientInfo = new McpSchema.Implementation("clientName", commonProperties.getVersion()); + + return McpClient.sync(transport) + .clientInfo(clientInfo) + .requestTimeout(commonProperties.getRequestTimeout()) + .transportContextProvider(new AuthenticationMcpTransportContextProvider()) + .build(); +} +---- + +Then add the client to your chat client: + +[source,java] +---- +var chatResponse = chatClient.prompt("Prompt the LLM to do the thing") + .toolCallbacks(new SyncMcpToolCallbackProvider(mcpClient1, mcpClient2, mcpClient3)) + .call() + .content(); +---- + +=== Known Limitations + +[IMPORTANT] +==== + +* Spring WebFlux servers are not supported. +* Spring AI autoconfiguration initializes MCP clients at app start, requiring workarounds for user-based authentication. +* Unlike the server module, the client implementation supports the SSE transport with both `HttpClient` and `WebClient`. + +==== + +== MCP Authorization Server + +The MCP Authorization Server module enhances link:https://docs.spring.io/spring-security/reference/7.0/servlet/oauth2/authorization-server/index.html[Spring Security's OAuth 2.0 Authorization Server] with features relevant to the link:https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization[MCP authorization spec], such as Dynamic Client Registration and Resource Indicators. + +=== Dependencies + +[tabs] +====== +Maven:: ++ +[source,xml] +---- + + org.springaicommunity + mcp-authorization-server + +---- + +Gradle:: ++ +[source,groovy] +---- +implementation 'org.springaicommunity:mcp-authorization-server' +---- +====== + +=== Configuration + +Configure the authorization server in your `application.yml`: + +[source,yaml] +---- +spring: + application: + name: sample-authorization-server + security: + oauth2: + authorizationserver: + client: + default-client: + token: + access-token-time-to-live: 1h + registration: + client-id: "default-client" + client-secret: "{noop}default-secret" + client-authentication-methods: + - "client_secret_basic" + - "none" + authorization-grant-types: + - "authorization_code" + - "client_credentials" + redirect-uris: + - "http://127.0.0.1:8080/authorize/oauth2/code/authserver" + - "http://localhost:8080/authorize/oauth2/code/authserver" + # mcp-inspector + - "http://localhost:6274/oauth/callback" + # claude code + - "https://claude.ai/api/mcp/auth_callback" + user: + # A single user, named "user" + name: user + password: password + +server: + servlet: + session: + cookie: + # Override the default cookie name (JSESSIONID). + # This allows running multiple Spring apps on localhost, and they'll each have their own cookie. + # Otherwise, since the cookies do not take the port into account, they are confused. + name: MCP_AUTHORIZATION_SERVER_SESSIONID +---- + +Then activate the authorization server capabilities with a security filter chain: + +[source,java] +---- +@Bean +SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + // all requests must be authenticated + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + // enable authorization server customizations + .with(McpAuthorizationServerConfigurer.mcpAuthorizationServer(), withDefaults()) + // enable form-based login, for user "user"/"password" + .formLogin(withDefaults()) + .build(); +} +---- + + +=== Known Limitations + +[IMPORTANT] +==== + +* Spring WebFlux servers are not supported. +* Every client supports ALL `resource` identifiers. + +==== + +== Samples and Integrations + +The link:https://github.com/spring-ai-community/mcp-security/tree/main/samples[samples directory] contains working examples for all modules in this project, including integration tests. + +With `mcp-server-security` and a supporting `mcp-authorization-server`, you can integrate with: + +* Cursor +* Claude Desktop +* link:https://modelcontextprotocol.io/docs/tools/inspector[MCP Inspector] + +NOTE: When using the link:https://modelcontextprotocol.io/docs/tools/inspector[MCP Inspector], you may need to disable CSRF and CORS protection. + +== Additional Resources + +* link:https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#communication-security[MCP Authorization Specification] +* link:https://github.com/spring-ai-community/mcp-security[MCP Security GitHub Repository] +* link:https://github.com/spring-ai-community/mcp-security/tree/main/samples[Sample Applications] +* link:https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization[MCP Authorization Specification] +* link:https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/index.html[Spring Security OAuth 2.0 Resource Server] +* link:https://docs.spring.io/spring-security/reference/servlet/oauth2/client/index.html[Spring Security OAuth 2.0 Client] +* link:https://docs.spring.io/spring-security/reference/7.0/servlet/oauth2/authorization-server/index.html[Spring Authorization Server] +