Cognito-based MCP Servers
How to implement an MCP server supporting OAuth using AWS Cognito. In this article, I would like to show you not only how to do it, but also provide a step-by-step guide on working with it. Authentication is changing a lot in the MCP area. If you don’t believe me, check the FastMCP warning on the Authentication page. Keep that in mind as it might change over time. I will try to keep this article updated.
Pirate ship, AKA what we want to achieve
Root flow
Let’s assume our MCP server is exposed under http://localhost:8080/mcp. In this scenario, our resource server will also act as an authorization server. How does it work? Let’s start from the beginning. Let’s create our test environment.
Running debugger
We can do so by running MCP Inspector with a command:
npx @modelcontextprotocol/inspectorAfter that, we will see the welcome MCP Inspector screen where we can point it to http://localhost:8080/mcp like on the screen below. There is an extra info there about Auth section.
After clicking this button, we can see a view where we can play with the whole auth flow, which is super helpful whenever you are implementing such a flow (which might be complicated).
We have an option to use a Guided OAuth Flow flow that goes through the OAuth flow step-by-step, so it might be easier for you as an engineer to follow what is going on and setup endpoints to correctly set it. Another option is to go with Quick OAuth Flow that will automate going through all steps on your behalf, which is very useful when you are trying to set up the correct auth and you are retrying for the 123562153761725 time.
Since we are starting with an empty MCP Server, we have no OAuth endpoints there yet. So after starting the flow, we can get (as expected) an error.
Specification
The whole flow is described in the official MCP Documentation. Since we have /mcp set in the inspector as our server MCP resource, it is being used according to the 1st flow described below in the specs.
Metadata Discovery
The MCP Inspector attempts to determine how your server is protected and attempts to load the advertised authentication flow. It is proceeding with multiple requests being sent to your server that try to discover right endpoint. Here is an example flow executed by the MCP inspector.
Protected resource redirection
Before we go to the implementation. Let’s think for a second about how our server consumer is actually redirected to discover our server metadata in real life, not triggered over the inspector, which is a fake OAuth start attempt.
When we are trying to connect to our Server, the client is trying to initialize a connection with the MCP server by calling it over the /mcp endpoint. When our server validates that requests and the token is missing or invalid, we have to return an error with a 401 status and a WWW-Authenticate header with information about the protected resource endpoint.
WWW-Authenticate: Bearer resource_metadata=\"http://localhost:8080/.well-known/oauth-protected-resource/mcp\"After implementing the OAuth middleware, which validates the correct Authorization token, our flow will evolve a bit. It has just started with a connection attempt, followed by the exact same discovery. So we can assume that we can progress from there using the MCP inspector OAuth flow debugger.
Metadata Resource Discovery implementation
We now need to expose the endpoint that will serve protected resource information, as specified in the RFC here.
We can implement a response that returns the one below. Authorization server will be exposed under / and our resource (MCP endpoints) under /mcp with additional info about supported scopes.
GET http://localhost:8080/.well-known/oauth-protected-resource/mcpHTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 25 Aug 2025 13:03:12 GMT
Content-Length: 177{
"authorization_servers": [
"http://localhost:8080"
],
"bearer_methods_supported": [
"header"
],
"resource": "http://localhost:8080/mcp",
"scopes_supported": [
"openid",
"profile",
"email"
]
}
So now, we are returning 200 here, and our flow evolves into flow with limited tries of protected resource discovery, but it still fails the whole discovery process.
Metadata OpenID implementation
Since we are using Cognito, we can implement returning the OpenID configuration. We can have an implementation that loads data from the official Cognito well-known URL https://cognito-idp.<region-id>.amazonaws.com/<user-pool-id>/.well-known/openid-configuration, extends it, and returns it to our MCP Client.
We want to add a couple of customizations that will be important in the next steps. Most important things:
- Override
token_endpointas we don’t want our clients to provide client secret explicitly in case of not going with DCR flow - Advertize
registration_endpointto not fail clients that depend on DCR flow
Example response could look like that:
GET http://localhost:8080/.well-known/openid-configurationHTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 25 Aug 2025 13:52:53 GMT
Content-Length: 1540{
"authorization_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/oauth2/authorize",
"client_id": "69eahpaqpmtbgljh6bufp42f0",
"code_challenge_methods_supported": [
"S256"
],
"end_session_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/logout",
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"issuer": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_b2xK16XUA",
"jwks_uri": "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_b2xK16XUA/.well-known/jwks.json",
"redirect_uris": [
"http://127.0.0.1:33418",
"http://127.0.0.1:35535/oauth/callback",
"http://localhost:3080/api/mcp/mcp-local-api/oauth/callback",
"http://localhost:3080/c/new",
"http://localhost:3080/oauth/openid/callback",
"http://localhost:33418",
"http://localhost:35535/oauth/callback",
"http://localhost:55570/oauth/callback",
"http://localhost:6274/oauth/callback",
"http://localhost:6274/oauth/callback/debug",
"http://localhost:8080",
"https://vscode.dev/redirect"
],
"registration_endpoint": "http://localhost:8080/register",
"response_types_supported": [
"code",
"token"
],
"revocation_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/oauth2/revoke",
"scopes_supported": [
"openid",
"email",
"phone",
"profile"
],
"subject_types_supported": [
"public"
],
"token_endpoint": "http://localhost:8080/oauth/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"userinfo_endpoint": "https://mcp-auth.auth.eu-west-1.amazoncognito.com/oauth2/userInfo"
}
Now, a small success! We got 1st green step!
Dynamic Client Registration (DCR)
The next step on the MCP Inspector calls list is “Client registration”. The idea is that the MCP Server will automatically create a client for itself to reduce the friction in the authentication client registration process.
DCR is extremely useful, especially since different clients use various paths and ports, which can make management cumbersome. However, the question is whether you would like to add such functionality. Cognito does not support it out of the box, so you will have to implement it yourself. How could this be implemented if we were to support it? We can invoke the CreateUserPoolClient API based on the request input and return the clientId to the customer along with the clientSecret.
The client will invoke your server with a similar payload to the one below, so you can easily create a Cognito client if you wish.
POST /register HTTP/1.1{
"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": "openid profile email"
}I am not a big fan of this approach. I agree with Aaron Parecki that “The problem is most commercial APIs are not going to enable Dynamic Client Registration on their production servers. For example, in order to get client credentials to use the Google APIs, you need to register as a developer and then register an OAuth client after logging in. Dynamic Client Registration would allow a client to register itself without the link to the developer’s account. That would mean there is no paper trail for who the client was developed by. The Dynamic Client Registration endpoint can’t require authentication by definition, so is a public endpoint that can create clients, which as you can imagine opens up some potential security issues.” If you’ve got interested, the whole article can be found here.
Bypass DCR
So, how can we bypass DCR? MCP Docs suggests that we can hardcode that information on the client end. We can do something better.
We can dynamically load details of our Cognito client and serve them to the client while removing the secret on the fly. That way, we won’t be forced to hardcode that client ID here and there while still “having support” (fake one) for the clients requiring DCR.
HTTP/1.1 201 Created
Connection: close
Access-Control-Allow-Origin: *
Content-Type: application/json{
"client_id": "69eahpaqpmtbgljh6bufp42f0",
"client_id_issued_at": 1756130873,
"client_name": "MCP Inspector",
"client_secret_expires_at": 0,
"client_uri": "https://github.com/modelcontextprotocol/inspector",
"grant_types": [
"authorization_code",
"refresh_token"
],
"redirect_uris": [
"http://localhost:6274/oauth/callback/debug"
],
"response_types": [
"code"
],
"scope": "openid profile email",
"token_endpoint_auth_method": "none"
}Now, after returning that response under /register our flow works up to this point!
Another step green! Great!
Token authorization
In the previous step, we have overridden the authorization endpoint under token_endpoint and now we are going to implement it.
We are going to “intercept” the request to the Cognito token endpoint and extend its payload with client_secret and send it to the actual Cognito to exchange it for tokens.
Request we receive:
POST /oauth/token HTTP/1.1grant_type=authorization_code&code=<some_code>&code_verifier=<some_code_verifier>&redirect_uri=http%3A%2F%2Flocalhost%3A6274%2Foauth%2Fcallback%2Fdebug&client_id=69eahpaqpmtbgljh6bufp42f0&resource=http%3A%2F%2Flocalhost%3A8080%2FmcpAfter calling the Cognito token with the same form values, extended with client_secret We are returning a response like the one below:
HTTP/1.1 200 OK
Connection: close
Access-Control-Allow-Origin: *
Content-Type: application/json{
"access_token": "...",
"expires_in": 3600,
"id_token": "...",
"refresh_token": "...",
"token_type": "Bearer"
}Hooray! Full flow is green!
Finally, our flow looks like this:
First full MCP Inspector connection
So now we can click connect and see access to the tools we have exposed in our MCP server.
Fantastic! Now let’s test the connection with VSCode. Let’s use the configuration below:
{
"servers": {
"auth-mcp": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
},
"inputs": []
}Our invocation flow will look like the diagram below:
Thanks to that, finally, we can see the protected tool working fine. Thanks to the Cognito context and groups, we can filter permissions and visibility of the tools.
Of course, this flow will also work for other types of clients, such as LibreChat with Ollama.
Summary… and words of warning
There are many quirks in the clients when it comes to using the context path. If you would like to expose your MCP server under a load balancer or common gateway, you may encounter unusual behaviors and inconsistencies. Because of that, I think I will go with a dedicated auth service for MCPs in the way that PayPal did. Sometimes those flows work a bit differently across VS Code, MCP Inspector, and LibreChat, and I believe that the words MUST and SHOULD in the specs make no difference in many situations. Regarding those different path scans, you can take a look at them as well in the specs:
It might be tricky sometimes… but hey! Now you know how it works and how to deal with it :) Good luck!
Materials:
There was not much space for implementation details that you can find here. Code is written in Go as a draft: https://github.com/SodaDev/mcp-auth
