Skip to content

πŸ” Complete serverless authentication demo using React, AWS Cognito, API Gateway, and Terraform. Showcases multiple auth flows and authorization patterns.

License

Notifications You must be signed in to change notification settings

igor-raits/serverless-api-react-auth-demo

Repository files navigation

Serverless API + React Authentication Demo

A comprehensive demonstration of AWS API Gateway authentication with Amazon Cognito, featuring a React web application and Python testing scripts. This project showcases a modern, secure, and highly extensible serverless architecture with multiple authentication methods and demonstrates the complete OAuth flow.

This architecture is designed to be production-ready and can easily be extended with federated identity providers like Okta, Auth0, PingFederation, and other SAML/OIDC providers through Cognito's flexible identity federation capabilities.

πŸ—οΈ Architecture Overview

This demo implements a full-stack serverless application with:

  • Frontend: React 19 + Vite SPA hosted on CloudFront + S3
  • Backend: AWS API Gateway HTTP API with Lambda functions
  • Authentication: Amazon Cognito User Pool + Identity Pool
  • Authorization: AWS IAM roles for fine-grained access control
  • Testing: Python scripts for comprehensive API testing

Architecture Benefits

βœ… Serverless: No server management, automatic scaling, pay-per-use
βœ… Secure: JWT-based authentication with AWS IAM authorization
βœ… Scalable: Handles millions of users with Cognito and API Gateway
βœ… Cost-Effective: Pay only for actual usage, not idle resources
βœ… Global: CloudFront CDN for worldwide low-latency access
βœ… Extensible: Easy integration with enterprise identity providers
βœ… Maintainable: Infrastructure as Code with Terraform
βœ… Observable: Built-in AWS monitoring and logging capabilities

Authentication Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   React App     β”‚    β”‚   Cognito User   β”‚    β”‚  Cognito        β”‚
β”‚                 β”‚    β”‚   Pool           β”‚    β”‚  Identity Pool  β”‚
β”‚                 β”‚    β”‚                  β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                        β”‚                       β”‚
         β”‚ 1. OAuth redirect      β”‚                       β”‚
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚                       β”‚
         β”‚                        β”‚                       β”‚
         β”‚ 2. ID + Access tokens  β”‚                       β”‚
         │◄────────────────────────                       β”‚
         β”‚                        β”‚                       β”‚
         β”‚ 3. Exchange ID token   β”‚                       β”‚
         β”‚ for AWS credentials    β”‚                       β”‚
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚
         β”‚                        β”‚                       β”‚
         β”‚ 4. Temporary AWS creds β”‚                       β”‚
         │◄───────────────────────┼────────────────────────
         β”‚                        β”‚                       β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                       β”‚
β”‚   API Gateway   β”‚                                       β”‚
β”‚                 β”‚                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚
         β”‚                                                β”‚
         β”‚ 5. API call with SigV4 + X-Cognito-Id-Token    β”‚
         β”‚β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸš€ Quick Start

Prerequisites

  • AWS CLI configured with appropriate credentials
  • Terraform >= 1.0
  • Node.js >= 18
  • Python >= 3.11 (for testing scripts - requires modern union syntax and match statements)

1. Clone and Configure

git clone https://github.com/igor-raits/serverless-api-react-auth-demo
cd serverless-api-react-auth-demo

# Copy and configure Terraform variables
cp terraform.auto.tfvars.example terraform.auto.tfvars

2. Configure Terraform Variables

Edit terraform.auto.tfvars:

# AWS Configuration
aws_region  = "us-east-1"           # Your preferred AWS region
aws_profile = "your-profile-name"   # Your AWS CLI profile

# CORS Configuration (optional)
# Add localhost for local development
# apigw_cors_allow_origins_extra = ["http://localhost:3000", "http://localhost:5173"]

3. Deploy Infrastructure

# Initialize Terraform
terraform init

# Plan deployment (optional but recommended)
terraform plan

# Deploy all resources
terraform apply

This creates:

  • Cognito User Pool and Identity Pool
  • API Gateway HTTP API with Lambda backend
  • S3 bucket and CloudFront distribution
  • IAM roles and policies

4. Deploy React Application

# Build and deploy React app
./run.sh

The script will:

  • Extract Terraform outputs
  • Install npm dependencies
  • Build React app with proper environment variables
  • Deploy to S3 and CloudFront

5. Create Cognito Users

# Use the user management script
./manage-users.sh

Choose option 1 to create a new user, then 2 to set a permanent password.

6. Test the Application

Web Interface: Visit the CloudFront domain URL (shown after deployment) to test the React app.

Python Testing Scripts:

# Install testing dependencies (optional)
pip install pycognito boto3 requests

# Run comprehensive API tests
python test_auth.py

πŸ“ Project Structure

β”œβ”€β”€ terraform.auto.tfvars.example   # Terraform configuration template
β”œβ”€β”€ providers.tf                    # Terraform providers
β”œβ”€β”€ variables.tf                    # Terraform variables
β”œβ”€β”€ cognito.tf                     # Cognito User/Identity Pools
β”œβ”€β”€ apigw.tf                       # API Gateway + Lambda
β”œβ”€β”€ fe.tf                          # S3 + CloudFront frontend
β”œβ”€β”€ outputs.tf                     # Terraform outputs
β”œβ”€β”€ lambda_function.py             # Lambda backend code
β”œβ”€β”€ run.sh                         # React development and deployment script
β”œβ”€β”€ manage-users.sh                # Cognito user management
β”œβ”€β”€ test_auth.py                   # Python API testing script
└── react-app/
    β”œβ”€β”€ index.html                 # Main HTML template
    β”œβ”€β”€ package.json               # Node.js dependencies
    β”œβ”€β”€ vite.config.js             # Vite build configuration
    └── src/
        β”œβ”€β”€ index.jsx              # React app entry point
        β”œβ”€β”€ index.css              # Global styles
        β”œβ”€β”€ App.jsx                # Main React component
        β”œβ”€β”€ CallbackPage.jsx       # OAuth callback handler
        └── config.js              # AWS Amplify configuration

πŸ” Authorization Architecture Layers

This demo showcases a multi-layered authorization architecture that progresses from completely public access to fine-grained user-specific permissions:

Layer 1: API Endpoints and Their Requirements

The demo includes three endpoints that demonstrate different authorization patterns:

GET /test/plain

  • Requirement: None - completely public
  • Access: Anyone on the internet
  • Use Case: Health checks, public documentation, marketing pages

GET /test/public

  • Requirement: Valid AWS credentials (any AWS IAM user/role)
  • Access: Anyone with AWS credentials that have execute-api:Invoke permission
  • Use Case: Semi-public APIs that need basic AWS authentication but not user identity

GET /test/auth

  • Requirement: AWS credentials obtained through Cognito authentication flow
  • Access: Users who have authenticated via Cognito and obtained AWS credentials
  • Use Case: User-specific APIs that need identity context and authorization

Layer 2: IAM Roles and Assignment Logic

The Cognito Identity Pool acts as a credential broker, exchanging Cognito tokens for temporary AWS credentials mapped to specific IAM roles:

Role Assignment Hierarchy

1. Unauthenticated Role

  • When assigned: When requesting AWS credentials without providing any Cognito tokens
  • Typical permissions: Very limited - only specific "public" endpoints
  • Example access: Can call /test/public but not /test/auth

2. Default Authenticated Role

  • When assigned: When providing valid Cognito User Pool tokens but no specific group mapping applies
  • Typical permissions: Standard user access to most application endpoints
  • Example access: Can call both /test/public and /test/auth

3. Group-Mapped Roles

  • When assigned: When user belongs to specific Cognito groups (Admin, Viewer, etc.)
  • Role selection: Identity Pool examines cognito:groups claim in JWT and maps to specialized roles
  • Typical permissions: Role-specific access patterns

Group-to-Role Mapping Logic

The Identity Pool can be configured with mapping rules that examine JWT claims and assign different IAM roles:

Mapping Rule Priority:

  1. Check if user has cognito:groups containing "Admin" β†’ Assign Admin IAM role
  2. Check if user has cognito:groups containing "Viewer" β†’ Assign Viewer IAM role
  3. Fallback to default authenticated role

Real-World Scenario:

User authenticates β†’ JWT contains "cognito:groups": ["Admin"]
                  β†’ Identity Pool sees "Admin" group
                  β†’ Maps to Admin IAM role with elevated permissions
                  β†’ User can now access admin-only endpoints

Role Permission Examples

Admin Role Permissions:

  • All API Gateway endpoints (execute-api:Invoke on */*/*)
  • Full database operations (dynamodb:GetItem, dynamodb:Query, dynamodb:PutItem, dynamodb:DeleteItem)
  • Complete S3 access (s3:GetObject, s3:PutObject, s3:DeleteObject)

Viewer Role Permissions:

  • Read-only API endpoints (execute-api:Invoke on */*/GET/*)
  • Database read operations (dynamodb:GetItem, dynamodb:Query)
  • S3 object reading (s3:GetObject)

Standard Authenticated Role Permissions:

  • Most API endpoints except admin-specific ones
  • User-scoped database operations
  • User-scoped S3 access

Layer 3: Group Priority and Precedence

Cognito Groups have a precedence value that determines which group takes priority when a user belongs to multiple groups:

Group Precedence Example:

  • Admin group: precedence = 0 (highest priority)
  • Manager group: precedence = 10
  • Viewer group: precedence = 99 (lowest priority)

Multi-Group Scenario:

User belongs to: ["Manager", "Viewer"]
β†’ Identity Pool sees both groups
β†’ "Manager" has higher precedence (lower number)
β†’ User gets Manager IAM role permissions

Layer 4: Fine-Grained Authorization via JWT Parsing

Beyond IAM: Once inside the Lambda function, you can parse the JWT token for user-specific authorization that goes beyond what IAM roles can provide.

JWT Token Structure

{
  "sub": "12345678-1234-1234-1234-123456789012",
  "email": "[email protected]",
  "cognito:groups": ["Admin", "ProjectManager"],
  "cognito:username": "[email protected]"
}

Note: Custom attributes like custom:department or custom:project_ids can be configured in the Cognito User Pool and automatically mapped from federated identity providers (Okta, Auth0, etc.). This enables department-based or project-based authorization patterns.

Lambda Authorization Patterns

Pattern 1: Resource Ownership

def check_resource_access(jwt_payload, resource_id):
    user_id = jwt_payload.get("sub")

    # Check if user owns the resource
    resource = get_resource(resource_id)
    if resource.owner_id == user_id:
        return True

    # Check group-based override
    user_groups = jwt_payload.get("cognito:groups", [])
    if "Admin" in user_groups:
        return True

    return False

Pattern 2: Department-Based Access

def check_department_access(jwt_payload, resource_id):
    user_dept = jwt_payload.get("custom:department")  # Requires User Pool custom attribute

    resource = get_resource(resource_id)
    return resource.department == user_dept

Pattern 3: Project-Based Access

def check_project_access(jwt_payload, resource_id):
    user_projects = jwt_payload.get("custom:project_ids", "").split(",")  # Custom attribute

    resource = get_resource(resource_id)
    return resource.project_id in user_projects

Dynamic Group Assignment (Optional Extension)

Cognito Hooks for Advanced Scenarios: You can use Cognito Lambda triggers to dynamically assign groups during user lifecycle events. This is purely optional and not required for the demo, but shows the extensibility:

# Pre Token Generation Lambda trigger (optional advanced pattern)
def lambda_handler(event, context):
    # Handle token generation events where group assignment is needed
    # TokenGeneration_HostedAuth: Cognito Hosted UI sign-in (includes federated providers)
    # TokenGeneration_Authentication: Direct API authentication flows
    if event["triggerSource"] in {"TokenGeneration_HostedAuth", "TokenGeneration_Authentication"}:

        user_pool_id = event["userPoolId"]
        username = event["userName"]

        # Extract user attributes (from federated identity or custom attributes)
        user_attributes = event["request"].get("userAttributes", {})
        department = user_attributes.get("custom:department", "")

        # Simple mapping: Engineering department gets Developer group
        if department == "Engineering":
            # Copy existing group configuration to preserve current groups
            group_config = event["request"].get("groupConfiguration", {})
            existing_groups = group_config.get("groupsToOverride", [])

            # Add Developer group if not already present
            if "Developer" not in existing_groups:
                # Assign to Developer group (which maps to Developer IAM role in Identity Pool)
                cognito_client.admin_add_user_to_group(
                    UserPoolId=user_pool_id,
                    Username=username,
                    GroupName="Developer",
                )

                # Copy the entire group configuration and add the new group
                updated_group_config = group_config.copy()
                updated_group_config["groupsToOverride"] = existing_groups + ["Developer"]

                # Update the current token being generated
                event["response"]["claimsOverrideDetails"] = {
                    "groupOverrideDetails": updated_group_config,
                }

        # Note: Identity Pool will later map 'Developer' group to Developer IAM role
        # when user requests AWS credentials for API calls

    return event

Use Cases for Dynamic Assignment:

  • Federated Identity Mapping: Map custom:department from external identity providers to Cognito groups (e.g., Engineering β†’ Developer group)
  • Conditional Access: Assign groups based on user attributes during first authentication
  • Zero Manual Setup: Users get proper permissions immediately without admin intervention
  • Identity Provider Integration: Works seamlessly with any SAML/OIDC provider that sends department/role attributes

Authorization Flow Summary

Internet Request
       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. API Gateway  β”‚ ← Public endpoints (no auth required)
β”‚    Endpoint     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. IAM Auth     β”‚ ← AWS credentials validation
β”‚    Validation   β”‚   β€’ Any AWS creds for /test/public
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β€’ Cognito-issued creds for /test/auth
       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. Role-Based   β”‚ ← IAM role permissions check
β”‚    IAM Policy   β”‚   β€’ Unauthenticated β†’ limited access
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β€’ Authenticated β†’ broader access
       ↓               β€’ Group-mapped β†’ specialized access
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 4. Lambda       β”‚ ← JWT parsing for user-specific logic
β”‚    Fine-Grained β”‚   β€’ Resource ownership checks
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β€’ Custom attribute validation
       ↓               β€’ Business rule enforcement
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 5. Business     β”‚ ← Your application logic
β”‚    Logic        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The architecture provides multiple authorization layers from public access to fine-grained user-specific permissions.

πŸ”§ Lambda Function Purpose

The lambda_function.py serves as a demonstration backend that showcases how to:

  • Extract user information from the custom X-Cognito-Id-Token header
  • Decode JWT tokens without verification (for demo purposes)
  • Parse Cognito groups (Admin, Viewer) for role-based access control
  • Handle both authenticated and unauthenticated requests gracefully
  • Return detailed debugging information including:
    • User identity details (username, email, sub, etc.)
    • Group memberships and authorization flags
    • API Gateway request context
    • AWS credentials context (Identity Pool ID)

Sample Lambda Response:

{
  "message": "Hello from Lambda!",
  "user_info": {
    "username": "[email protected]",
    "email": "[email protected]",
    "sub": "12345678-1234-1234-1234-123456789012"
  },
  "user_groups": ["Admin"],
  "is_admin": true,
  "is_viewer": false,
  "api_info": {
    "api_id": "abc123xyz",
    "stage": "$default",
    "request_id": "12345678-1234-1234-1234-123456789012"
  }
}

This pattern demonstrates how to build stateless, secure APIs that can make authorization decisions based on user identity and group membership.

πŸ” Role-Based Access Control (RBAC) Implementation

This architecture provides a foundation for implementing RBAC by combining Cognito groups (coarse-grained) with user.userId-based permissions (fine-grained):

The Two-Layer Authorization Model

Layer 1: Cognito Groups (Application-Level Roles)

  • What: Broad application roles (Admin, Viewer, Manager, Editor)
  • Where: Stored in JWT token as cognito:groups claim
  • Used for: Feature access, UI visibility, API endpoint authorization
  • Performance: Fast - no database lookup required

Layer 2: User Permissions (Resource-Level Access)

  • What: Specific resource ownership and granular permissions
  • Where: Stored in DynamoDB or other database, keyed by user.userId (Cognito sub)
  • Used for: Document ownership, team memberships, custom business rules
  • Performance: Single database query per authorization check

Integration Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   API Gateway   β”‚    β”‚   Lambda API     β”‚    β”‚   DynamoDB      β”‚
β”‚   (AWS IAM)     β”‚    β”‚   Function       β”‚    β”‚   Permissions   β”‚
β”‚                 β”‚    β”‚                  β”‚    β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                        β”‚                       β”‚
         β”‚ 1. Request +           β”‚                       β”‚
         β”‚ X-Cognito-Id-Token     β”‚                       β”‚
         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚                       β”‚
         β”‚                        β”‚ 2. Extract groups     β”‚
         β”‚                        β”‚ from JWT token        β”‚
         β”‚                        β”‚                       β”‚
         β”‚                        β”‚ 3. Query user perms   β”‚
         β”‚                        β”‚ by user.userId        β”‚
         β”‚                        β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚
         β”‚                        β”‚                       β”‚
         β”‚                        β”‚ 4. Permission data    β”‚
         β”‚                        │◄───────────────────────
         β”‚                        β”‚                       β”‚
         β”‚ 5. API response        β”‚                       β”‚
         │◄────────────────────────                       β”‚

Note: API Gateway still uses AWS IAM authorization - the Lambda function is your regular API endpoint that simply checks permissions as part of its business logic.

How Components Work Together

1. Cognito Groups Handle High-Level Access:

# In Lambda function - extract from JWT (no DB query)
user_groups = jwt_payload.get("cognito:groups", [])

# Quick feature access decisions
if "Admin" in user_groups:
    return allow_all_access()
if "Viewer" in user_groups and request_method == "GET":
    return continue_to_resource_check()

2. DynamoDB Handles Fine-Grained Permissions:

# Query by user.userId for resource-specific permissions
user_id = jwt_payload.get("sub")  # Cognito user ID
permissions = dynamodb.get_item(
    Key={"user_id": user_id, "resource_id": resource_id}
)

# Business logic decisions
if permissions.get("owner") == user_id:
    return allow_access()
if user_id in permissions.get("collaborators", []):
    return allow_read_only()

DynamoDB Permission Store Design

Why DynamoDB is Perfect for This:

  • Fast lookups by user_id and resource_id
  • Schema flexibility for evolving permission structures
  • Serverless - fits the architecture perfectly
  • Global Secondary Indexes for querying by resource or team
  • TTL support for temporary permissions

Key Access Patterns:

# Pattern 1: User's permissions on specific resource
{
    "user_id": "user-123",           # Partition key
    "resource_id": "doc-456",        # Sort key
    "permissions": ["read", "write"],
    "granted_by": "admin-789",
    "expires_at": 1234567890
}

# Pattern 2: Team memberships (using GSI)
{
    "user_id": "user-123",
    "team_id": "team-abc",           # GSI partition key
    "role": "manager",
    "resource_type": "team_membership"
}

Lambda Authorization Flow

Simple Integration Example:

def lambda_handler(event, context):
    # Step 1: Extract user info from custom header
    user_info, user_groups = get_user_info_from_token(event)

    # Step 2: Quick group-based checks (no DB query needed)
    if "Admin" in user_groups:
        # Admin can access everything - proceed with full business logic
        return handle_admin_request(event, user_info)

    # Step 3: Resource-specific authorization (DynamoDB query if needed)
    resource_id = event["pathParameters"].get("id")
    user_id = user_info["sub"]

    has_permission = check_user_permission(user_id, resource_id, "read")

    if has_permission or "Manager" in user_groups:
        return handle_authorized_request(event, user_info)
    else:
        return {"statusCode": 403, "body": "Access denied"}

def check_user_permission(user_id, resource_id, action):
    # Single DynamoDB query for fine-grained permissions
    response = dynamodb.get_item(
        Key={"user_id": user_id, "resource_id": resource_id}
    )
    permissions = response.get("Item", {}).get("permissions", [])
    return action in permissions

def handle_authorized_request(event, user_info):
    # Your actual business logic here
    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "Success!",
            "data": get_user_specific_data(user_info["sub"])
        })
    }

Real-World Usage Examples

Document Management System:

  • Cognito Groups: Editor can create documents, Viewer can only read
  • User Permissions: Each document has specific collaborators beyond group rules
  • Integration: Check group first (fast), then document ownership (DynamoDB)

Multi-Tenant Application:

  • Cognito Groups: OrgAdmin manages organization, User accesses resources
  • User Permissions: Each user belongs to specific organizations and projects
  • Integration: Group defines scope, DynamoDB defines which orgs/projects

Integration Benefits

  1. Performance: Group checks are instant, permission checks are single queries
  2. Scalability: Cognito handles millions of users, DynamoDB scales permissions independently
  3. Flexibility: Add new groups without DB changes, add new permissions without Cognito changes
  4. Maintainability: Clear separation between broad roles and specific permissions
  5. Cost-Effective: Only query DynamoDB when needed, leverage JWT caching

The architecture separates concerns between coarse-grained group permissions and fine-grained resource permissions.

πŸ”‘ The X-Cognito-Id-Token Header Explained

The X-Cognito-Id-Token header is a custom implementation used in this demo to pass user context to the Lambda function:

Why Use This Header?

  1. User Information: API Gateway's IAM authorization only validates AWS credentials but doesn't provide user details
  2. Group Membership: The ID token contains Cognito groups (Admin, Viewer) for authorization logic
  3. User Context: Enables personalized responses based on user attributes

How It Works

// React app sends both SigV4 auth AND custom header
headers['Authorization'] = 'AWS4-HMAC-SHA256 ...'  // SigV4 signature
headers['X-Cognito-Id-Token'] = tokens.idToken     // JWT with user info
# Lambda function extracts user info from the custom header
def get_user_info_from_token(event):
    headers = event.get("headers", {})
    id_token = headers.get("x-cognito-id-token")  # Case insensitive

    # Decode JWT to extract user info and groups
    payload = decode_jwt_payload(id_token)
    user_groups = payload.get("cognito:groups", [])

    return user_info, user_groups

Security Considerations

  • The JWT signature should be verified in production (this demo skips verification for simplicity)
  • The custom header is in addition to, not instead of, proper AWS authentication
  • Consider using API Gateway JWT authorizers for production scenarios

πŸ› οΈ API Endpoints

Endpoint Authorization Access Level Description
GET /test/plain None Public No authentication required
GET /test/public AWS IAM Unauthenticated role Requires Identity Pool credentials
GET /test/auth AWS IAM Authenticated role Requires User Pool authentication

IAM Policy Differences

Unauthenticated Role:

{
  "Effect": "Allow",
  "Action": "execute-api:Invoke",
  "Resource": "arn:aws:execute-api:*/*/GET/test/public"
}

Authenticated Role:

{
  "Effect": "Allow",
  "Action": "execute-api:Invoke",
  "Resource": "arn:aws:execute-api:*/*/*"
}

πŸ§ͺ Testing Scenarios

The test_auth.py script demonstrates multiple authentication patterns:

1. No Authentication

curl https://your-api-gateway.execute-api.us-east-1.amazonaws.com/test/plain

2. AWS IAM with User Credentials

# Uses your AWS CLI profile credentials
session = boto3.Session(profile_name=AWS_PROFILE)
credentials = session.get_credentials()
# Signs request with SigV4

3. Cognito Password Authentication

cognito_client.initiate_auth(
    ClientId=client_id,
    AuthFlow="USER_PASSWORD_AUTH",
    AuthParameters={"USERNAME": username, "PASSWORD": password}
)

4. Cognito SRP Authentication (Optional)

# Requires 'pycognito' library
pip install pycognito

# More secure than password auth
u = Cognito(user_pool_id, client_id, username=username)
u.authenticate(password=password)

5. Unauthenticated Cognito Identity

# Gets temporary AWS credentials without authentication
identity_response = cognito_identity.get_id(
    IdentityPoolId=identity_pool_id
    # No Logins parameter = unauthenticated
)

πŸ‘₯ User Management

Creating Users

Use the manage-users.sh script for user management:

./manage-users.sh

Options available:

  1. Create new user - Creates user with temporary password
  2. Set permanent password - Updates user to permanent password
  3. List all users - Shows all users and their status
  4. Add user to group - Assigns users to Admin or Viewer groups
  5. Delete user - Removes user from pool

Manual User Creation

# Create user via AWS CLI
aws cognito-idp admin-create-user \
  --user-pool-id "us-east-1_XXXXXXXXX" \
  --username "[email protected]" \
  --user-attributes Name=email,Value="[email protected]" Name=email_verified,Value=true \
  --temporary-password "TempPass123!" \
  --message-action SUPPRESS

# Set permanent password
aws cognito-idp admin-set-user-password \
  --user-pool-id "us-east-1_XXXXXXXXX" \
  --username "[email protected]" \
  --password "NewPassword123!" \
  --permanent

πŸ”§ Configuration Details

Environment Variables (React App)

The run.sh script automatically sets these during build:

export VITE_USER_POOL_ID="us-east-1_XXXXXXXXX"
export VITE_USER_POOL_CLIENT_ID="XXXXXXXXXXXXXXXXXXXXXXXXXX"
export VITE_IDENTITY_POOL_ID="us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
export VITE_OAUTH_DOMAIN="your-domain.auth.us-east-1.amazoncognito.com"
export VITE_API_ENDPOINT="https://api-id.execute-api.us-east-1.amazonaws.com"
export VITE_AWS_REGION="us-east-1"
export VITE_REDIRECT_SIGN_IN="https://cloudfront-domain/callback"
export VITE_REDIRECT_SIGN_OUT="https://cloudfront-domain/"

Cognito Configuration

User Pool Settings:

  • Username attributes: Email
  • Password policy: Simplified for demo (6 chars minimum)
  • Groups: Admin (precedence 0), Viewer (precedence 99)
  • OAuth flows: Authorization code flow
  • Managed Login: Version 2 (modern Hosted UI)

Identity Pool Settings:

  • Unauthenticated access: Enabled
  • Authentication providers: Cognito User Pool
  • IAM roles: Separate roles for authenticated/unauthenticated

🌐 Federation & Extensibility

This architecture supports federated identity providers such as Okta, Auth0, PingFederation, Azure AD, and other SAML/OIDC providers through Cognito's identity federation capabilities.

Federation Capabilities

  1. Single Sign-On (SSO): Users authenticate once across all corporate applications
  2. Centralized User Management: Leverage existing corporate directories
  3. Multi-Organization Support: Mix and match providers for unified login across multiple companies or business units
  4. Compliance: Meet enterprise security and audit requirements
  5. Zero-Trust Architecture: Fine-grained access control through IAM policies
  6. Scalability: Handle millions of users across multiple identity sources

The architecture supports enterprise authentication patterns through Cognito's federation capabilities.

πŸš€ Production Considerations

Security Enhancements

  1. JWT Verification: Implement proper JWT signature verification in Lambda
  2. Password Policy: Strengthen password requirements
  3. MFA: Enable multi-factor authentication
  4. Advanced Security: Enable Cognito advanced security features
  5. Custom Certificates: Set up custom SSL certificates and ensure TLS 1.2+ for enhanced security
  6. Rate Limiting: Implement API Gateway throttling and usage plans
  7. Input Validation: Add comprehensive input validation in Lambda functions

Monitoring & Logging

  1. CloudWatch: Monitor API Gateway and Lambda metrics
  2. CloudTrail: Track authentication events
  3. WAF: Add Web Application Firewall for additional protection

πŸ› Troubleshooting

Common Issues

"Missing required Terraform outputs"

  • Ensure terraform apply completed successfully
  • Check AWS credentials and permissions

"CORS errors in browser"

  • Check API Gateway CORS configuration
  • Verify CloudFront domain is in allowed origins

"403 Forbidden from API"

  • Verify user is in correct Cognito group
  • Check IAM policies for authenticated/unauthenticated roles
  • Ensure Identity Pool is configured correctly

"Python syntax errors in test_auth.py"

  • Ensure you're using Python 3.11+ (required for | None union syntax and match statements)
  • Update Python: python --version should show 3.11 or higher

Debug Steps

  1. Check Terraform outputs:

    terraform output
  2. Test API endpoints manually:

    python test_auth.py
  3. Verify Cognito configuration:

    aws cognito-idp describe-user-pool --user-pool-id <pool-id>
  4. Check browser console for detailed error messages

πŸ“š Learn More

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Test thoroughly
  5. Submit a pull request

πŸ“„ License

This project is released into the public domain under The Unlicense.

About

πŸ” Complete serverless authentication demo using React, AWS Cognito, API Gateway, and Terraform. Showcases multiple auth flows and authorization patterns.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published