Replies: 1 comment
-
I've managed to make it work. I use Keycloak and here is some code.
import static org.springframework.security.config.Customizer.withDefaults;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
/**
* The MCP server security configuration.
*/
@Configuration
public class McpSecurityConfiguration {
/**
* Requires authentication for the /mcp endpoint.
*
* @param http the HTTP configuration
* @param mcpServerProperties the MCP server properties
* @param mcpAuthenticationEntryPoint the MCP authentication entry point
* @return the security filter chain
* @throws Exception the exception
*/
@Bean
@Order(10)
public SecurityFilterChain mcpSecurityFilterChain(HttpSecurity http,
McpServerStreamableHttpProperties mcpServerProperties,
McpAuthenticationEntryPoint mcpAuthenticationEntryPoint) throws Exception {
return http
// Require authentication for /mcp
.securityMatcher(mcpServerProperties.getMcpEndpoint())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)
// Convert JWT token to a user details
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(new McpJwtAuthenticationConverter())))
// Return 401 HTTP status code and WWW-Authenticate header if not authenticated yet
.exceptionHandling(exception -> exception.defaultAuthenticationEntryPointFor(
mcpAuthenticationEntryPoint,
PathPatternRequestMatcher.withDefaults().matcher(mcpServerProperties.getMcpEndpoint())))
.build();
}
}
import java.util.List;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
/**
* Converts {@link Jwt} to {@link OAuth2AuthenticationToken}.
* <p>
* Used to adapt bearer authentication token to OIDC token.
*/
public class McpJwtAuthenticationConverter implements Converter<Jwt, OAuth2AuthenticationToken> {
@Override
public OAuth2AuthenticationToken convert(Jwt source) {
var token = new OidcIdToken(source.getTokenValue(), source.getIssuedAt(),
source.getExpiresAt(), source.getClaims());
var principal = new DefaultOidcUser(List.of(), token);
return new OAuth2AuthenticationToken(principal, List.of(), "keycloak");
}
}
import java.util.LinkedHashMap;
import java.util.stream.Collectors;
import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerStreamableHttpProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Adds WWW-Authenticate HTTP response header containing resource metadata URL
* that can be used by MCP clients for authentication in accordance with RFC
* 9728. The header also contains error details according to RFC 6750 if any.
*/
@Component
public class McpAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Value("${your-application.backend-url}")
private String backendUrl;
private final McpServerStreamableHttpProperties mcpServerProperties;
/**
* Instantiates a new MCP authentication entry point.
*
* @param mcpServerProperties the MCP server properties
*/
public McpAuthenticationEntryPoint(McpServerStreamableHttpProperties mcpServerProperties) {
this.mcpServerProperties = mcpServerProperties;
}
/**
* Adds WWW-Authenticate header.
*
* @param request the request
* @param response the response
* @param authenticationException the authentication exception
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) {
var status = HttpStatus.UNAUTHORIZED;
var parameters = new LinkedHashMap<String, String>();
parameters.put("resource_metadata",
backendUrl + ProtectedResourceController.PROTECTED_RESOURCE + mcpServerProperties.getMcpEndpoint());
if (authenticationException instanceof OAuth2AuthenticationException oauthAuthenticationException) {
var error = oauthAuthenticationException.getError();
parameters.put("error", error.getErrorCode());
if (StringUtils.hasText(error.getDescription())) {
parameters.put("error_description", error.getDescription());
}
if (StringUtils.hasText(error.getUri())) {
parameters.put("error_uri", error.getUri());
}
if (error instanceof BearerTokenError bearerTokenError) {
if (StringUtils.hasText(bearerTokenError.getScope())) {
parameters.put("scope", bearerTokenError.getScope());
}
status = bearerTokenError.getHttpStatus();
}
}
response.setStatus(status.value());
response.addHeader(HttpHeaders.WWW_AUTHENTICATE,
"Bearer" + parameters.entrySet().stream()
.map(param -> param.getKey() + "=\"" + param.getValue() + "\"")
.collect(Collectors.joining(", ", " ", "")));
}
}
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
/**
* Provides /.well-known/oauth-protected-resource and
* /.well-known/oauth-authorization-server endpoints.
*/
@RestController
public class ProtectedResourceController {
/** The Constant PROTECTED_RESOURCE. */
public static final String PROTECTED_RESOURCE = "/.well-known/oauth-protected-resource";
@Value("${your-application.backend-url}")
private String backendUrl;
@Value("${your-application.oauth2-server-url}")
private String oauth2ServerUrl;
/**
* Provides a resource metadata for an MCP client according to RFC 9728.
* Contains authorization server URL.
*
* @param request the request
* @return the resource metadata
*/
@GetMapping(PROTECTED_RESOURCE + "/**")
public Map<String, Object> resourceMetadata(HttpServletRequest request) {
return Map.of(
"resource", backendUrl + request.getRequestURI().substring(PROTECTED_RESOURCE.length()),
"authorization_servers", List.of(oauth2ServerUrl),
"bearer_methods_supported", List.of("header"));
}
/**
* Redirects to an OAuth 2.0 authorization server returning metadata according
* to RFC 8414. Required by some MCP clients, for example Cursor IDE.
*
* @return the response entity
*/
@GetMapping("/.well-known/oauth-authorization-server")
public ResponseEntity<Void> redirectToKeycloak() {
var headers = new HttpHeaders();
headers.add("Location", oauth2ServerUrl + "/.well-known/openid-configuration");
return new ResponseEntity<>(headers, HttpStatus.FOUND);
}
}
I've also tried LM Studio but it seems it doesn't support authentication at all. Tried VS Code but it shows |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
I have an MCP server. How to enable OAuth for the server so 3rd party MCP clients (VS Code, LM Studio, ...) will be able to authenticate users?
Beta Was this translation helpful? Give feedback.
All reactions