This tutorial walks through a simplified pattern for deploying an MCP Server on AWS, enforcing
SSO authentication
and per-user role assumption. It assumes you already run other
services in your VPC with SSO and IAM roles.

0) Prerequisites
- An OIDC-compliant IdP/SSO that
issues tokens (e.g., Amazon Cognito,
Okta, Auth0). - A DB mapping from
(user_sub [, tenant/org]) → IAM role + tags/limits. - Familiarity with ECS Fargate,
API Gateway, and
IAM.
1) Front door: API Gateway HTTP API
Purpose: require tokens on every call; keep MCP server “auth-dumb”.
- Create HTTP API (not REST API).
- Route:
ANY /{proxy+}→ VPC Link
(to NLB). - Add a Lambda Authorizer:
- Identity source:
Authorizationheader (Bearer). - Cache disabled initially (0s).
- Identity source:
Return 401 with WWW-Authenticate when token missing/invalid.
2) Lambda Authorizer
Purpose: validate tokens and decide user role.
Steps: 1. Validate OIDC token (iss/aud/exp/sig).\
- Extract claims:
sub,email,org/tenant, optional
OIDC-A claims.\ - DB lookup:
sub → { roleArn, tags[], limits }.\ - Return Allow and inject headers upstream.
// Node/TS sketch export async function handler(event) { const token = parseBearer(event.headers.authorization); const claims = await verifyJwt(token, { iss, jwks, aud }); const { roleArn, tags } = await dbLookup(claims.sub); return { isAuthorized: true, context: { "x-decided-role": roleArn, "x-source-identity": claims.sub, "x-session-tags": JSON.stringify(tags) } }; }
3) Private networking to MCP
- Deploy MCP server behind
NLB
in private subnets. - Configure API Gateway VPC Link → NLB.
- Forward headers to MCP.
4) IAM wiring
4.1 ECS Task Role
Minimal permissions, only sts:AssumeRole.{ "Effect":"Allow", "Action":["sts:AssumeRole","sts:TagSession"], "Resource":["arn:aws:iam::<acct>:role/quilt-mcp-target-*"] }
4.2 Target Role Trust Policy
Restrict to ECS task role; enforce SourceIdentity and tag patterns.{ "Effect":"Allow", "Principal":{"AWS":"arn:aws:iam::<acct>:role/<ecsTaskRole>"}, "Action":["sts:AssumeRole","sts:TagSession"], "Condition":{ "StringLike":{"sts:SourceIdentity":"usr-*"}, "ForAllValues:StringEquals":{"sts:TagKeys":["tenant"]} } }
4.3 Resource Policy Example (S3 ABAC)
{ "Effect":"Allow", "Principal":{"AWS":"*"}, "Action":["s3:GetObject","s3:PutObject"], "Resource":"arn:aws:s3:::quilt-tenant-${aws:PrincipalTag/tenant}/*", "Condition":{"StringEquals":{"aws:PrincipalTag/tenant":"${aws:PrincipalTag/tenant}"}} }
5) MCP Server code
Read headers → Assume role per request.import boto3, json def creds_from_headers(headers): role_arn = headers["x-decided-role"] source_id = headers["x-source-identity"] tags = json.loads(headers["x-session-tags"]) sts = boto3.client("sts") resp = sts.assume_role( RoleArn=role_arn, RoleSessionName=source_id[:32], SourceIdentity=source_id, Tags=tags, DurationSeconds=1800 ) return resp["Credentials"]
6) Token acquisition (“force SSO”)
- Agents/clients must present a Bearer token
(aud = api://quilt-mcp). - Supported flows:
- Device Authorization Grant (generic MCP
clients). - Auth Code + PKCE (first-party desktop apps).
- Device Authorization Grant (generic MCP
401 response example:WWW-Authenticate: Bearer realm="quilt-mcp", authorization_uri="https://login.example.com/device", token_service="https://login.example.com/token"
7) Observability
- CloudTrail:
all calls showsourceIdentity=<user_sub>. - Logs:
(sub, tenant, roleArn, tags, decision, requestId). - Metrics: deny spikes, STS errors.
8) Hardening options
- DPoP
proofs to prevent token replay. - mTLS at custom domains for high-trust tenants.
- Verified Permissions (Cedar) for
centralized allow/deny.
Checklist
- [ ] API Gateway + VPC Link → private NLB/ALB\
- [ ] Lambda Authorizer: JWT verify → DB map → inject headers\
- [ ] ECS task role with only
sts:AssumeRole\ - [ ] Target role trust: enforce
SourceIdentity+ tags\ - [ ] ABAC resource policies (e.g., S3)\
- [ ] MCP server: AssumeRole per request\
- [ ] 401 with device flow bootstrap\
- [ ] Logging, metrics, alarms

Leave a comment