Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Stack, Token } from 'aws-cdk-lib';
import { Names, Stack, Token } from 'aws-cdk-lib';
import * as bedrockagentcore from 'aws-cdk-lib/aws-bedrockagentcore';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as iam from 'aws-cdk-lib/aws-iam';
Expand Down Expand Up @@ -389,6 +389,16 @@ export class Gateway extends GatewayBase {
*/
public userPoolClient?: cognito.IUserPoolClient;

/**
* The Cognito User Pool Domain created for the gateway (if using default Cognito authorizer)
*/
public userPoolDomain?: cognito.UserPoolDomain;

/**
* The Cognito Resource Server created for the gateway (if using default Cognito authorizer)
*/
public resourceServer?: cognito.UserPoolResourceServer;

constructor(scope: Construct, id: string, props: GatewayProps) {
super(scope, id);
// Enhanced CDK Analytics Telemetry
Expand Down Expand Up @@ -669,19 +679,67 @@ export class Gateway extends GatewayBase {

/**
* Creates a default Cognito authorizer for the gateway
* Provisions a Cognito User Pool and configures JWT authentication
* Provisions a Cognito User Pool and configures M2M (machine-to-machine) JWT authentication
* using OAuth 2.0 client credentials grant flow
* @see https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/identity-idp-cognito.html
* @internal
*/
private createDefaultCognitoAuthorizerConfig(): IGatewayAuthorizerConfig {
const userPool = new cognito.UserPool(this, 'UserPool', {
userPoolName: `${this.name}-gw-userpool`,
signInCaseSensitive: false,
});

const resourceServer = userPool.addResourceServer('ResourceServer', {
identifier: Names.uniqueResourceName(this, { maxLength: 256, separator: '-' }),
scopes: [
{
scopeName: 'read',
scopeDescription: 'Read access to gateway tools',
},
{
scopeName: 'write',
scopeDescription: 'Write access to gateway tools',
},
],
});

const userPoolClient = userPool.addClient('DefaultClient', {
userPoolClientName: `${this.name}-gw-client`,
generateSecret: true,
oAuth: {
flows: {
clientCredentials: true,
},
scopes: [
cognito.OAuthScope.resourceServer(resourceServer, {
scopeName: 'read',
scopeDescription: 'Read access to gateway tools',
}),
cognito.OAuthScope.resourceServer(resourceServer, {
scopeName: 'write',
scopeDescription: 'Write access to gateway tools',
}),
],
},
});

// Create Cognito Domain for OAuth2 token endpoint
// Use uniqueResourceName to generate a unique domain prefix toLowerCase() is required because the hash portion is uppercase
const domainPrefix = Names.uniqueResourceName(this, {
maxLength: 63, // Cognito domain prefix max length
separator: '-',
}).toLowerCase();

const userPoolDomain = userPool.addDomain('Domain', {
cognitoDomain: {
domainPrefix: domainPrefix,
},
});

this.userPool = userPool;
this.userPoolClient = userPoolClient;
this.userPoolDomain = userPoolDomain;
this.resourceServer = resourceServer;

return GatewayAuthorizer.usingCognito({
userPool: userPool,
allowedClients: [userPoolClient],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Minimal test application for Gateway integration
FROM public.ecr.aws/docker/library/python:3.12-slim

WORKDIR /app

# Copy application code
COPY app.py .

# Expose port for AgentCore Runtime
EXPOSE 8080

# Run the application
CMD ["python", "app.py"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import json
import os
import base64
import urllib.request
from http.server import HTTPServer, BaseHTTPRequestHandler


def get_token():
"""Get M2M access token from Cognito"""
credentials = f"{os.environ['CLIENT_ID']}:{os.environ['CLIENT_SECRET']}"
auth = base64.b64encode(credentials.encode()).decode()

req = urllib.request.Request(
os.environ['TOKEN_ENDPOINT'],
data=f"grant_type=client_credentials&scope={os.environ['SCOPE']}".encode(),
headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': f'Basic {auth}'
}
)

with urllib.request.urlopen(req) as response:
return json.loads(response.read())['access_token']


def call_tool(url, token):
"""Call Gateway tool via MCP"""
if not url.endswith('/mcp'):
url = f"{url.rstrip('/')}/mcp"

req = urllib.request.Request(
url,
data=json.dumps({
'jsonrpc': '2.0',
'method': 'tools/call',
'params': {'name': 'test-tools___hello', 'arguments': {}},
'id': 1
}).encode(),
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {token}'
}
)

with urllib.request.urlopen(req) as response:
return json.loads(response.read())['result']


class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/ping':
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'healthy'}).encode())

def do_POST(self):
if self.path == '/invocations':
try:
token = get_token()
result = call_tool(os.environ['GATEWAY_URL'], token)

self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({
'output': {
'message': 'Gateway M2M authentication successful',
'result': result
}
}).encode())
except Exception as e:
self.send_response(500)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'error': str(e)}).encode())

def log_message(self, format, *args):
pass


if __name__ == '__main__':
HTTPServer(('', 8080), Handler).serve_forever()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# No external dependencies required for minimal test application
Original file line number Diff line number Diff line change
Expand Up @@ -2307,3 +2307,39 @@ describe('MCP Server Target Configuration Tests', () => {
expect(target.credentialProviderConfigurations).toHaveLength(1);
});
});

describe('Gateway M2M Authentication Tests', () => {
let stack: cdk.Stack;

beforeEach(() => {
const app = new cdk.App();
stack = new cdk.Stack(app, 'TestStack', {
env: { account: '123456789012', region: 'us-east-1' },
});
});

test('Should create default Cognito authorizer with M2M support', () => {
new Gateway(stack, 'TestGateway', {
gatewayName: 'test-gateway',
});

const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::Cognito::UserPoolClient', {
AllowedOAuthFlows: ['client_credentials'],
AllowedOAuthFlowsUserPoolClient: true,
GenerateSecret: true,
});

// Resource Server identifier is auto-generated using Names.uniqueResourceName()
template.hasResourceProperties('AWS::Cognito::UserPoolResourceServer', {
Scopes: [
{ ScopeName: 'read', ScopeDescription: 'Read access to gateway tools' },
{ ScopeName: 'write', ScopeDescription: 'Write access to gateway tools' },
],
});

// Domain name is auto-generated using Names.uniqueResourceName()
template.resourceCountIs('AWS::Cognito::UserPoolDomain', 1);
});
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading