Skip to content

How to Authenticate and Authorize Applications with Different Permissions Using Amazon Cognito M2M

16 minute read
Content level: Advanced
0

Implement secure machine-to-machine authentication with differentiated access permissions using Amazon Cognito and API Gateway.

The Challenge

Microservices and automated systems often need different levels of access to the same backend APIs. For example, an Order Processing Service requires full read/write access to customer data, while a Reporting Service needs only read access for analytics. Traditional approaches like shared API keys don't provide the granular permission control needed for these machine-to-machine scenarios, leading to security risks and compliance violations.

Architecture Solution

API Gateway Cognito Authorizer with Scope-Based Authorization

Amazon Cognito M2M authentication using OAuth 2.0 Client Credentials Grant combined with API Gateway Cognito Authorizer provides centralized authorization for machine-to-machine communication. This approach validates JWT tokens and enforces scope-based permissions at the API Gateway layer, simplifying backend implementation.

Example Scenario: Two microservices with different permissions

  • Order Processing Service: Full access (customer-api/admin, customer-api/write, customer-api/read)
  • Reporting Service: Read-only access (customer-api/read)

Architecture Diagram

Solution Architecture

This is how it works: Each microservice authenticates with Cognito using unique client credentials and receives a JWT access token containing specific scopes. API Gateway validates the token signature and enforces permissions based on the scopes, only forwarding authorized requests to the backend.

Backend Lambda Implementation

With API Gateway handling authorization, the backend Lambda is simplified:

import json

def lambda_handler(event, context):
    # API Gateway has already validated JWT and scopes
    # No JWT validation code needed in backend
    
    http_method = event['httpMethod']
    path = event['path']
    
    if http_method == 'GET' and path == '/customers':
        # Both admin and employee can access (both have read scope)
        return {
            'statusCode': 200,
            'body': json.dumps({'customers': get_customer_list()})
        }
    
    elif http_method == 'DELETE' and '/customers/' in path:
        # Only admin can access (API Gateway enforces admin scope)
        customer_id = path.split('/')[-1]
        delete_customer_record(customer_id)
        return {
            'statusCode': 200,
            'body': json.dumps({'message': f'Customer {customer_id} deleted'})
        }
    
    return {
        'statusCode': 404,
        'body': json.dumps({'error': 'Not found'})
    }

def get_customer_list():
    return [{'id': '123', 'name': 'John Doe'}, {'id': '456', 'name': 'Jane Smith'}]

def delete_customer_record(customer_id):
    # Simulate customer deletion
    print(f"Deleting customer {customer_id}")

Deployment Process

Deploy the complete Infrastructure as Code template provided in the Appendix. The CloudFormation template creates:

  • Cognito User Pool with OAuth domain
  • Resource Server with custom scopes (read, write, admin)
  • Two Application Clients with different permission levels
  • API Gateway with Cognito Authorizer
  • Lambda function for backend processing
  • Method-level scope enforcement

Step 1: Save the CloudFormation Template

Copy the complete CloudFormation template from the Appendix section at the end of this article and save it to a new file:

# Create a new file for the template
nano cognito-m2m-demo.yaml

# Or use your preferred text editor
code cognito-m2m-demo.yaml
vim cognito-m2m-demo.yaml

Paste the entire CloudFormation template content from the Appendix into this file and save it.

Step 2: Deploy the Stack

Run the following AWS CLI command to deploy the complete solution:

aws cloudformation create-stack \
  --stack-name cognito-m2m-demo \
  --template-body file://cognito-m2m-demo.yaml \
  --capabilities CAPABILITY_IAM \
  --region us-east-1

The outputs will provide:

  • TokenEndpoint: OAuth2 token endpoint URL
  • OrderClientId: Order Processing Service client ID
  • ReportingClientId: Reporting Service client ID
  • ApiUrl: API Gateway endpoint URL
  • QuickTestCommands: Ready-to-use test commands

Testing and Validation

Test 1: Order Processing Service Gets Full Access Token

# Get stack outputs
TOKEN_ENDPOINT=$(aws cloudformation describe-stacks --stack-name cognito-m2m-demo --query 'Stacks[0].Outputs[?OutputKey==`TokenEndpoint`].OutputValue' --output text)
ORDER_CLIENT_ID=$(aws cloudformation describe-stacks --stack-name cognito-m2m-demo --query 'Stacks[0].Outputs[?OutputKey==`OrderClientId`].OutputValue' --output text)
API_URL=$(aws cloudformation describe-stacks --stack-name cognito-m2m-demo --query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' --output text)

# Get client secret from Cognito console, then request token
CREDENTIALS=$(echo -n "$ORDER_CLIENT_ID:$ORDER_CLIENT_SECRET" | base64)
ORDER_TOKEN=$(curl -s -X POST "$TOKEN_ENDPOINT" \
  -H "Authorization: Basic $CREDENTIALS" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=customer-api/read customer-api/write customer-api/admin" \
  | jq -r '.access_token')

echo "Order Processing Service token obtained: ${ORDER_TOKEN:0:50}..."

Test 2: Reporting Service Gets Read-Only Token

REPORTING_CLIENT_ID=$(aws cloudformation describe-stacks --stack-name cognito-m2m-demo --query 'Stacks[0].Outputs[?OutputKey==`ReportingClientId`].OutputValue' --output text)

CREDENTIALS=$(echo -n "$REPORTING_CLIENT_ID:$REPORTING_CLIENT_SECRET" | base64)
REPORTING_TOKEN=$(curl -s -X POST "$TOKEN_ENDPOINT" \
  -H "Authorization: Basic $CREDENTIALS" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&scope=customer-api/read" \
  | jq -r '.access_token')

echo "Reporting Service token obtained: ${REPORTING_TOKEN:0:50}..."

Test 3: API Gateway Enforces Scope-Based Authorization

# Order Processing Service can delete customers (has admin scope)
curl -X DELETE "$API_URL/customers/123" \
  -H "Authorization: Bearer $ORDER_TOKEN"
# Expected: 200 OK - Customer deleted (API Gateway allows, Lambda processes)

# Reporting Service cannot delete customers (lacks admin scope)
curl -X DELETE "$API_URL/customers/123" \
  -H "Authorization: Bearer $REPORTING_TOKEN"
# Expected: 403 Forbidden (API Gateway blocks, never reaches Lambda)
{
  "message": "Insufficient privileges to access this resource"
}

# Reporting Service can read customer data (has read scope)
curl -X GET "$API_URL/customers" \
  -H "Authorization: Bearer $REPORTING_TOKEN"
# Expected: 200 OK - Customer data returned (API Gateway allows, Lambda processes)
{
  "customers": [
    {"id": "123", "name": "John Doe"},
    {"id": "456", "name": "Jane Smith"}
  ]
}

This demonstrates that API Gateway validates JWT tokens and enforces scope-based permissions for machine-to-machine communication before requests reach the backend Lambda function.

Security Benefits

API Gateway Cognito Authorizer provides:

  • AWS-Managed Security: No custom JWT validation code needed
  • Centralized Authorization: All permission checks at API Gateway layer
  • Cryptographic Protection: RSA-256 signatures prevent token tampering
  • Time-bound Security: 1-hour automatic token expiration
  • Performance Optimization: Built-in authorizer result caching
  • Simplified Backend: Lambda focuses on business logic only

JWT Token Security: Any attempt to modify token scopes breaks the cryptographic signature, immediately detected by API Gateway's built-in validation.


Considerations: When API Gateway API Keys Are Sufficient

API Gateway API Keys are simpler and sufficient when all applications need identical access to the same resources.

Use API Gateway API Keys When:

1. Uniform Data Access

All applications access the same data with identical permissions:

# Backend Lambda - API Gateway already validated API key
def lambda_handler(event, context):
    # No authorization code needed - all clients get same data
    return {
        'statusCode': 200,
        'body': json.dumps(get_all_system_metrics())
    }

2. Rate Limiting and Client Identification

You need to identify and limit different clients but they all get the same access:

# API Gateway Method - API Key required, same backend access
GetMetricsMethod:
  Type: AWS::ApiGateway::Method
  Properties:
    ApiKeyRequired: true  # API Gateway validates key
    # Same Lambda backend for all clients

Use Cognito M2M When:

1. Different Permission Levels

Applications need different levels of access to the same resources:

# Backend Lambda - API Gateway already validated JWT scopes
def lambda_handler(event, context):
    # API Gateway passes different requests based on scopes
    if event['httpMethod'] == 'GET':
        return get_orders()  # Both admin and employee reach here
    elif event['httpMethod'] == 'DELETE':
        return delete_order()  # Only admin reaches here (API Gateway filtered)

Real-World Examples:

API Keys SufficientCognito M2M Required
Multiple monitoring tools accessing same metricsOrder service (full CRUD) vs Analytics (read-only)
Public API with usage tiers (same data, different limits)SaaS platform with tenant-specific permissions
Partner integrations with identical data access permissionFinancial: trading vs reporting vs compliance systems

Quick Assessment:

  • Do different apps need different access levels? → Cognito M2M
  • Just need to identify and rate-limit clients? → API Keys
  • All apps access same data identically? → API Keys

Conclusion

Amazon Cognito M2M authentication with API Gateway Cognito Authorizer solves microservice authorization challenges by providing OAuth 2.0 scopes for fine-grained permissions, AWS-managed JWT token validation at the API Gateway layer, and simplified backend implementation. The solution includes centralized authorization enforcement, detailed audit trails, and meets industry compliance standards.

Key Achievement: Order Processing Service gets full access while Reporting Service gets read-only access, with API Gateway enforcing cryptographic token validation and scope-based authorization before requests reach the backend. The approach transforms complex machine-to-machine authorization requirements into a manageable, secure, and scalable system using AWS-managed services.


Appendix: Complete Infrastructure Code

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Application Authentication with Cognito M2M and API Gateway Authorizer'

Parameters:
  ProjectName:
    Type: String
    Default: 'cognito-m2m-demo'
    Description: 'Project name for resource naming'
    AllowedPattern: '^[a-z0-9-]+$'
    ConstraintDescription: 'Must contain only lowercase letters, numbers, and hyphens'

Resources:
  # Cognito User Pool
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub '${ProjectName}-user-pool'
      UserPoolTags:
        Purpose: M2M-Demo
        Project: !Ref ProjectName

  # User Pool Domain (Fixed with compliant domain name)
  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Sub 'm2m-demo-${AWS::AccountId}'
      UserPoolId: !Ref UserPool

  # Resource Server with Scopes
  ResourceServer:
    Type: AWS::Cognito::UserPoolResourceServer
    Properties:
      UserPoolId: !Ref UserPool
      Identifier: 'customer-api'
      Name: 'Customer API'
      Scopes:
        - ScopeName: 'read'
          ScopeDescription: 'Read access'
        - ScopeName: 'write'
          ScopeDescription: 'Write access'
        - ScopeName: 'admin'
          ScopeDescription: 'Admin access'

  # Order Processing Service Client (Full Permissions)
  OrderClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: ResourceServer
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: 'OrderProcessingService'
      GenerateSecret: true
      AllowedOAuthFlows: [client_credentials]
      AllowedOAuthScopes: ['customer-api/read', 'customer-api/write', 'customer-api/admin']
      AllowedOAuthFlowsUserPoolClient: true

  # Reporting Service Client (Read-Only)
  ReportingClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: ResourceServer
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: 'ReportingService'
      GenerateSecret: true
      AllowedOAuthFlows: [client_credentials]
      AllowedOAuthScopes: ['customer-api/read']
      AllowedOAuthFlowsUserPoolClient: true

  # Lambda Execution Role with enhanced security
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                'aws:SourceAccount': !Ref 'AWS::AccountId'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Tags:
        - Key: Purpose
          Value: M2M-Demo
        - Key: Project
          Value: !Ref ProjectName

  # Backend Lambda Function with updated runtime
  BackendLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${ProjectName}-backend'
      Runtime: python3.12
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 30
      MemorySize: 128
      Description: 'Backend API for Cognito M2M demo'
      Tags:
        - Key: Purpose
          Value: M2M-Demo
        - Key: Project
          Value: !Ref ProjectName
      Code:
        ZipFile: |
          import json
          import logging
          
          # Configure logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          
          def lambda_handler(event, context):
              try:
                  logger.info(f"Received event: {json.dumps(event)}")
                  
                  http_method = event.get('httpMethod', '')
                  path = event.get('path', '')
                  
                  # Handle GET /customers
                  if http_method == 'GET' and path == '/customers':
                      return {
                          'statusCode': 200,
                          'headers': {
                              'Content-Type': 'application/json',
                              'Access-Control-Allow-Origin': '*'
                          },
                          'body': json.dumps({
                              'customers': [
                                  {'id': '123', 'name': 'John Doe', 'email': 'john@example.com'},
                                  {'id': '456', 'name': 'Jane Smith', 'email': 'jane@example.com'}
                              ],
                              'message': 'Successfully retrieved customers'
                          })
                      }
                  
                  # Handle DELETE /customers/{id}
                  elif http_method == 'DELETE' and '/customers/' in path:
                      customer_id = path.split('/')[-1]
                      logger.info(f"Deleting customer: {customer_id}")
                      
                      return {
                          'statusCode': 200,
                          'headers': {
                              'Content-Type': 'application/json',
                              'Access-Control-Allow-Origin': '*'
                          },
                          'body': json.dumps({
                              'message': f'Customer {customer_id} deleted successfully',
                              'customerId': customer_id
                          })
                      }
                  
                  # Handle unsupported methods/paths
                  else:
                      return {
                          'statusCode': 404,
                          'headers': {
                              'Content-Type': 'application/json',
                              'Access-Control-Allow-Origin': '*'
                          },
                          'body': json.dumps({
                              'error': 'Not found',
                              'message': f'Method {http_method} not supported for path {path}'
                          })
                      }
                      
              except Exception as e:
                  logger.error(f"Error processing request: {str(e)}")
                  return {
                      'statusCode': 500,
                      'headers': {
                          'Content-Type': 'application/json',
                          'Access-Control-Allow-Origin': '*'
                      },
                      'body': json.dumps({
                          'error': 'Internal server error',
                          'message': 'An error occurred processing your request'
                      })
                  }

  # API Gateway with enhanced configuration
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub '${ProjectName}-api'
      Description: 'API with Cognito M2M Authorization'
      EndpointConfiguration:
        Types:
          - REGIONAL
      Tags:
        - Key: Purpose
          Value: M2M-Demo
        - Key: Project
          Value: !Ref ProjectName

  # Cognito Authorizer with enhanced configuration
  CognitoAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Name: !Sub '${ProjectName}-cognito-authorizer'
      RestApiId: !Ref ApiGateway
      Type: COGNITO_USER_POOLS
      ProviderARNs:
        - !GetAtt UserPool.Arn
      IdentitySource: method.request.header.Authorization
      AuthorizerResultTtlInSeconds: 300

  # Customers Resource
  CustomersResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: 'customers'

  # Customer ID Resource
  CustomerIdResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !Ref CustomersResource
      PathPart: '{id}'

  # GET /customers Method (Read scope required)
  GetCustomersMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref CustomersResource
      HttpMethod: GET
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref CognitoAuthorizer
      AuthorizationScopes:
        - 'customer-api/read'
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BackendLambda.Arn}/invocations'

  # DELETE /customers/{id} Method (Admin scope required)
  DeleteCustomerMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref CustomerIdResource
      HttpMethod: DELETE
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref CognitoAuthorizer
      AuthorizationScopes:
        - 'customer-api/admin'
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BackendLambda.Arn}/invocations'

  # Lambda Permission for API Gateway
  LambdaApiGatewayPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref BackendLambda
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/*'

  # API Deployment with proper dependencies
  ApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
      - GetCustomersMethod
      - DeleteCustomerMethod
    Properties:
      RestApiId: !Ref ApiGateway
      StageName: 'prod'

Outputs:
  UserPoolId:
    Description: 'Cognito User Pool ID'
    Value: !Ref UserPool
    Export:
      Name: !Sub '${AWS::StackName}-UserPoolId'

  UserPoolArn:
    Description: 'Cognito User Pool ARN'
    Value: !GetAtt UserPool.Arn
    Export:
      Name: !Sub '${AWS::StackName}-UserPoolArn'

  UserPoolDomain:
    Description: 'Cognito User Pool Domain'
    Value: !Ref UserPoolDomain
    Export:
      Name: !Sub '${AWS::StackName}-UserPoolDomain'

  TokenEndpoint:
    Description: 'OAuth2 Token Endpoint for M2M authentication'
    Value: !Sub 'https://m2m-demo-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com/oauth2/token'
    Export:
      Name: !Sub '${AWS::StackName}-TokenEndpoint'

  OrderClientId:
    Description: 'Order Processing Service Client ID (Full permissions: read, write, admin)'
    Value: !Ref OrderClient
    Export:
      Name: !Sub '${AWS::StackName}-OrderClientId'

  ReportingClientId:
    Description: 'Reporting Service Client ID (Read-only permissions)'
    Value: !Ref ReportingClient
    Export:
      Name: !Sub '${AWS::StackName}-ReportingClientId'

  ApiUrl:
    Description: 'API Gateway URL for testing endpoints'
    Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod'
    Export:
      Name: !Sub '${AWS::StackName}-ApiUrl'

  JWKSEndpoint:
    Description: 'JWKS Endpoint for Token Validation'
    Value: !Sub 'https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json'
    Export:
      Name: !Sub '${AWS::StackName}-JWKSEndpoint'

  TestEndpoints:
    Description: 'Test endpoints for validation'
    Value: !Sub |
      GET (read scope): https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers
      DELETE (admin scope): https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers/{id}

  QuickTestCommands:
    Description: 'Ready-to-use test commands (get client secrets first)'
    Value: !Sub |
      # Step 1: Get client secrets
      aws cognito-idp describe-user-pool-client --user-pool-id ${UserPool} --client-id ${OrderClient} --query 'UserPoolClient.ClientSecret' --output text
      aws cognito-idp describe-user-pool-client --user-pool-id ${UserPool} --client-id ${ReportingClient} --query 'UserPoolClient.ClientSecret' --output text
      
      # Step 2: Test Order Service (replace CLIENT_SECRET)
      curl -X POST https://m2m-demo-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com/oauth2/token \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "grant_type=client_credentials&client_id=${OrderClient}&client_secret=CLIENT_SECRET&scope=customer-api/read customer-api/write customer-api/admin"
      
      # Step 3: Test API endpoints (replace ACCESS_TOKEN)
      curl -X GET https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers -H "Authorization: Bearer ACCESS_TOKEN"
      curl -X DELETE https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers/123 -H "Authorization: Bearer ACCESS_TOKEN"

  DeploymentValidation:
    Description: 'Validation checklist for successful deployment'
    Value: !Sub |
      ✅ User Pool Created: ${UserPool}
      ✅ Domain Available: https://m2m-demo-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com
      ✅ API Gateway Deployed: https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod
      ✅ Lambda Function Ready: ${BackendLambda}
      ✅ Two Clients Configured: Order (full access) + Reporting (read-only)
      ✅ Ready for Testing: Use QuickTestCommands output above

  # Resource Server with Scopes
  ResourceServer:
    Type: AWS::Cognito::UserPoolResourceServer
    Properties:
      UserPoolId: !Ref UserPool
      Identifier: 'customer-api'
      Name: 'Customer API'
      Scopes:
        - ScopeName: 'read'
          ScopeDescription: 'Read access'
        - ScopeName: 'write'
          ScopeDescription: 'Write access'
        - ScopeName: 'admin'
          ScopeDescription: 'Admin access'

  # Order Processing Service Client (Full Permissions)
  OrderClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: ResourceServer
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: 'OrderProcessingService'
      GenerateSecret: true
      AllowedOAuthFlows: [client_credentials]
      AllowedOAuthScopes: ['customer-api/read', 'customer-api/write', 'customer-api/admin']
      AllowedOAuthFlowsUserPoolClient: true

  # Reporting Service Client (Read-Only)
  ReportingClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: ResourceServer
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: 'ReportingService'
      GenerateSecret: true
      AllowedOAuthFlows: [client_credentials]
      AllowedOAuthScopes: ['customer-api/read']
      AllowedOAuthFlowsUserPoolClient: true

  # Lambda Execution Role with enhanced security
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ProjectName}-lambda-execution-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
            Condition:
              StringEquals:
                'aws:SourceAccount': !Ref 'AWS::AccountId'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Tags:
        - Key: Purpose
          Value: M2M-Demo
        - Key: Project
          Value: !Ref ProjectName

  # Backend Lambda Function with updated runtime
  BackendLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${ProjectName}-backend'
      Runtime: python3.12
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 30
      MemorySize: 128
      Description: 'Backend API for Cognito M2M demo'
      Tags:
        - Key: Purpose
          Value: M2M-Demo
        - Key: Project
          Value: !Ref ProjectName
      Code:
        ZipFile: |
          import json
          import logging
          
          # Configure logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          
          def lambda_handler(event, context):
              try:
                  logger.info(f"Received event: {json.dumps(event)}")
                  
                  http_method = event.get('httpMethod', '')
                  path = event.get('path', '')
                  
                  # Handle GET /customers
                  if http_method == 'GET' and path == '/customers':
                      return {
                          'statusCode': 200,
                          'headers': {
                              'Content-Type': 'application/json',
                              'Access-Control-Allow-Origin': '*'
                          },
                          'body': json.dumps({
                              'customers': [
                                  {'id': '123', 'name': 'John Doe', 'email': 'john@example.com'},
                                  {'id': '456', 'name': 'Jane Smith', 'email': 'jane@example.com'}
                              ],
                              'message': 'Successfully retrieved customers'
                          })
                      }
                  
                  # Handle DELETE /customers/{id}
                  elif http_method == 'DELETE' and '/customers/' in path:
                      customer_id = path.split('/')[-1]
                      logger.info(f"Deleting customer: {customer_id}")
                      
                      return {
                          'statusCode': 200,
                          'headers': {
                              'Content-Type': 'application/json',
                              'Access-Control-Allow-Origin': '*'
                          },
                          'body': json.dumps({
                              'message': f'Customer {customer_id} deleted successfully',
                              'customerId': customer_id
                          })
                      }
                  
                  # Handle unsupported methods/paths
                  else:
                      return {
                          'statusCode': 404,
                          'headers': {
                              'Content-Type': 'application/json',
                              'Access-Control-Allow-Origin': '*'
                          },
                          'body': json.dumps({
                              'error': 'Not found',
                              'message': f'Method {http_method} not supported for path {path}'
                          })
                      }
                      
              except Exception as e:
                  logger.error(f"Error processing request: {str(e)}")
                  return {
                      'statusCode': 500,
                      'headers': {
                          'Content-Type': 'application/json',
                          'Access-Control-Allow-Origin': '*'
                      },
                      'body': json.dumps({
                          'error': 'Internal server error',
                          'message': 'An error occurred processing your request'
                      })
                  }

  # API Gateway with enhanced configuration
  ApiGateway:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub '${ProjectName}-api'
      Description: 'API with Cognito M2M Authorization'
      EndpointConfiguration:
        Types:
          - REGIONAL
      Tags:
        - Key: Purpose
          Value: M2M-Demo
        - Key: Project
          Value: !Ref ProjectName

  # Cognito Authorizer with enhanced configuration
  CognitoAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Name: !Sub '${ProjectName}-cognito-authorizer'
      RestApiId: !Ref ApiGateway
      Type: COGNITO_USER_POOLS
      ProviderARNs:
        - !GetAtt UserPool.Arn
      IdentitySource: method.request.header.Authorization
      AuthorizerResultTtlInSeconds: 300

  # Customers Resource
  CustomersResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !GetAtt ApiGateway.RootResourceId
      PathPart: 'customers'

  # Customer ID Resource
  CustomerIdResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref ApiGateway
      ParentId: !Ref CustomersResource
      PathPart: '{id}'

  # GET /customers Method (Read scope required)
  GetCustomersMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref CustomersResource
      HttpMethod: GET
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref CognitoAuthorizer
      AuthorizationScopes:
        - 'customer-api/read'
      MethodResponses:
        - StatusCode: '200'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
        - StatusCode: '401'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
        - StatusCode: '403'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BackendLambda.Arn}/invocations'

  # OPTIONS /customers Method (CORS preflight)
  OptionsCustomersMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref CustomersResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      MethodResponses:
        - StatusCode: '200'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
            method.response.header.Access-Control-Allow-Headers: false
            method.response.header.Access-Control-Allow-Methods: false
      Integration:
        Type: MOCK
        RequestTemplates:
          application/json: '{"statusCode": 200}'
        IntegrationResponses:
          - StatusCode: '200'
            ResponseParameters:
              method.response.header.Access-Control-Allow-Origin: "'*'"
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'GET,OPTIONS'"

  # DELETE /customers/{id} Method (Admin scope required)
  DeleteCustomerMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref CustomerIdResource
      HttpMethod: DELETE
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref CognitoAuthorizer
      AuthorizationScopes:
        - 'customer-api/admin'
      MethodResponses:
        - StatusCode: '200'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
        - StatusCode: '401'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
        - StatusCode: '403'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${BackendLambda.Arn}/invocations'

  # OPTIONS /customers/{id} Method (CORS preflight)
  OptionsCustomerIdMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref ApiGateway
      ResourceId: !Ref CustomerIdResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      MethodResponses:
        - StatusCode: '200'
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: false
            method.response.header.Access-Control-Allow-Headers: false
            method.response.header.Access-Control-Allow-Methods: false
      Integration:
        Type: MOCK
        RequestTemplates:
          application/json: '{"statusCode": 200}'
        IntegrationResponses:
          - StatusCode: '200'
            ResponseParameters:
              method.response.header.Access-Control-Allow-Origin: "'*'"
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'DELETE,OPTIONS'"

Outputs:
  UserPoolId:
    Description: 'Cognito User Pool ID'
    Value: !Ref UserPool
    Export:
      Name: !Sub '${AWS::StackName}-UserPoolId'

  UserPoolArn:
    Description: 'Cognito User Pool ARN'
    Value: !GetAtt UserPool.Arn
    Export:
      Name: !Sub '${AWS::StackName}-UserPoolArn'

  TokenEndpoint:
    Description: 'OAuth2 Token Endpoint for M2M authentication'
    Value: !Sub 'https://${ProjectName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com/oauth2/token'
    Export:
      Name: !Sub '${AWS::StackName}-TokenEndpoint'

  OrderClientId:
    Description: 'Order Processing Service Client ID (Full permissions: read, write, admin)'
    Value: !Ref OrderClient
    Export:
      Name: !Sub '${AWS::StackName}-OrderClientId'

  ReportingClientId:
    Description: 'Reporting Service Client ID (Read-only permissions)'
    Value: !Ref ReportingClient
    Export:
      Name: !Sub '${AWS::StackName}-ReportingClientId'

  ApiUrl:
    Description: 'API Gateway URL for testing endpoints'
    Value: !Sub 'https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod'
    Export:
      Name: !Sub '${AWS::StackName}-ApiUrl'

  JWKSEndpoint:
    Description: 'JWKS Endpoint for Token Validation'
    Value: !Sub 'https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json'
    Export:
      Name: !Sub '${AWS::StackName}-JWKSEndpoint'

  TestEndpoints:
    Description: 'Test endpoints for validation'
    Value: !Sub |
      GET (read scope): https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers
      DELETE (admin scope): https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers/{id}

  CurlTestCommands:
    Description: 'Sample curl commands for testing (replace CLIENT_ID and CLIENT_SECRET)'
    Value: !Sub |
      # Get access token for Order Client (full permissions):
      curl -X POST https://${ProjectName}-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com/oauth2/token \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "grant_type=client_credentials&client_id=ORDER_CLIENT_ID&client_secret=ORDER_CLIENT_SECRET&scope=customer-api/read customer-api/write customer-api/admin"
      
      # Get access token for Reporting Client (read-only):
      curl -X POST https://${ProjectName}-${AWS::AccountId}.auth.${AWS::Region}.amazonaws.com/oauth2/token \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "grant_type=client_credentials&client_id=REPORTING_CLIENT_ID&client_secret=REPORTING_CLIENT_SECRET&scope=customer-api/read"
      
      # Test GET customers (requires read scope):
      curl -X GET https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers \
        -H "Authorization: Bearer ACCESS_TOKEN"
      
      # Test DELETE customer (requires admin scope):
      curl -X DELETE https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prod/customers/123 \
        -H "Authorization: Bearer ACCESS_TOKEN"

⚠️ Remember to clean up test resources after validation to avoid ongoing costs!