Skip to content

Commit de51f6c

Browse files
committed
feat: Cognito user pool with OAuth for HTTP access to MCP servers
1 parent 8819322 commit de51f6c

File tree

12 files changed

+1319
-0
lines changed

12 files changed

+1319
-0
lines changed

.github/dependabot.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,16 @@ updates:
9090
prefix: "chore(deps)"
9191
rebase-strategy: "disabled"
9292

93+
- package-ecosystem: npm
94+
directory: "/examples/servers/auth"
95+
schedule:
96+
interval: monthly
97+
open-pull-requests-limit: 10
98+
versioning-strategy: increase
99+
commit-message:
100+
prefix: "chore(deps)"
101+
rebase-strategy: "disabled"
102+
93103
- package-ecosystem: npm
94104
directory: "/examples/servers/weather-alerts"
95105
schedule:

.github/workflows/cdk-checks.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,39 @@ jobs:
7676
env:
7777
CDK_DEFAULT_ACCOUNT: "000000000000"
7878

79+
check_auth_stack:
80+
name: Check Typescript-based Cognito stack
81+
runs-on: ubuntu-latest
82+
permissions:
83+
contents: read
84+
timeout-minutes: 15
85+
steps:
86+
- uses: actions/checkout@v4
87+
88+
- name: "Set up Typescript"
89+
uses: actions/setup-node@v4
90+
with:
91+
node-version: 20
92+
93+
- name: Install CDK CLI
94+
run: npm install -g aws-cdk
95+
96+
- name: Install dependencies
97+
run: |
98+
npm ci
99+
npm audit --audit-level critical
100+
working-directory: ./examples/servers/auth
101+
102+
- name: Build
103+
run: npm run build
104+
working-directory: ./examples/servers/auth
105+
106+
- name: Synthesize CDK stack
107+
run: cdk synth --app 'node lib/mcp-auth.js'
108+
working-directory: ./examples/servers/auth
109+
env:
110+
CDK_DEFAULT_ACCOUNT: "000000000000"
111+
79112
check_weather_alerts_server:
80113
name: Check Typescript-based Weather Alerts Server
81114
runs-on: ubuntu-latest

.versionrc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
"filename": "e2e_tests/typescript/package-lock.json",
2727
"type": "json"
2828
},
29+
{
30+
"filename": "examples/servers/auth/package.json",
31+
"type": "json"
32+
},
33+
{
34+
"filename": "examples/servers/auth/package-lock.json",
35+
"type": "json"
36+
},
2937
{
3038
"filename": "examples/servers/weather-alerts/package.json",
3139
"type": "json"

DEVELOP.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ aws iam attach-role-policy \
3535
cdk bootstrap aws://<aws account id>/us-east-2
3636
```
3737

38+
Deploy the Cognito user pool for OAuth authentication via the MCP streamable HTTP transport.
39+
40+
```bash
41+
cd examples/servers/auth
42+
43+
npm install
44+
45+
npm run build
46+
47+
cdk deploy --app 'node lib/mcp-auth.js'
48+
49+
./sync-cognito-user-password.sh
50+
```
51+
52+
Test the OAuth configuration with [oauth2c](https://github.com/cloudentity/oauth2c):
53+
54+
```bash
55+
./test-interactive-oauth.sh
56+
57+
./test-automated-oauth.sh
58+
```
59+
3860
### Build the Python module
3961

4062
Install the run-mcp-servers-with-aws-lambda Python module from source:

examples/servers/auth/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# MCP Auth Stack
2+
3+
This CDK stack creates AWS Cognito resources for OAuth authentication/authorization to be used with the example MCP (Model Context Protocol) servers.

examples/servers/auth/lib/mcp-auth.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import { Construct } from "constructs";
3+
import {
4+
UserPool,
5+
OAuthScope,
6+
ResourceServerScope,
7+
UserPoolResourceServer,
8+
CfnUserPoolUser,
9+
} from "aws-cdk-lib/aws-cognito";
10+
import { Secret } from "aws-cdk-lib/aws-secretsmanager";
11+
import { AwsSolutionsChecks, NagSuppressions } from "cdk-nag";
12+
13+
export class McpAuthStack extends cdk.Stack {
14+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
15+
super(scope, id, props);
16+
17+
// Create Cognito User Pool
18+
const userPool = new UserPool(this, "McpAuthUserPool", {
19+
userPoolName: `mcp-lambda-examples`,
20+
selfSignUpEnabled: false,
21+
signInAliases: {
22+
email: true,
23+
username: true,
24+
},
25+
autoVerify: {
26+
email: true,
27+
},
28+
passwordPolicy: {
29+
minLength: 12,
30+
requireLowercase: true,
31+
requireUppercase: true,
32+
requireDigits: true,
33+
requireSymbols: true,
34+
},
35+
accountRecovery: cdk.aws_cognito.AccountRecovery.NONE,
36+
removalPolicy: cdk.RemovalPolicy.DESTROY,
37+
standardThreatProtectionMode:
38+
cdk.aws_cognito.StandardThreatProtectionMode.FULL_FUNCTION,
39+
featurePlan: cdk.aws_cognito.FeaturePlan.PLUS,
40+
});
41+
42+
// Create a user in the user pool
43+
new CfnUserPoolUser(this, "McpAuthUser", {
44+
userPoolId: userPool.userPoolId,
45+
username: "mcp-user",
46+
userAttributes: [
47+
{
48+
name: "email",
49+
value: "mcp-user@example.com",
50+
},
51+
{
52+
name: "email_verified",
53+
value: "true",
54+
},
55+
],
56+
messageAction: "SUPPRESS",
57+
});
58+
59+
const userCredentialsSecret = new Secret(this, "McpUserPassword", {
60+
secretName: `mcp-lambda-examples-user-creds`,
61+
description: "Credentials for MCP user",
62+
generateSecretString: {
63+
secretStringTemplate: JSON.stringify({ username: "mcp-user" }),
64+
generateStringKey: "password",
65+
excludeCharacters: '"@/\\',
66+
includeSpace: false,
67+
passwordLength: 16,
68+
requireEachIncludedType: true,
69+
},
70+
removalPolicy: cdk.RemovalPolicy.DESTROY,
71+
});
72+
73+
NagSuppressions.addResourceSuppressions(userCredentialsSecret, [
74+
{
75+
id: "AwsSolutions-SMG4",
76+
reason:
77+
"Credentials will not be automatically rotated for this example.",
78+
},
79+
]);
80+
81+
// Create User Pool Domain
82+
userPool.addDomain("McpAuthUserPoolDomain", {
83+
cognitoDomain: {
84+
domainPrefix: `mcp-lambda-examples-${this.account}`,
85+
},
86+
});
87+
88+
// Scope for each MCP server that will use this user pool.
89+
// The scope name must match the URL path for the MCP server
90+
// in the API gateway.
91+
const mcpServers = ["mcpdoc", "cat-facts"];
92+
const resourceServerScopes = mcpServers.map(
93+
(mcpServer) =>
94+
new ResourceServerScope({
95+
scopeName: mcpServer,
96+
scopeDescription: `Scope for ${mcpServer} MCP server`,
97+
})
98+
);
99+
const resourceServer = new UserPoolResourceServer(this, "ResourceServer", {
100+
identifier: "mcp-resource-server",
101+
userPool: userPool,
102+
scopes: resourceServerScopes,
103+
});
104+
const oauthScopes = resourceServerScopes.map((scope) =>
105+
OAuthScope.resourceServer(resourceServer, scope)
106+
);
107+
108+
// OAuth client for interactive chatbots:
109+
// The client will redirect users to the browser for sign-in
110+
const interactiveClient = userPool.addClient("InteractiveClient", {
111+
generateSecret: false,
112+
preventUserExistenceErrors: true,
113+
enableTokenRevocation: true,
114+
accessTokenValidity: cdk.Duration.minutes(60),
115+
refreshTokenValidity: cdk.Duration.days(60),
116+
oAuth: {
117+
flows: {
118+
authorizationCodeGrant: true,
119+
},
120+
scopes: oauthScopes,
121+
callbackUrls: [
122+
"http://localhost:9876/callback", // For local testing with oauth2c
123+
],
124+
},
125+
authFlows: {
126+
userPassword: true,
127+
},
128+
});
129+
130+
// OAuth client for automated integration tests:
131+
// The client will provide a client secret for the access token
132+
const automatedClient = userPool.addClient("AutomatedClient", {
133+
generateSecret: true,
134+
preventUserExistenceErrors: true,
135+
enableTokenRevocation: true,
136+
accessTokenValidity: cdk.Duration.minutes(60),
137+
refreshTokenValidity: cdk.Duration.days(60),
138+
oAuth: {
139+
flows: {
140+
clientCredentials: true,
141+
},
142+
scopes: oauthScopes,
143+
},
144+
authFlows: {},
145+
});
146+
147+
const automatedClientSecret = new Secret(this, "AutomatedClientSecret", {
148+
secretName: `mcp-lambda-examples-oauth-client-secret`,
149+
description: "Client secret for automated MCP client",
150+
secretStringValue: automatedClient.userPoolClientSecret,
151+
removalPolicy: cdk.RemovalPolicy.DESTROY,
152+
});
153+
154+
NagSuppressions.addResourceSuppressions(automatedClientSecret, [
155+
{
156+
id: "AwsSolutions-SMG4",
157+
reason:
158+
"OAuth client secret will not be automatically rotated for this example",
159+
},
160+
]);
161+
162+
// Outputs with export names for cross-stack references
163+
new cdk.CfnOutput(this, "UserPoolId", {
164+
value: userPool.userPoolId,
165+
description: "Cognito User Pool ID",
166+
exportName: "McpAuth-UserPoolId",
167+
});
168+
169+
new cdk.CfnOutput(this, "UserPoolDomain", {
170+
value: userPool.userPoolProviderUrl,
171+
description: "Cognito User Pool Domain URL",
172+
exportName: "McpAuth-UserPoolDomain",
173+
});
174+
175+
new cdk.CfnOutput(this, "AuthorizationUrl", {
176+
value: `${userPool.userPoolProviderUrl}/oauth2/authorize`,
177+
description: "OAuth Authorization URL",
178+
exportName: "McpAuth-AuthorizationUrl",
179+
});
180+
181+
new cdk.CfnOutput(this, "TokenUrl", {
182+
value: `${userPool.userPoolProviderUrl}/oauth2/token`,
183+
description: "OAuth Token URL",
184+
exportName: "McpAuth-TokenUrl",
185+
});
186+
187+
new cdk.CfnOutput(this, "InteractiveOAuthClientId", {
188+
value: interactiveClient.userPoolClientId,
189+
description: "Client ID for interactive OAuth flow",
190+
exportName: "McpAuth-InteractiveClientId",
191+
});
192+
193+
new cdk.CfnOutput(this, "AutomatedOAuthClientId", {
194+
value: automatedClient.userPoolClientId,
195+
description: "Client ID for automated OAuth flow",
196+
exportName: "McpAuth-AutomatedClientId",
197+
});
198+
199+
new cdk.CfnOutput(this, "OAuthClientSecretArn", {
200+
value: automatedClientSecret.secretArn,
201+
description: "ARN of the secret containing the OAuth client secret",
202+
exportName: "McpAuth-ClientSecretArn",
203+
});
204+
205+
new cdk.CfnOutput(this, "UserCredentialsSecretArn", {
206+
value: userCredentialsSecret.secretArn,
207+
description:
208+
"ARN of the secret containing the login credentials for mcp-user",
209+
exportName: "McpAuth-UserCredentialsArn",
210+
});
211+
}
212+
}
213+
214+
const app = new cdk.App();
215+
const stack = new McpAuthStack(app, "LambdaMcpServer-Auth", {
216+
env: { account: process.env["CDK_DEFAULT_ACCOUNT"], region: "us-east-2" },
217+
stackName: "LambdaMcpServer-Auth",
218+
});
219+
220+
// Add CDK NAG suppressions for the entire stack
221+
NagSuppressions.addStackSuppressions(stack, [
222+
{
223+
id: "AwsSolutions-IAM4",
224+
reason:
225+
"AWS managed policies are acceptable for CDK custom resource Lambda functions (created to retrieve OAuth client secret)",
226+
appliesTo: [
227+
"Policy::arn:<AWS::Partition>:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
228+
],
229+
},
230+
]);
231+
232+
cdk.Aspects.of(stack).add(new AwsSolutionsChecks({ verbose: true }));
233+
app.synth();

0 commit comments

Comments
 (0)