/images/resources/tutorials/writing-apis/mcp-authorization-implementation.png

Implementing MCP Authorization for APIs

Advanced Security
QualityAvailability
Download on GitHub
On this page

The Model Context Protocol (MCP) Authorization draft specification enables an open ecosystem where AI agents securely connect to APIs using OAuth. The Design MCP Authorization for APIs article explains the security considerations when organizations need to expose sensitive API data to AI agents using MCP. This page and the related GitHub repository supplement the design article with a practical example.

This tutorial first explains how to run the deployment example from the GitHub repository on your local computer. The deployment includes an MCP server as a secured entry point to APIs, so that AI agents can use MCP clients to connect, with zero-delay onboarding. The MCP technical flow section then highlights particular implementation details.

Example Overview

The code example exposes an API that provides secured information on stock prices to AI agents. Security controls must restrict who can access the data. In the example, an AI agent would only have read permissions to the user's authorized stock data. The overall flow uses a number of components, where all backend components run behind an API gateway. In the example, the MCP server is a stateless utility API that provides an API entry point for Streamable HTTP Transport requests.

AI Agent Flow

The Curity Identity Server acts as an OAuth authorization server to enable a number of security behaviors.

  1. The MCP client runs OAuth flows that use security standards.
  2. The MCP client receives a locked-down opaque access token.
  3. The MCP client calls the MCP server with the opaque access token.
  4. The Phantom Token Plugin introspects the opaque access token and sends a JWT access token to the MCP server.
  5. The MCP server validates the JWT access token and checks for its required audience.
  6. The MCP server tool uses token exchange to change the audience of the access token.
  7. The MCP server tool sends a JWT access token to upstream APIs.
  8. The upstream API implements the main authorization to control access to business resourcs.

Deploy Backend Components

The example deployment provides the following backend endpoints. To reduce infrastructure on a local computer the example uses HTTP URLs, whereas a real deployment would use HTTPS URLs.

EndpointURLDescription
Stocks APIhttp://api.demo.example/stocksThe API entry point for non MCP clients.
MCP Server Entry Pointhttp://mcp.demo.exampleEndpoint that the MCP client integrates with.
MCP Resource Server Metadata URLhttp://mcp.demo.example/.well-known/oauth-protected-resourceUsed by the MCP client to discover the MCP server's authorization server.
Curity Identity Server OAuth Metadata URLhttp://login.demo.example/.well-known/oauth-authorization-serverEndpoint of the Curity Identity Server that enables the MCP client to dynamically register.
Curity Identity Server Admin UIhttp://admin.demo.example/adminThe administration user interface for the Curity Identity Server.
Test Email Inboxhttp://mail.demo.exampleA mail server for testing purposes that lets you receive emails for test users.

To resolve these URLs on a local computer, add the following entries to the /etc/hosts file.

text
1
127.0.0.1 api.demo.example mcp.demo.example admin.demo.example login.demo.example mail.demo.example

Clone the GitHub link at the top of this page to a local folder and run a command shell in that folder. Ensure that your local computer meets the prerequisites from the repository's README file. Then run the following commands to deploy all backend components, where LICENSE_FILE_PATH points to your license file for the Curity Identity Server. If you do not have any license for the Curity Identity Server yet, download a trial license from Curity's Developer Portal.

bash
123
export LICENSE_FILE_PATH=~/Desktop/license.json
./build.sh
./deploy.sh

Run the MCP Client

To connect to secured APIs, AI agents should use an MCP client that implements the client side of the MCP draft authorization specification. In an open ecosystem of AI agents you typically do not control the MCP client's OAuth code and rely upon standards-based integrations. To demonstrate the flows, the GitHub repository includes a copy of the simple MCP OAuth client from the Model Context Protocol TypeScript SDK. The MCP client is a console application that you can run with the following commands.

bash
123
cd mcp-client
npm install
npm start

MCP Authorization Technical Flow

Running the MCP client triggers an initial request to http://mcp.demo.example which triggers the standards-based flow from the MCP authorization draft specification. The following sections explain some key points about the example deployment and the security techniques it uses.

API Gateway Routes

The example deployment uses the Kong API gateway to expose endpoints. The routes use host names and paths from client requests to invoke APIs. In the API gateway configuration, the MCP server uses the Kong Phantom Token Plugin and requires a valid opaque access token.

yaml
1234567891011121314151617
services:
- name: mcp-server
url: http://utility-mcp-server:3000/
routes:
- name: mcp-server-route
hosts:
- mcp.demo.example
paths:
- /
plugins:
- name: phantom-token
config:
introspection_endpoint: http://idsvr:8443/oauth/v2/oauth-introspect
client_id: api-gateway-client
client_secret: Password1
token_cache_seconds: 900
resource_metadata_url: http://mcp.demo.example/.well-known/oauth-protected-resource

Discovery

When the MCP client first connects to the MCP server it does not have a valid access token. According to the MCP authorization draft specification, the backend should receive an HTTP 401 response with a WWW-Authenticate response header that provides an OAuth protected resource metadata endpoint for the MCP server that the client calls. The phantom token plugin implements this response.

http
12
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error='"invalid_token", resource_metadata="http://mcp.demo.example/.well-known/oauth-protected-resource"

The MCP client then calls the MCP server's resouce metadata URL, which returns details about its authorization server.

json
1234567
{
"resource": "https://mcp.demo.example/",
"resource_name": "MCP Server",
"authorization_servers": [
"https://login.demo.example"
]
}

Next, the MCP client takes the authorization_servers value and appends ./well-known/oauth-authorization-server to it, to form the OAuth authorization server metadata URL. The MCP client calls this URL and receives data from the Curity Identity Server, which includes details about OAuth endpoints and client authentication.

json
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
{
"acr_values_supported": [
"urn:se:curity:authentication:email:email"
],
"authorization_endpoint": "http://login.demo.example/oauth/v2/oauth-authorize",
"authorization_response_iss_parameter_supported": true,
"authorization_signing_alg_values_supported": [
"PS256"
],
"claims_parameter_supported": true,
"code_challenge_methods_supported": [
"S256",
"plain"
],
"grant_types_supported": [
"refresh_token",
"implicit",
"client_credentials",
"password",
"https://curity.se/grant/accesstoken",
"authorization_code"
],
"introspection_endpoint": "http://login.demo.example/oauth/v2/oauth-introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"issuer": "http://login.demo.example/oauth/v2/oauth-anonymous",
"jwks_uri": "http://login.demo.example/oauth/v2/oauth-anonymous/jwks",
"pushed_authorization_request_endpoint": "http://login.demo.example/oauth/v2/oauth-authorize/par",
"registration_endpoint": "http://login.demo.example/token-service/oauth-registration",
"registration_endpoint_auth_methods_supported": [
"Bearer"
],
"registration_endpoint_auth_signing_alg_values_supported": [],
"response_modes_supported": [
"fragment",
"fragment.jwt",
"jwt",
"form_post",
"query",
"query.jwt",
"form_post.jwt"
],
"response_types_supported": [
"code",
"code id_token",
],
"scopes_supported": [
"address",
"phone",
"email",
"openid",
"profile",
"stocks/read"
],
"token_endpoint": "http://login.demo.example/oauth/v2/oauth-token",
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
]
}

Dynamic Client Registration

Next, the client makes an HTTP POST request to the Curity Identity Server's registration endpoint. To do so, the MCP client uses the registration_endpoint from authorization server metadata.

json
1234567891011121314
{
"client_name": "Simple OAuth MCP Client",
"grant_types": [
"authorization_code"
],
"redirect_uris": [
"http://localhost:8090/callback"
],
"response_types": [
"code"
],
"scope": "stocks/read",
"token_endpoint_auth_method": "client_secret_post"
}

The Curity Identity Server then creates a dynamic client, assigns it a generated client ID and client secret and also applies a policy for dynamic clients that use the stocks/read scope. This example policy enforces PKCE, sets the access token lifetime to 15 minutes and disables the use of refresh tokens. In the Curity Identity Server you add a JavaScript Pre-Processing Procedure to the registration endpoint to create a DCR policy.

javascript
12345678910111213141516171819
function result(context) {
var request = context.getRequest();
var httpMethod = request.getMethod();
var attributes = {};
if (httpMethod === 'POST') {
var body = request.getParsedBodyAsJson();
if (body && body.scope) {
if (body.scope.split(' ').indexOf('stocks/read') !== -1) {
attributes.require_proof_key = true;
attributes.access_token_ttl = 900;
attributes.refresh_token_ttl = 0;
}
}
}
return attributes;
}

To view the Dynamic Client Registration settings, log in to the Admin UI at http://admin.demo.example/admin with the credentials from the GitHub repository's README file. Then, navigate to Token ServiceGeneralDynamic Registration. You can find the JavaScript procedure under Token ServiceEndpoints →. Edit the line for token-service-dynamic-client-registration and view its pre-processing procedure.

DCR Policy

User Authentication

User authentication uses an OAuth 2.1 code flow with parameters of the following form. The example MCP client sends a resource parameter to instruct the authorization server where it wants to use its access token.

http
123456789
GET /oauth/v2/oauth-authorize?
client_id=9709bb67-c0ab-483f-beed-d40db1859abe&
response_type=code&
redirect_uri=http://localhost:8090/callback&
scope=stocks/read&
code_challenge=UDCiyI7N7vjI9vfz0w7m4exLFfHB4-clsyCgnTjh7EA&
code_challenge_method=S256&
resource=http://mcp.demo.example/
Host: login.demo.example

The example MCP client is a console app that runs a code flow with a client ID and client secret that are unique to each dynamic instance. User authentication is only allowed for administrator approved users (those included in the backend deployment). Users prove ownership of their corporate email and authenticate with a one-time password. When the browser requests an email address, enter one of the email addresses from the repository's README file. Then, navigate to the test email inbox at http://mail.demo.example, get the one-time password and paste it into the browser window.

Email Authentication

Next, act as a user who consents to the AI agent's requested level of access.

User Consent

After user authentication and consent, the MCP client receives an authorization code and the Curity Identity Server also returns an iss parameter. MCP clients should validate the iss value to help protect against authorization server mix-up attacks, as explained in the OAuth 2.0 Authorization Server Issuer Identification specification from RFC 9207.

Token Issuance

Finally, the MCP client uses its dynamic client ID and client secret and redeems the authorization code for OAuth tokens with a request of the following form.

http
1234567891011
POST /oauth/v2/token HTTP/1.1
Host: login.demo.example
Content-Type: application/x-www-form-urlencoded
client_id=582056b0-9bce-48c2-812d-777152b3a871&
client_secret=LrHBT_NmUFsa_WCkU0JAdhzJtnSc2M54ztA3bjYtNAg&
grant_type=authorization_code&
redirect_uri=http://localhost:8090/callback&
code=nqb2raZqZpX44uQ74CJnXwIDnXaYb6Xm&
code_verifier=WccahGqMpe4vJ.UIc.jJ7ZpGk6Ex2ig20GkJd2oMK&
resource=http://mcp.demo.example/

The MCP client receives a token response that includes a short-lived opaque access token with a limited scope.

json
123456
{
"access_token": "_0XBPWQQ_2fb1bc61-0e98-413c-a44d-d8a46d3bd2f2",
"expires_in": 900,
"scope": "stocks/read",
"token_type": "bearer"
}

Using MCP Tools

Once the example MCP client has a valid opaque access token, it starts an interactive shell. The user can run commands like list to view MCP tools that the MCP server implements. In all requests from the MCP client to the MCP server, the MCP client includes the opaque access token in the HTTP Authorization header.

text
12345678910111213141516171819202122232425262728293031323334353637
🚀 Simple MCP OAuth Client
Connecting to: http://mcp.demo.example
🔗 Attempting to connect to http://mcp.demo.example...
🔐 Creating OAuth provider...
🔐 OAuth provider created
👤 Creating MCP client...
👤 Client created
🔐 Starting OAuth flow...
🚢 Creating transport with OAuth provider...
🚢 Transport created
🔌 Attempting connection (this will trigger OAuth redirect)...
📌 OAuth redirect handler called - opening browser
Opening browser to: http://login.demo.example/oauth/v2/oauth-authorize?response_type=code&client_id=24ae8cd9-3d44-434e-9506-1342d76eea5c&code_challenge=u1IP4WbEWQQbS04foPIsNdjE28v_-8yQefhrqr9zE9M&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A8090%2Fcallback&scope=stocks%2Fread
🌐 Opening browser for authorization: http://login.demo.example/oauth/v2/oauth-authorize?response_type=code&client_id=24ae8cd9-3d44-434e-9506-1342d76eea5c&code_challenge=u1IP4WbEWQQbS04foPIsNdjE28v_-8yQefhrqr9zE9M&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A8090%2Fcallback&scope=stocks%2Fread
🔐 OAuth required - waiting for authorization...
OAuth callback server started on http://localhost:8090
📥 Received callback: /callback?iss=http%3A%2F%2Flogin.demo.example%2Foauth%2Fv2%2Foauth-anonymous&code=dodc5hVzHJ3kAKmHgnGRyiyNgkVIVyNx
✅ Authorization code received: dodc5hVzHJ...
🔐 Authorization code received: dodc5hVzHJ3kAKmHgnGRyiyNgkVIVyNx
🔌 Reconnecting with authenticated transport...
🚢 Creating transport with OAuth provider...
🚢 Transport created
🔌 Attempting connection (this will trigger OAuth redirect)...
✅ Connected successfully
🎯 Interactive MCP Client with OAuth
Commands:
list - List available tools
call <tool_name> [args] - Call a tool
quit - Exit the client
mcp> list
📋 Available tools:
1. fetch-stock-prices
Description: A tool to fetch secured information about financial stock prices

The user can then ask the MCP client to call the MCP server and get stocks data with the following command. The MCP server then returns some mock data that the stocks API provides.

text
1234
mcp> call fetch-stock-prices
🔧 Tool 'fetch-stock-prices' result:
[{"id":"COM1","name":Company 1","price":450.22},{"id":"COM2","name":"Company 2","price":250.62},{"id":"COM3","name":"Company 3","price":21.07}]

MCP Server

The code example implements a minimal, stateless MCP server with the TypeScript SDK, to provide API entry points for the MCP client.

typescript
1234567891011
const serverInfo = {
name: 'utility-mcp-server',
version: '1.0.0'
};
this.mcpServer = new McpServer(serverInfo);
this.mcpServer.tool(
'fetch-stock-prices',
'A tool to fetch secured information about financial stock prices',
this.fetchStockPricesFromApi,
);

The MCP server implements JWT access token validation, as for any other resource server. In particular, the MCP server only accepts access tokens whose audience restrictions include the MCP server's identity.

typescript
12345678910111213141516
public async validateAccessToken(request: Request, response: Response, next: NextFunction): Promise<void> {
{
const accessToken = this.readAccessToken(request);
if (!accessToken) {
throw new ApiError(401, 'invalid_token', 'Missing, invalid or expired access token');
}
const options = {
issuer: this.configuration.requiredIssuer,
audience: this.configuration.requiredAudience,
algorithms: [this.configuration.requiredJwtAlgorithm],
} as JWTVerifyOptions;
const claims = await jwtVerify(accessToken, this.remoteJwksSet, options);
return claims;
}

The main role of the MCP server is to call upstream APIs with a JWT access token and return responses to the MCP client. Before doing so, the MCP server uses OAuth token exchange to change the audience of the access token to that of the upstream API.

http
12345678910
POST /oauth/v2/oauth-token HTTP/1.1
Host: login.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&client_id=mcp-server
&client_secret=Password1
&subject_token=eyJraWQiOiItMTcyNTQxNzE2NyIsIng...
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&audience=http://api.demo.example

The following code demonstrates the MCP server tool's logic when it calls the upstream API.

typescript
123456789101112131415161718192021222324
private async fetchStockPricesFromApi(extra: RequestHandlerExtra<ServerRequest, ServerNotification>): Promise<CallToolResult> {
try {
const receivedAccessToken = extra.authInfo?.token || '';
const oauthClient = new TokenExchangeClient(this.configuration, this.errorHandler);
const exchangedAccessToken = await oauthClient.exchangeAccessToken(receivedAccessToken);
const apiClient = new StocksApiClient(this.configuration, this.errorHandler);
const data = await apiClient.getStocks(exchangedAccessToken);
console.log('MCP server successfully called stocks API');
return {
content: [{
type: "text",
text: data
}]
};
} catch (e: any) {
return this.toolErrorResponse(e as McpServerError);
}
}

MCP SDKs are relatively new and some behavior may currently lack interoperable solutions, like how MCP clients handle HTTP 401 or HTTP 403 responses from upstream APIs after MCP tool requests. In such cases, return useful error values to the MCP client, like the HTTP status code and the WWW-Authenticate header. The code example demonstrates this approach in its JSON-RPC error responses.

Conclusion

This tutorial explained some key points of the MCP deployment example, so that a user can securely connect an MCP client to an MCP server and then instruct the AI agent to use tools that call APIs with access tokens. The Curity Identity Server gives you control over the finer security details, so that you can grant the right level of access to MCP clients.

An OAuth architecure provides you with the building blocks to securely connect to APIs from any type of client. With the right separation it should be straightforward to add AI agents as a new type of API client. Done correctly, you should only need to implement a lightweight MCP server (or use an MCP gateway) to provide a new entry point to APIs.

Newsletter

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Newsletter

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial

Was this helpful?