Securing your MCP servers

|
Share
Securing your MCP servers

When we first released the Box MCP server, it operated exclusively over the STDIO transport protocol  —  a simple, stdin/stdout communication model perfect for local Claude Desktop integrations. Authentication was straightforward: the MCP server ran in the user’s local environment with their Box credentials, and there was an implicit trust boundary.

But as the Model Context Protocol ecosystem evolved, we recognized the need to support HTTP and Server-Sent Events (SSE) transports. These protocols enable powerful new scenarios: MCP servers running as shared services, cloud-hosted agents, and multi-user deployments. However, they also introduced a critical security challenge that STDIO never had: How do we authenticate remote MCP clients connecting to our server over the network?

This question led us down an interesting architectural journey that resulted in implementing the OAuth 2.1 Protected Resource pattern (RFC 9728) that MCP clients expect ,  essentially making our MCP server use the OAuth 2.1 Protected Resource. This uncovered some fascinating insights about security boundaries, authentication patterns, and the inherent complexity of integrating multiple OAuth systems.

In this post, I’ll walk you through the challenges we faced, the decisions we made, and what we learned about building secure, flexible MCP servers that integrate with enterprise APIs like Box.

Two separate authentication layers

As we implemented HTTP and SSE transports, it quickly became clear that we weren’t dealing with a single authentication problem . We were dealing with two distinct authentication layers, each serving a different purpose.

Layer 1: MCP client ↔ MCP server authentication

Question: Who is making this request?

When an MCP client connects to our server over HTTP or SSE, we need to verify:

  • Is this a legitimate client?
  • Is this request authenticated and authorized?
  • Should we process this request or reject it?

This is standard API authentication :  protecting our MCP server from unauthorized access.

Layer 2: MCP server ↔ Box API authentication

Question: What Box resources can be accessed?

Once we’ve authenticated the MCP client, our server needs to interact with the Box API:

  • Which Box user’s content should we access?
  • What permissions do we have?
  • Are we acting as a service account or on behalf of a user?

This is Box API authentication :  proving to Box that we have the right to access specific resources.

MCP Auth Flow

At first glance, this dual authentication might seem like unnecessary complexity. Why can’t we just use one OAuth flow for everything? The answer lies in understanding that these two authentication layers serve fundamentally different purposes and exist at different trust boundaries.

Why this separation is intentional

The dual authentication pattern isn’t a bug or an oversight . It’s a deliberate architectural decision that provides crucial flexibility for developers. Here’s why keeping these authentication layers separate is actually a feature.

The power of flexibility

By decoupling MCP client authentication from Box API authentication, we enable two fundamentally different deployment scenarios.

Scenario 1: The Specialized Agent (Service Account)

Imagine you’re building an AI agent that helps your finance team with quarterly reporting. This agent needs access to a specific set of financial documents in Box ,  and only those documents, regardless of which team member is using the agent.

MCP Auth flow for specialized agent

With a service account, the Box API authentication is independent of who’s using the MCP client. Every user gets access to the same carefully curated Box content. The MCP authentication verifies who can use the agent, but the Box authentication determines what content is available, and these are completely separate concerns.

Scenario 2: The personal assistant (user-based)

Now imagine a general-purpose AI assistant that helps users manage their own Box files. Each user should access their own Box content, not a shared repository.

MCP Auth flow for personal assistant

In this scenario, the MCP server needs to rely on the MCP client to handle the authentication.

Different trust boundaries

These two authentication layers exist at fundamentally different trust boundaries:

MCP client authentication: Protects the MCP server infrastructure

  • “Can this client connect to my server?”
  • “Is this request legitimate?”
  • Infrastructure-level security

Box API authentication: Protects Box content and determines access scope

  • “What Box resources can be accessed?”
  • “Are we acting as a service account or a user?”
  • Data-level security

Enterprise flexibility

This separation also allows enterprises to plug in their own authentication requirements:

  • MCP layer: Use your organization’s SSO, OAuth provider, or custom auth
  • Box layer: Use Box OAuth, CCG (Client Credentials Grant), or JWT based on your Box app configuration

For example, a company might require their own domain authentication for MCP clients (ensuring only company employees can access the agent), while using a Box service account for consistent, controlled access to company documents.

Starting simple: Token-based authentication

Before diving into OAuth 2.1, we started with the simplest possible solution for MCP client authentication: Bearer token authentication.

The Box API authentication is still in effect, using one of the supported modes, OAuth, CCG, or JWT.

The concept is straightforward:

  1. Generate a secret token (essentially a strong password)
  2. Configure it in your environment: BOX_MCP_SERVER_AUTH_TOKEN=your_secret_token_here
  3. MCP clients include it in the Authorization header: Authorization: Bearer your_secret_token_here
  4. The MCP server validates the token before processing any requests
# Simplified middleware concept
class AuthMiddleware:
   def __init__(self, app):
       self.app = app
       self.expected_token = os.getenv("BOX_MCP_SERVER_AUTH_TOKEN")
  
   async def __call__(self, scope, receive, send):
       # Extract Authorization header
       auth_header = get_header(scope, "authorization")
      
       if not auth_header or not auth_header.startswith("Bearer "):
           return unauthorized_response()
      
       token = auth_header.split(" ")[1]
      
       if token != self.expected_token:
           return unauthorized_response()
      
       # Token valid, proceed
       await self.app(scope, receive, send)

This approach is perfect for:

  • Single-user deployments: One person running their own MCP server
  • Trusted environments: Development, testing, or internal networks
  • Simple use cases: Quick setup without OAuth complexity
  • Service-to-service communication: When you control both client and server

However, token-based authentication has some drawbacks:

  • No user identity: The server knows the request is authenticated, but not who the user is
  • Shared secrets: If multiple users need access, they all use the same token
  • Manual token management: Tokens must be generated, distributed, and rotated manually
  • No standard discovery: Clients need out-of-band configuration to know how to authenticate

The OAuth 2.1 gap

As we thought about broader MCP ecosystem adoption, we realized that MCP clients implementing the full MCP specification would expect to discover authentication requirements automatically using OAuth 2.1 standards (RFC 9728 — OAuth Protected Resource Metadata).

A standards-compliant MCP client should be able to:

  1. Query /.well-known/oauth-protected-resource to discover authentication requirements
  2. Obtain a token from the specified authorization server
  3. Use that token to access the MCP server

Our simple token authentication, while functional, didn’t support this discovery mechanism. This is what led us to implement the OAuth 2.0 Protected Resource endpoints  —  not to replace token authentication, but to support MCP clients that follow the full OAuth 2.1 specification.

Implementing MCP OAuth 2.1 with Box API

To prove that standards-based OAuth 2.1 authentication could work with our MCP server, we needed to implement the server-side protected resource endpoints and test them with a real authorization server. 

Following RFC 9728 (OAuth 2.0 Protected Resource Metadata), we implemented the discovery endpoints that MCP clients expect:

# Simplified OAuth 2.1 Protected Resource Metadata endpoint
{
   "authorization_servers": [
       "https://account.box.com"
   ],
   "bearer_methods_supported": [
       "header"
   ],
   "resource": "http://my.host.local:8005/mcp",
   "resource_documentation": "https://developer.box.com/",
   "scopes_supported": [
       "root_readwrite",
       "ai.readwrite"
   ]
}

This endpoint tells MCP clients:

  • Where to get tokens: The authorization server URL
  • How to send tokens: Via Authorization header
  • What this server is: A protected MCP resource

In turn, the MCP client queries the authorization servers to get more information about the specific OAuth implementation:

# https://account.box.com/.well-known/oauth-authorization-server
{
 "issuer": "https://api.box.com",
 "authorization_endpoint": "https://account.box.com/api/oauth2/authorize",
 "token_endpoint": "https://api.box.com/oauth2/token",
 "response_types_supported": [
   "code"
 ],
 "grant_types_supported": [
   "authorization_code",
   "refresh_token"
 ],
 "token_endpoint_auth_methods_supported": [
   "client_secret_basic",
  "client_secret_post"
 ],
 "revocation_endpoint": "https://api.box.com/oauth2/revoke",
 "revocation_endpoint_auth_methods_supported": [
   "client_secret_basic",
   "client_secret_post"
 ],
 "code_challenge_methods_supported": [
   "S256"
 ],
 "service_documentation": "https://developer.box.com/guides/authentication/",
 "op_policy_uri": "https://www.box.com/legal/termsofservice",
 "op_tos_uri": "https://www.box.com/legal/termsofservice"
}

Now the MCP client has everything it needs to start the OAuth authorization flow.

Testing with OAuth 2.1

Using the MCP inspector to test this, we configure our remote MCP server, and set the Client ID , and Client Secret :

MCP Inspector connecting

MCP inspector connecting using specific client id and secret

And it worked! When an MCP client implements the OAuth 2.1 flow, our server successfully:

  1. Advertised its authentication requirements via discovery endpoints
  2. Performs the initial OAuth authorization flow
  3. Stores the credentials internally

By implementing RFC 9728, our MCP server became compatible with the broader OAuth 2.1 ecosystem. Any MCP client that implements the OAuth 2.1 discovery flow, and provides a way of setting the client and secret, can now authenticate with our server.

Working around DCR

Box API does not support dynamic client registration (DCR).

Not all MCP clients behave the same, sometimes not even from the same company. For example, Claude Desktop supports configuring the client id and secret, but Claude Code does not, and requires DCR.

To get this to work we need to:

  • Add the registration endpoint to the Box authorization server metadata
  • Create our own registration endpoint that always returns the client id and secret specified in our Box API application

The first one is rather simple. We replace the Box authorization endpoint with our own:

{
 "resource": "http://my.host.local:8005/mcp",
 "authorization_servers": [
   "http://my.host.local:8005"
 ],
 "scopes_supported": [
   "root_readwrite",
   "ai.readwrite"
 ],
 "bearer_methods_supported": [
   "header"
 ],
 "resource_documentation": "https://developer.box.com/"
}

In the implementation of thehttp://my.host.local:8005/.well-known/oauth-authorization-serveendpoint, we query the original https://account.box.com/.well-known/oauth-authorization-server , and if the registration endpoint is not present, we inject our own:

{
 "issuer": "https://api.box.com",
 "authorization_endpoint": "https://account.box.com/api/oauth2/authorize",
 "token_endpoint": "https://api.box.com/oauth2/token",
----------------------------------------------------------------------
 "registration_endpoint": "http://my.host.local:8005/oauth/register",
----------------------------------------------------------------------
 "scopes_supported": [
   "root_readwrite",
   "ai.readwrite"
 ],
 "response_types_supported": [
   "code"
 ],
 "grant_types_supported": [
   "authorization_code",
   "refresh_token"
 ],
 "token_endpoint_auth_methods_supported": [
   "client_secret_basic",
   "client_secret_post"
 ],
 "service_documentation": "https://developer.box.com/guides/authentication/",
 "revocation_endpoint": "https://api.box.com/oauth2/revoke",
 "revocation_endpoint_auth_methods_supported": [
   "client_secret_basic",
   "client_secret_post"
 ],
 "code_challenge_methods_supported": [
   "S256"
 ],
 "op_policy_uri": "https://www.box.com/legal/termsofservice",
 "op_tos_uri": "https://www.box.com/legal/termsofservice"
}

When the MCP client hits that endpoint, it sends this payload:

{
 "redirect_uris": [
   "http://localhost:6274/oauth/callback/debug"
 ],
 "token_endpoint_auth_method": "none",
 "grant_types": [
   "authorization_code",
   "refresh_token"
 ],
 "response_types": [
   "code"
 ],
 "client_name": "MCP Inspector",
 "client_uri": "https://github.com/modelcontextprotocol/inspector",
 "scope": "root_readwrite ai.readwrite"
}

Now all we need to do is send back our specific client id and secret:

{
 "redirect_uris": [
   "http://localhost:6274/oauth/callback/debug"
 ],
 "token_endpoint_auth_method": "none",
 "grant_types": [
   "authorization_code",
   "refresh_token"
 ],
 "response_types": [
   "code"
 ],
 "client_id": "...",
 "client_secret": "...",
 "client_id_issued_at": 1762277599,
 "client_secret_expires_at": 0
}

From here the authorization flow is the same as before.

However this is only a partial implementation. If the MCP client sends a callback URI that is not previously registered in the app, Box will reject it.

Here is our Box application configuration with some common URI for MCP clients:

IMG

Common redirect URIs

Current state

With this release of the Box Community MCP server, we now have a lot of flexibility for authentication and consequently for use cases.

usage: mcp_server_box.py [-h] [--transport {stdio,sse,http}] [--host HOST]
                        [--port PORT]
                        [--mcp-auth-type {oauth,token,none}]
                        [--box-auth-type {oauth,ccg,jwt,mcp_client}]

Box Community MCP Server

options:
 -h, --help            show this help message and exit
 --transport {stdio,sse,http}
                       Transport type (default: stdio)
 --host HOST           Host for SSE/HTTP transport (default: localhost)
 --port PORT           Port for SSE/HTTP transport (default: 8005)
 --mcp-auth-type {oauth,token,none}
                       Authentication type for MCP server (default:
                       token)
 --box-auth-type {oauth,ccg,jwt,mcp_client}
                       Authentication type for Box API (default: oauth)

MCP authentication types

OAuth uses the MCP client OAuth protocol and implements partial Dynamic Client Registration to simplify the authentication setup process. This method can accept specific client ID and client secret credentials that are configured on the MCP client and must be used with --box-auth-type=mcp_client. Through this configuration, Box authentication is delegated to the MCP client, which handles the OAuth flow and credential management on behalf of the MCP server.

Token provides API key style authentication for the MCP server by configuring an access token that the MCP server validates against incoming requests. This authentication method operates independently of Box API authentication and requires server-side Box authentication through oauthccg, or jwt. Token authentication cannot be used with --box-auth-type=mcp_client, as it requires the server to handle Box authentication directly rather than delegating it to the client.

None provides no authentication validation between the MCP client and server, assuming all requests from the MCP client are valid. This authentication method operates independently of Box API authentication, working seamlessly with all Box authentication types including oauthccgjwt, and mcp_client. It’s important to note that the stdio transport automatically forces the usage of none authentication, making it the default choice for standard input/output communication channels.

Box authentication types

The Box API always requires authentication. The MCP server supports multiple Box authentication methods:

Box OAuth 2.0 provides standard user authentication by opening a browser for user authorization. The system maintains the user’s Box security context throughout the session and requires proper configuration of both the Box OAuth client ID and client secret.

Box CCG provides server-side authentication that requires a Box Custom App with CCG enabled. This method authenticates as either a Service Account or a specific user and requires a Box admin to enable the application. This supports service accounts scenarios where direct user interaction isn’t needed.

Box JWT authentication provides server-side authentication using JSON Web Tokens and requires a Box JWT App with a public/private key pair. This also supports service account scenarios where direct user interaction isn’t needed.

MCP Client authentication delegates Box authentication to the MCP client. When using mcp-auth-type=oauth, the MCP client configures Box OAuth during the MCP OAuth flow, while mcp-auth-type=none requires the MCP client to send a valid Box API bearer token in the authorization header. This approach is useful when the MCP client handles Box authorization through any supported method, including developer tokens, OAuth, CCG, or JWT.

Compatibility Matrix

MCP auth type: OAuth

  • Box auth type: OAuth -> ❌ Not supported
  • Box auth type: CCG -> ❌ Not supported
  • Box auth type: JWT -> ❌ Not supported
  • Box auth type: MCP_Client -> ✅ Supported
    Recommended for Claude Desktop. Supports sending client ID and client secret configurations in MCP client. Partial support of Dynamic Client Registration

MCP auth type: Token

  • Box auth type: OAuth -> ✅ Supported
  • Box auth type: CCG -> ✅ Supported
  • Box auth type: JWT -> ✅ Supported
  • Box auth type: MCP_Client -> ❌ Not supported
    Token requires server-side Box auth

MCP auth type: None

  • Box auth type: OAuth -> ✅ Supported
  • Box auth type: CCG -> ✅ Supported
    Common for development
  • Box auth type: JWT -> ✅ Supported
    Common for development
  • Box auth type: MCP Client -> ✅ Supported
    Requires Box API valid Bearer token sent in Authorization header

Complex scenarios

Scaling beyond single-user and server setups

While the basic MCP server configuration works well for individual developers working with a single server, complex scenarios emerge when your company needs to run a multi-user and multi MCP server scenario. 

In these enterprise environments, you’re no longer dealing with just one person connecting to one server . Instead, you have multiple users who need access to multiple MCP servers simultaneously, each potentially requiring different permissions and authentication credentials. 

Managing this web of connections quickly becomes unwieldy if each user-server pair requires separate credential configuration. This is where architectural patterns like authentication proxies become essential. 

Authentication proxy offers a solution by extracting authentication into a separate service that can handle credential management, token rotation, and access control centrally, allowing your MCP servers to focus on their core functionality while the proxy handles the security layer for all users and servers in your infrastructure.

Auth proxy scenario

Pros:

  • Centralized credential management: Store and manage all authentication credentials in one place rather than scattered across individual user configurations, reducing the risk of credential sprawl and making updates easier to deploy
  • Simplified user experience: Users don’t need to configure or maintain credentials for each MCP Server they access  —  the proxy handles authentication transparently, making onboarding faster and reducing support burden
  • Consistent access control: Enforce uniform authorization policies across all MCP servers from a central location, ensuring that permission changes take effect immediately across your entire infrastructure

Cons:

  • Single point of failure: If the authentication proxy goes down, all users lose access to all MCP servers, making high availability architecture and redundancy critical but more complex to implement
  • Added latency: Every request must pass through an additional network hop for authentication, which can introduce noticeable delays, especially for high-frequency operations or geographically distributed teams
  • Increased infrastructure complexity: Requires deploying, maintaining, and securing an additional service layer, including considerations for load balancing, failover, and disaster recovery

Conclusion

Building secure MCP servers that integrate with enterprise APIs like Box reveals an important truth: Authentication is rarely simple, and that complexity often serves a purpose.

What we’ve learned

Through implementing HTTP and SSE transports for the Box MCP server, we discovered that dual authentication isn’t a bug  —  it’s a feature that provides crucial flexibility:

  • MCP client authentication answers “Who is calling my server?” and protects your infrastructure
  • Box API authentication answers “What content can be accessed?” and determines the scope of operations

These serve different purposes, exist at different trust boundaries, and enable different deployment patterns ,  from specialized agents with curated content (service accounts) to personal assistants accessing user files (OAuth per user).

What we’ve built

Our implementation demonstrates that standards-based authentication works:

✅ OAuth 2.1 Protected Resource endpoints (RFC 9728) enable standards-compliant MCP client discovery
✅ Flexible authentication modes support everything from simple Bearer tokens to full OAuth 2.1 flows
✅ Proven Box API integration works with OAuth, CCG, and JWT authentication methods

The path forward

For multi-user deployments, the embedded token management within the MCP server  —  adding the token mapping, storage, and Box OAuth onboarding flows needed to support multiple users accessing their own Box content. This approach serves the majority of use cases while keeping deployment simple.

For organizations with more complex requirements  ( multiple MCP servers, centralized authentication, or multi-backend support ) the authentication proxy pattern offers an interesting alternative worth exploring. We’ve documented this approach not as the “right” answer, but as a valid architectural option for specific scenarios.

The bigger picture

The challenges we’ve encountered with the Box MCP server aren’t unique to Box. Any MCP server that integrates with an enterprise API will face similar questions:

  • How do you authenticate MCP clients?
  • How do you map authenticated users to backend credentials?
  • Where should token management logic live?
  • How do you balance simplicity with flexibility?

There’s no universal “correct” answer . It depends on your deployment model, scale, security requirements, and organizational structure.

An invitation

The Box MCP server is open source, and we believe these authentication patterns benefit from community input and experimentation. Whether you:

  • Have ideas for improving our token management approach
  • Want to prototype an authentication proxy
  • Are solving similar problems in your own MCP servers
  • Have questions about the architecture

We’d love to hear from you. Visit our GitHub repository, open issues, submit pull requests, or start discussions about authentication patterns in the MCP ecosystem.

Building secure, flexible AI agent infrastructure is a team sport. Let’s figure this out together.

Resources