Artículo

Autorización MCP con Spring AI y OAuth2

Autorización MCP con Spring AI y OAuth2

1. Visión general

Protocolo de Contexto del Modelo (MCP) permite que los modelos de IA accedan a datos empresariales a través de APIs seguras. Cuando construimos servidores MCP que manejan información sensible, necesitamos una autorización adecuada para controlar quién puede acceder a qué datos.

OAuth2 proporciona seguridad basada en tokens que funciona bien con sistemas MCP. En lugar de crear autenticaciones personalizadas, podemos usar los estándares OAuth2 para proteger nuestros servidores MCP y gestionar el acceso de los clientes.

En este artículo veremos cómo asegurar servidores y clientes MCP usando Spring AI y OAuth2. Construiremos un ejemplo completo con tres componentes: un servidor de autorización, un servidor MCP protegido con herramientas de cálculo y un cliente que maneja tanto las peticiones de usuarios como las del sistema.

2. Arquitectura de Seguridad de MCP

Para asegurar servidores MCP, es importante entender cómo integraremos un servidor de autorización antes del servidor MCP.

Nuestro sistema incluye un Servidor de Autorización para emitir tokens JWT con los permisos adecuados, un Servidor MCP para validar tokens y controlar el acceso a las herramientas de cálculo. Además, hay un Cliente MCP que obtiene tokens y gestiona la autenticación para diferentes tipos de peticiones:

flujo de autenticación mcp cliente servidor oauth 988x1024-1-289x300

Los servidores MCP actúan como Servidores de Recursos OAuth2. Verifican los tokens JWT en los encabezados de las peticiones antes de procesar cualquier operación. Esto separa las preocupaciones de seguridad de la lógica empresarial. Los clientes obtienen tokens de acceso de un servidor de autorización OAuth2. Luego, los clientes incluyen estos tokens en las peticiones MCP. Finalmente, los servidores MCP validan los tokens antes de permitir las operaciones. Así funcionarán nuestros componentes:

3. Construyendo el Servidor de Autorización

Comenzaremos con el servidor de autorización ya que los demás componentes dependen de él.

3.1. Añadiendo Dependencias

Necesitamos agregar la dependencia del servidor de autorización OAuth2 para usarla:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.2. Configurando el Servidor de Autorización

Configuraremos el servidor en application.yml:

server:
  port: 9000

spring:
  security:
    user:
      name: user
      password: password
    oauth2:
      authorizationserver:
        client:
          oidc-client:
            registration:
              client-id: "mcp-client"
              client-secret: "{noop}mcp-secret"
              client-authentication-methods:
                - "client_secret_basic"
              authorization-grant-types:
                - "authorization_code"
                - "client_credentials"
                - "refresh_token"
              redirect-uris:
                - "http://localhost:8080/authorize/oauth2/code/authserver"
              scopes:
                - "openid"
                - "profile"
                - "calc.read"
                - "calc.write"

Esto configura un servidor de autorización en el puerto 9000 con un cliente que admite tanto el flujo de código de autorización (para usuarios) como el flujo de credenciales de cliente (para sistemas).

4. Asegurando el Servidor MCP

Ahora crearemos un servidor MCP que requiera tokens OAuth2 y proporcione herramientas de cálculo.

4.1. Configurando Dependencias para el Servidor MCP

Agregamos el soporte del servidor de recursos OAuth2:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    <version>1.0.0-M7</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

4.2. Configuración del Servidor

Configuramos el servidor MCP como un servidor de recursos OAuth2:

server.port=8090

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:9000

spring.ai.mcp.server.enabled=true
spring.ai.mcp.server.name=mcp-calculator-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.stdio=false

Spring Boot maneja automáticamente la validación JWT cuando configuramos la URI del emisor. Cada solicitud a nuestro servidor MCP ahora requiere un token JWT válido en el encabezado Authorization.

4.3. Creando Herramientas MCP

Los LLMs suelen ser malos en matemáticas. Por eso, debemos darles acceso a herramientas que puedan calcular los resultados según la solicitud:

@Tool(description = "Add two numbers")
public CalculationResult add(
  @ToolParam(description = "First number") double a,
  @ToolParam(description = "Second number") double b) {
      double result = a + b;
      return new CalculationResult("addition", a, b, result);
}

@Tool(description = "Multiply two numbers")
public CalculationResult multiply(
  @ToolParam(description = "First number") double a,
  @ToolParam(description = "Second number") double b) {
      double result = a * b;
      return new CalculationResult("multiplication", a, b, result);
}

La configuración de seguridad protege automáticamente todas las herramientas MCP. Las peticiones sin tokens válidos son rechazadas. Estas herramientas se añaden al contexto y se entregan al LLM en cada consulta del usuario. Luego el LLM decide qué herramienta usar para responder la consulta del usuario.

5. Construyendo el Cliente MCP

Ahora necesitamos el cliente para manejar la parte más compleja ya que debe gestionar tanto las peticiones de usuarios como la inicialización del sistema.

5.1. Dependencias para el Cliente

Agregamos las dependencias del mcp-client y del oauth2-client:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

5.2. Configuración del Cliente

Ahora necesitamos configurar dos registros de cliente OAuth2 en application.properties:

server.port=8080

spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8090
spring.ai.mcp.client.type=SYNC

spring.security.oauth2.client.provider.authserver.issuer-uri=http://localhost:9000

# Cliente OAuth2 para peticiones iniciadas por el usuario (Authorization Code Grant)
spring.security.oauth2.client.registration.authserver.client-id=mcp-client
spring.security.oauth2.client.registration.authserver.client-secret=mcp-secret
spring.security.oauth2.client.registration.authserver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.authserver.provider=authserver
spring.security.oauth2.client.registration.authserver.scope=openid,profile,mcp.read,mcp.write
spring.security.oauth2.client.registration.authserver.redirect-uri={baseUrl}/authorize/oauth2/code/{registrationId}

# Cliente OAuth2 para peticiones máquina a máquina (Client Credentials Grant)
spring.security.oauth2.client.registration.authserver-client-credentials.client-id=mcp-client
spring.security.oauth2.client.registration.authserver-client-credentials.client-secret=mcp-secret
spring.security.oauth2.client.registration.authserver-client-credentials.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.authserver-client-credentials.provider=authserver
spring.security.oauth2.client.registration.authserver-client-credentials.scope=mcp.read,mcp.write

spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}

Necesitamos dos registros para distintos flujos de autenticación. El registro authserver utiliza el flujo de código de autorización para peticiones iniciadas por usuarios. El registro authserver-client-credentials utiliza el flujo de credenciales de cliente para el arranque del sistema.

5.3. Configuración de Seguridad

Configuramos Spring Security para manejar OAuth2:

@Bean
WebClient.Builder webClientBuilder(McpSyncClientExchangeFilterFunction filterFunction) {
    return WebClient.builder()
      .apply(filterFunction.configuration());
}

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
      .oauth2Client(Customizer.withDefaults())
      .csrf(CsrfConfigurer::disable)
      .build();
}

5.4. Seleccionando el Token Adecuado

Todo el reto aquí es elegir el token correcto para cada petición. Necesitamos una implementación personalizada de ExchangeFilterFunction que detecte el contexto de la petición:

@Component
public class McpSyncClientExchangeFilterFunction implements ExchangeFilterFunction {

    private final ClientCredentialsOAuth2AuthorizedClientProvider clientCredentialTokenProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
    private final ServletOAuth2AuthorizedClientExchangeFilterFunction delegate;
    private final ClientRegistrationRepository clientRegistrationRepository;
    private static final String AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID = "authserver";
    private static final String CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID = "authserver-client-credentials";

    public McpSyncClientExchangeFilterFunction(OAuth2AuthorizedClientManager clientManager,
        ClientRegistrationRepository clientRegistrationRepository) {
        this.delegate = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientManager);
        this.delegate.setDefaultClientRegistrationId(AUTHORIZATION_CODE_CLIENT_REGISTRATION_ID);
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes) {
            return this.delegate.filter(request, next);
        }
        else {
            var accessToken = getClientCredentialsAccessToken();
            var requestWithToken = ClientRequest.from(request)
              .headers(headers -> headers.setBearerAuth(accessToken))
              .build();
            return next.exchange(requestWithToken);
        }
    }

    private String getClientCredentialsAccessToken() {
        var clientRegistration = this.clientRegistrationRepository
          .findByRegistrationId(CLIENT_CREDENTIALS_CLIENT_REGISTRATION_ID);

        var authRequest = OAuth2AuthorizationContext.withClientRegistration(clientRegistration)
          .principal(new AnonymousAuthenticationToken("client-credentials-client", "client-credentials-client",
             AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
          .build();
        return this.clientCredentialTokenProvider.authorize(authRequest).getAccessToken().getTokenValue();
    }

    public Consumer<WebClient.Builder> configuration() {
        return builder -> builder.defaultRequest(this.delegate.defaultRequest()).filter(this);
    }
}

El filtro comprueba si hay una solicitud web activa. Si la hay, usa el token de código de autorización del usuario. Si no (por ejemplo, durante el arranque de la aplicación), usa credenciales de cliente.

6. Utilizando el Sistema MCP Seguro

Ahora que tenemos todos los componentes cubiertos, veamos cómo podemos usar el sistema MCP seguro de manera efectiva.

6.1. Creando un ChatClient

Conectamos todo con un ChatClient:

@Bean
ChatClient chatClient(ChatClient.Builder chatClientBuilder, List<McpSyncClient> mcpClients) {
    return chatClientBuilder.defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpClients))
      .build();
}

6.2. Haciendo Peticiones

Ahora podemos usar el ChatClient normalmente. La seguridad ocurre automáticamente:

@GetMapping("/calculate")
public String calculate(@RequestParam String expression, @RegisteredOAuth2AuthorizedClient("authserver") OAuth2AuthorizedClient authorizedClient) {
    String prompt = String.format("Please calculate the following mathematical expression using the available calculator tools: %s", expression);

    return chatClient.prompt()
      .user(prompt)
      .call()
      .content();
}

Durante el arranque, la inicialización del cliente MCP utiliza tokens de credenciales de cliente. Cuando los usuarios hacen peticiones a través de la interfaz web, se usan sus tokens de código de autorización.

7. Verificando la Configuración

Para entender cómo funciona la aplicación, necesitamos revisar los resultados que produce. Antes de iniciar cualquier aplicación, debemos configurar las variables de entorno necesarias para el LLM. Tras la configuración, arrancamos el servidor de autorización en el puerto 9000 primero. Esto será importante ya que todos los demás módulos dependen del servidor de autorización. Luego arrancamos el servidor MCP en el puerto 8090, seguido por el cliente MCP en el puerto 8080.

Probar el flujo completo se vuelve sencillo. Visita el endpoint del cliente MCP y prueba lo siguiente:

http://{base_url}:8080/calculate?expression=15+25

El cliente obtendrá un token del servidor de autorización, llamará al servidor MCP con el token y devolverá el resultado del cálculo. Debemos asegurarnos de iniciar sesión en el servidor de autorización con las credenciales especificadas en los archivos de configuración.

8. Conclusión

En este tutorial exploramos cómo OAuth2 proporciona una seguridad robusta para sistemas MCP mediante la autorización basada en tokens estándar. El soporte OAuth2 de Spring Security permite una buena protección con una configuración mínima. Al separar el servidor de autorización, el servidor MCP y el cliente MCP, creamos una arquitectura donde cada componente se centra en su propia responsabilidad, otorgando flexibilidad.

Newsletter Semanal de Java

Cada viernes recibe lo más nuevo del ecosistema Java: frameworks, herramientas y mejores prácticas.

Sin spam. Cancela cuando quieras.

Compartir artículo