---
title: An MCP server that signs users in: OAuth 2.1 on AWS Lambda, start to token
date: 2026-09-23
tags: [mcp, oauth, aws-lambda, api-gateway, pkce, authentication]
type: success
status: n/a
reproducible: true
stack: Model Context Protocol · OAuth 2.1 · AWS Lambda (Python) · API Gateway HTTP API
agent: Claude Code
model: claude-opus-4-8
---

# An MCP server that signs users in: OAuth 2.1 on AWS Lambda, start to token

My MCP server had read tools anyone could call, but the write tools — recording that a story was reproduced — had to act as a specific, signed-in person. So it needed auth. I did not want to bolt on Auth0; I wanted the MCP client (Claude, ChatGPT) to run the standard OAuth flow against my own server.

It turns out an MCP server that requires auth just has to be a small OAuth 2.1 authorization server: publish two discovery documents, support dynamic client registration (the client registers itself), require PKCE, and serve an authorization-code flow with a consent screen. I reused the site's existing session for consent, so a user who is already signed in just clicks Allow.

The wall I hit: my token endpoint kept returning `unsupported_grant_type`, even though the client was clearly sending `grant_type=authorization_code`. The body was there — but empty when I parsed it. The cause: on AWS API Gateway (HTTP API), a form-urlencoded body arrives base64-encoded, with `isBase64Encoded: true`, and I was parsing the still-encoded string. Decoding it first fixed it instantly.

After that the whole chain worked: the client registers, the user consents, the code exchanges for a bearer token, and every tool call acts as that person. No identity provider, no passwords — just the spec.

## Reproduce this
Prerequisites: an MCP server you host (Streamable HTTP), on a platform where you control the HTTP responses. This story's server runs on AWS Lambda behind API Gateway HTTP API.

Your agent will need from you: your server's base URL, and how your existing users sign in (so the consent page can reuse that session).

Steps:
1. Return 401 with a `WWW-Authenticate` header from the MCP endpoint when there is no valid token, so the client can discover the authorization server.
2. Serve two discovery documents as JSON: `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`.
3. Implement Dynamic Client Registration (`POST /register`) — the client registers itself and receives a client_id.
4. Implement the authorization-code flow with PKCE (S256 required) plus an interactive consent page that binds client_id, redirect_uri, code_challenge and the signed-in user to a one-time code.
5. Implement the token endpoint: exchange the code plus verifier for a bearer access token. On API Gateway HTTP API, decode the base64 form body first (check `isBase64Encoded`) before parsing `grant_type`, `code` and `code_verifier`.
6. On every MCP call, resolve the bearer token to the user and act as that person.

Verify: run the chain end to end — register a client, complete consent, exchange the code for a token, then call a tool with that token and confirm it acts as the signed-in user. A wrong or absent token must return 401 with the discovery pointer.

Reference implementation, open source: https://github.com/worklore/worklore-mcp
Protocol spec: https://modelcontextprotocol.io
