AWS Cognito
AWS Cognito is Amazon's identity service, providing user pools for authentication and identity pools for AWS resource access. This chapter covers Cognito integration for MCP servers.
Note: Cognito is shown here as an example. If your organization already uses a different identity provider (Okta, Auth0, Entra ID, etc.), use that instead. The patterns in this chapter apply to any OIDC-compliant provider.
The Easy Way: cargo pmcp + CDK
The fastest path to production: Use cargo pmcp to configure OAuth, then let the generated CDK handle Cognito setup. You don't need to manually create user pools, configure clients, or set up resource servers—the CDK does it all.
Step 1: Initialize OAuth Configuration
# Initialize deployment with Cognito OAuth
cargo pmcp deploy init --target pmcp-run --oauth cognito
# This creates/updates .pmcp/deploy.toml with:
# .pmcp/deploy.toml
[auth]
enabled = true
provider = "cognito"
callback_urls = [
"http://localhost:3000/callback", # For development
]
[auth.dcr]
# Dynamic Client Registration for MCP clients
enabled = true
public_client_patterns = [
"claude",
"cursor",
"chatgpt",
"mcp-inspector",
]
default_scopes = [
"openid",
"email",
"mcp/read",
]
allowed_scopes = [
"openid",
"email",
"profile",
"mcp/read",
"mcp/write",
"mcp/admin",
]
Step 2: Deploy with CDK
The deployment generates a CDK stack that creates all Cognito resources:
# Build and deploy
cargo pmcp deploy
# The CDK stack creates:
# - Cognito User Pool with password policies
# - App client with OAuth flows configured
# - Resource server with MCP scopes
# - Optional: Federation with corporate IdP
What the CDK Creates
The generated CDK stack (in deploy/lib/) handles all the complexity:
// Example: What cargo pmcp deploy generates in CDK
// You don't write this - it's generated from deploy.toml
// User Pool with enterprise settings
const userPool = new cognito.UserPool(this, 'McpUserPool', {
userPoolName: `${serverId}-users`,
selfSignUpEnabled: false, // Admin-only provisioning
passwordPolicy: {
minLength: 12,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
},
mfa: cognito.Mfa.OPTIONAL,
});
// Resource server with MCP scopes
const resourceServer = userPool.addResourceServer('McpApi', {
identifier: 'mcp',
scopes: [
{ scopeName: 'read', scopeDescription: 'Read MCP resources' },
{ scopeName: 'write', scopeDescription: 'Modify MCP resources' },
{ scopeName: 'admin', scopeDescription: 'Admin operations' },
],
});
// App client with OAuth configuration
const appClient = userPool.addClient('McpClient', {
generateSecret: true,
oAuth: {
flows: { authorizationCodeGrant: true },
scopes: [/* from deploy.toml */],
callbackUrls: [/* from deploy.toml */],
},
});
The key insight: You configure OAuth in deploy.toml, and the deployment tooling generates the correct CDK/CloudFormation. You don't need to understand Cognito's complex configuration options.
Step 3: Your Server Code
Your Rust code just uses the OAuth middleware—it doesn't know or care that Cognito is the provider:
use pmcp::prelude::*; #[tokio::main] async fn main() -> Result<()> { // OAuth configuration is loaded from environment // (set by CDK stack outputs) let server = ServerBuilder::new("my-server", "1.0.0") .with_oauth_from_env() // Reads COGNITO_* env vars .with_tool(MyTool) .build()?; server.serve().await }
Manual Setup (When You Need Control)
If you need more control, or your organization has specific Cognito requirements, you can configure Cognito manually. The rest of this chapter covers manual setup.
Cognito Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Cognito for MCP Servers │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ User Pool │ Authentication │
│ │ ───────────── │ • User sign-up/sign-in │
│ │ • Users │ • Password policies │
│ │ • Groups │ • MFA │
│ │ • App clients │ • Custom attributes │
│ └────────┬────────┘ • Federation (SAML/OIDC) │
│ │ │
│ │ Issues JWT │
│ ▼ │
│ ┌─────────────────┐ │
│ │ MCP Server │ Validates JWT, extracts user info │
│ └─────────────────┘ │
│ │
│ (Optional for AWS access) │
│ ┌─────────────────┐ │
│ │ Identity Pool │ AWS credentials for resources │
│ └─────────────────┘ • S3, DynamoDB, etc. │
│ │
└─────────────────────────────────────────────────────────────────────┘
Creating a User Pool
AWS Console
- Go to Cognito → User Pools → Create user pool
- Configure sign-in:
- Email as username (recommended)
- Enable MFA (optional but recommended)
- Configure sign-up:
- Self-registration or admin-only
- Required attributes (email)
- Configure app client:
- Create app client for your MCP server
- Enable ALLOW_USER_SRP_AUTH
- Generate client secret (for server-side apps)
AWS CLI / CloudFormation
# cloudformation/cognito.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Cognito User Pool for MCP Server
Resources:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: mcp-server-users
UsernameAttributes:
- email
AutoVerifiedAttributes:
- email
MfaConfiguration: OPTIONAL
Policies:
PasswordPolicy:
MinimumLength: 12
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
Schema:
- Name: email
Required: true
Mutable: true
- Name: department
AttributeDataType: String
Mutable: true
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: mcp-server-client
GenerateSecret: true
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
- ALLOW_USER_PASSWORD_AUTH # For testing only
SupportedIdentityProviders:
- COGNITO
AllowedOAuthFlows:
- code
AllowedOAuthScopes:
- openid
- email
- profile
- mcp-server/read:tools
- mcp-server/execute:tools
AllowedOAuthFlowsUserPoolClient: true
CallbackURLs:
- https://your-app.com/callback
- http://localhost:3000/callback
ResourceServer:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
UserPoolId: !Ref UserPool
Identifier: mcp-server
Name: MCP Server API
Scopes:
- ScopeName: read:tools
ScopeDescription: Read MCP tools
- ScopeName: execute:tools
ScopeDescription: Execute MCP tools
- ScopeName: admin
ScopeDescription: Admin operations
Outputs:
UserPoolId:
Value: !Ref UserPool
UserPoolClientId:
Value: !Ref UserPoolClient
UserPoolDomain:
Value: !Sub "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}"
Rust Integration
Configuration
#![allow(unused)] fn main() { use std::env; #[derive(Debug, Clone)] pub struct CognitoConfig { pub region: String, pub user_pool_id: String, pub client_id: String, pub client_secret: Option<String>, } impl CognitoConfig { pub fn from_env() -> Result<Self, ConfigError> { Ok(Self { region: env::var("AWS_REGION") .unwrap_or_else(|_| "us-east-1".to_string()), user_pool_id: env::var("COGNITO_USER_POOL_ID")?, client_id: env::var("COGNITO_CLIENT_ID")?, client_secret: env::var("COGNITO_CLIENT_SECRET").ok(), }) } pub fn issuer(&self) -> String { format!( "https://cognito-idp.{}.amazonaws.com/{}", self.region, self.user_pool_id ) } pub fn jwks_uri(&self) -> String { format!("{}/.well-known/jwks.json", self.issuer()) } } }
Validator Setup
#![allow(unused)] fn main() { impl JwtValidatorConfig { pub fn from_cognito(config: &CognitoConfig) -> Self { Self { issuer: config.issuer(), audience: config.client_id.clone(), jwks_uri: config.jwks_uri(), algorithms: vec![Algorithm::RS256], leeway_seconds: 60, } } } // Main setup pub async fn setup_cognito_auth() -> Result<JwtValidator> { let config = CognitoConfig::from_env()?; let validator_config = JwtValidatorConfig::from_cognito(&config); Ok(JwtValidator::new(validator_config)) } }
Cognito-Specific Claims
#![allow(unused)] fn main() { #[derive(Debug, Deserialize)] pub struct CognitoClaims { // Standard claims pub sub: String, pub iss: String, pub aud: String, pub exp: u64, pub iat: u64, // Cognito-specific pub token_use: String, // "access" or "id" pub auth_time: Option<u64>, pub client_id: Option<String>, // User attributes (from ID token) pub email: Option<String>, pub email_verified: Option<bool>, // Groups (custom claim) #[serde(rename = "cognito:groups")] pub groups: Option<Vec<String>>, #[serde(rename = "cognito:username")] pub username: Option<String>, // Custom attributes (prefixed with "custom:") #[serde(flatten)] pub custom_attributes: HashMap<String, serde_json::Value>, } impl CognitoClaims { pub fn get_custom(&self, name: &str) -> Option<&serde_json::Value> { self.custom_attributes.get(&format!("custom:{}", name)) } pub fn is_access_token(&self) -> bool { self.token_use == "access" } pub fn is_id_token(&self) -> bool { self.token_use == "id" } } }
Groups and Permissions
Creating Groups
# Create groups in Cognito
aws cognito-idp create-group \
--user-pool-id us-east-1_xxxx \
--group-name Admins \
--description "Administrator access"
aws cognito-idp create-group \
--user-pool-id us-east-1_xxxx \
--group-name Developers \
--description "Developer access"
# Add user to group
aws cognito-idp admin-add-user-to-group \
--user-pool-id us-east-1_xxxx \
--username user@example.com \
--group-name Developers
Group-Based Authorization
#![allow(unused)] fn main() { impl AuthContext { pub fn from_cognito_claims(claims: &CognitoClaims) -> Self { let groups = claims.groups.clone().unwrap_or_default(); // Map groups to scopes let mut scopes: HashSet<String> = HashSet::new(); for group in &groups { match group.as_str() { "Admins" => { scopes.insert("admin:*".into()); scopes.insert("execute:tools".into()); scopes.insert("read:tools".into()); } "Developers" => { scopes.insert("execute:tools".into()); scopes.insert("read:tools".into()); } "ReadOnly" => { scopes.insert("read:tools".into()); } _ => {} } } Self { user_id: claims.sub.clone(), email: claims.email.clone(), name: claims.username.clone(), scopes, } } } }
Federation with Corporate IdP
SAML Federation
# CloudFormation for SAML IdP
SAMLIdentityProvider:
Type: AWS::Cognito::UserPoolIdentityProvider
Properties:
UserPoolId: !Ref UserPool
ProviderName: CorporateSSO
ProviderType: SAML
ProviderDetails:
MetadataURL: https://idp.company.com/metadata.xml
AttributeMapping:
email: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
given_name: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
family_name: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname
custom:department: Department
OIDC Federation
OIDCIdentityProvider:
Type: AWS::Cognito::UserPoolIdentityProvider
Properties:
UserPoolId: !Ref UserPool
ProviderName: Okta
ProviderType: OIDC
ProviderDetails:
client_id: okta-client-id
client_secret: okta-client-secret
authorize_scopes: openid email profile
oidc_issuer: https://company.okta.com
AttributeMapping:
email: email
given_name: given_name
family_name: family_name
Lambda Triggers
Customize authentication with Lambda triggers:
#![allow(unused)] fn main() { // Pre-token generation trigger - customize JWT claims use aws_lambda_events::event::cognito::CognitoEventUserPoolsPreTokenGen; use lambda_runtime::{service_fn, Error, LambdaEvent}; async fn pre_token_gen( event: LambdaEvent<CognitoEventUserPoolsPreTokenGen>, ) -> Result<CognitoEventUserPoolsPreTokenGen, Error> { let mut response = event.payload; // Add custom claims based on user attributes let user_attributes = &response.request.user_attributes; if let Some(department) = user_attributes.get("custom:department") { // Add department to claims response.response.claims_override_details .get_or_insert_default() .claims_to_add_or_override .get_or_insert_default() .insert("department".into(), department.clone()); } // Add permissions based on groups if let Some(groups) = user_attributes.get("cognito:groups") { let permissions = groups_to_permissions(groups); response.response.claims_override_details .get_or_insert_default() .claims_to_add_or_override .get_or_insert_default() .insert("permissions".into(), permissions.join(" ")); } Ok(response) } }
Testing with Cognito
Get Test Token
# Create test user
aws cognito-idp admin-create-user \
--user-pool-id us-east-1_xxxx \
--username testuser@example.com \
--user-attributes Name=email,Value=testuser@example.com \
--temporary-password TempPass123!
# Set permanent password
aws cognito-idp admin-set-user-password \
--user-pool-id us-east-1_xxxx \
--username testuser@example.com \
--password SecurePass123! \
--permanent
# Get tokens
aws cognito-idp admin-initiate-auth \
--user-pool-id us-east-1_xxxx \
--client-id your-client-id \
--auth-flow ADMIN_USER_PASSWORD_AUTH \
--auth-parameters USERNAME=testuser@example.com,PASSWORD=SecurePass123!
Integration Test
#![allow(unused)] fn main() { #[tokio::test] #[ignore] // Run with: cargo test -- --ignored async fn test_cognito_auth() { let config = CognitoConfig::from_env().unwrap(); let validator = JwtValidator::new(JwtValidatorConfig::from_cognito(&config)); // Get token via AWS SDK let token = get_cognito_token(&config).await.unwrap(); let claims = validator.validate(&token).await.unwrap(); assert!(!claims.sub.is_empty()); println!("User ID: {}", claims.sub); println!("Email: {:?}", claims.email); } async fn get_cognito_token(config: &CognitoConfig) -> Result<String> { let client = aws_sdk_cognitoidentityprovider::Client::new(&aws_config::load_from_env().await); let response = client .admin_initiate_auth() .user_pool_id(&config.user_pool_id) .client_id(&config.client_id) .auth_flow(AuthFlowType::AdminUserPasswordAuth) .auth_parameters("USERNAME", "testuser@example.com") .auth_parameters("PASSWORD", "SecurePass123!") .send() .await?; Ok(response.authentication_result() .unwrap() .access_token() .unwrap() .to_string()) } }
Summary
Recommended approach: Use cargo pmcp deploy init --oauth cognito to generate the CDK stack that handles all Cognito complexity. You configure scopes in deploy.toml, and the deployment creates the user pool, app client, and resource server automatically.
If you need manual setup, AWS Cognito integration requires:
- User Pool - Authentication and user management
- App Client - OAuth configuration
- Resource Server - Custom scopes
- Groups - Permission management
- Federation - Corporate IdP integration (optional)
Key Cognito-specific considerations:
- Token types: Access vs ID tokens
- Groups appear in
cognito:groupsclaim - Custom attributes prefixed with
custom: - Lambda triggers for claim customization
Remember: Cognito is just one option. If your organization uses Okta, Auth0, Entra ID, or another provider, use that instead—the patterns are the same.
Continue to Auth0 →