/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 APIhttps://api.demo.example/stocksThe API entry point for non MCP clients.
MCP Server Entry Pointhttps://mcp.demo.exampleEndpoint that the MCP client integrates with.
MCP Resource Server Metadata URLhttps://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 URLhttps://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 UIhttps://admin.demo.example/adminThe administration user interface for the Curity Identity Server.
Test Email Inboxhttps://mail.demo.exampleA mail server for testing purposes that lets you receive emails for test users.

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

Then follow the README instructions to add hostnames to the /etc/hosts file and configure operating system trust for the development SSL certificate. You will then be able to resolve the example URLs on your local computer.

Run MCP Clients

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 variety of clients that use the interoperable security standards from the MCP authorization protocol to connect to the deployed environment. The GitHub repository explains the setup instructions for each client.

MCP Authorization Technical Flow

Running an 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
123456789101112131415161718
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
scope: stocks/read

Discovery

When an 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 and scopes 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" scope="stocks/read"

The MCP client then calls the MCP server's resouce metadata URL, which returns details about supported scopes and the authorization server that the MCP server uses.

json
12345678910
{
"resource": "https://mcp.demo.example/",
"resource_name": "MCP Server",
"scopes_supported": [
"stocks/read"
],
"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

The client should then use the MCP Scope Selection Strategy and create a DCR request payload. 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": "MCP Inspector",
"grant_types": [
"authorization_code"
],
"redirect_uris": [
"http://localhost:6274/callback"
],
"response_types": [
"code"
],
"scope": "stocks/read",
"token_endpoint_auth_method": "client_secret_post"
}

Before creation of the dynamic client, the Curity Identity Server 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

The Curity Identity Server then creates a dynamic client, assigns it a generated client ID and client secret and returns it to the MCP client, which should persist the details and use them for subsequent connections to the Curity Identity Server.

json
123456789101112131415161718192021222324252627282930313233
{
"access_token_ttl": 900,
"audiences": [
"https://mcp.demo.example/"
],
"client_id": "15d64f90-5df1-4ab8-ae48-af0aa2e6d562",
"client_id_issued_at": 1759825565,
"client_name": "MCP Inspector",
"client_secret": "lYo5fRR4mMn9bTxIbigv9ZCn1lT6BEKuHA3zZpOxR0s",
"client_secret_expires_at": 0,
"default_acr_values": [
"urn:se:curity:authentication:email:email"
],
"grant_types": [
"authorization_code"
],
"post_logout_redirect_uris": [],
"redirect_uris": [
"http://localhost:6274/callback"
],
"require_proof_key": true,
"requires_consent": true,
"response_types": [
"code"
],
"scope": "stocks/read",
"subject_type": "public",
"token_endpoint_auth_method": "client_secret_post",
"token_endpoint_auth_methods": [
"client_secret_basic",
"client_secret_post"
]
}

User Authentication

User authentication uses an OAuth 2.1 code flow with parameters of the following form. The 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=15d64f90-5df1-4ab8-ae48-af0aa2e6d562
&response_type=code
&redirect_uri=http://localhost:65343/callback
&scope=stocks/read
&code_challenge=UDCiyI7N7vjI9vfz0w7m4exLFfHB4-clsyCgnTjh7EA
&code_challenge_method=S256
&resource=http://mcp.demo.example/
Host: login.demo.example

Each user's instance of the MCP client receives a distinct lient ID and client secret. 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=15d64f90-5df1-4ab8-ae48-af0aa2e6d562
&client_secret=lYo5fRR4mMn9bTxIbigv9ZCn1lT6BEKuHA3zZpOxR0s
&grant_type=authorization_code
&redirect_uri=http://localhost:65343/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 can interact with the secured MCP server's endpoints. On all requests, the MCP client includes the opaque access token in the HTTP Authorization header. The MCP client first requests a list of authorized tools. The MCP client can then invoke those tools on behalf of the user.

The MCP inspector is a technical tool that allows the user to invoke MCP operations manually:

MCP Inspector

In a real MCP host, like Claude Desktop, the user instead asks a natural language question and the AI agent can autonomously try to run any tool that the MCP server makes available to the client.

Claude Desktop

Developing MCP Servers

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;
response.locals.claimsPrincipal = new ClaimsPrincipal(result.payload);
next();
}

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);
}
}

The example MCP server also returns useful error responses to MCP clients, including WWW-Authenticate headers that comply with the MCP authorization protocol.

Developing MCP Clients

If you want to develop a custom MCP client in addition to MCP servers you can base an approach on the simpleOAuthClient.ts module from the TypeScript SDK example client, so that the underlying SDK does most of the work.

Also consider any particular requirements of MCP hosts that will run your MCP client. Many MCP hosts, like Claude, support a generic mechanism, where you configure a command and a set of arguments to connect to MCP servers. MCP hosts often provide a default MCP client implementation with a local proxy like mcp-remote, where users configure a connection with settings similar to those shown below.

json
12345678
{
"mcpServers": {
"curity-demo": {
"command": "npx",
"args": ["mcp-remote", "https://mcp.demo.example"],
}
}
}

Conclusion

This tutorial explained some key points of the MCP deployment example, so that a user can connect an MCP client to an MCP server and operate with least-privilege API access. The Curity Identity Server gives you control over the deeper security behaviors, so that your APIs can restrict the level of access granted to AI components.

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 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?