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.
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
β
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
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β 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 β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
- 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)
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
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"]
# 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
# 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
# Use the user management script
./manage-users.sh
Choose option 1 to create a new user, then 2 to set a permanent password.
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
βββ 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
This demo showcases a multi-layered authorization architecture that progresses from completely public access to fine-grained user-specific permissions:
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
The Cognito Identity Pool acts as a credential broker, exchanging Cognito tokens for temporary AWS credentials mapped to specific IAM roles:
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
The Identity Pool can be configured with mapping rules that examine JWT claims and assign different IAM roles:
Mapping Rule Priority:
- Check if user has
cognito:groups
containing "Admin" β Assign Admin IAM role - Check if user has
cognito:groups
containing "Viewer" β Assign Viewer IAM role - 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
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
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
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.
{
"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.
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
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
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.
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.
This architecture provides a foundation for implementing RBAC by combining Cognito groups (coarse-grained) with user.userId-based permissions (fine-grained):
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
(Cognitosub
) - Used for: Document ownership, team memberships, custom business rules
- Performance: Single database query per authorization check
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β 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.
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()
Why DynamoDB is Perfect for This:
- Fast lookups by
user_id
andresource_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"
}
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"])
})
}
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
- Performance: Group checks are instant, permission checks are single queries
- Scalability: Cognito handles millions of users, DynamoDB scales permissions independently
- Flexibility: Add new groups without DB changes, add new permissions without Cognito changes
- Maintainability: Clear separation between broad roles and specific permissions
- 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 is a custom implementation used in this demo to pass user context to the Lambda function:
- User Information: API Gateway's IAM authorization only validates AWS credentials but doesn't provide user details
- Group Membership: The ID token contains Cognito groups (
Admin
,Viewer
) for authorization logic - User Context: Enables personalized responses based on user attributes
// 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
- 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
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 |
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:*/*/*"
}
The test_auth.py
script demonstrates multiple authentication patterns:
curl https://your-api-gateway.execute-api.us-east-1.amazonaws.com/test/plain
# Uses your AWS CLI profile credentials
session = boto3.Session(profile_name=AWS_PROFILE)
credentials = session.get_credentials()
# Signs request with SigV4
cognito_client.initiate_auth(
ClientId=client_id,
AuthFlow="USER_PASSWORD_AUTH",
AuthParameters={"USERNAME": username, "PASSWORD": password}
)
# Requires 'pycognito' library
pip install pycognito
# More secure than password auth
u = Cognito(user_pool_id, client_id, username=username)
u.authenticate(password=password)
# Gets temporary AWS credentials without authentication
identity_response = cognito_identity.get_id(
IdentityPoolId=identity_pool_id
# No Logins parameter = unauthenticated
)
Use the manage-users.sh
script for user management:
./manage-users.sh
Options available:
- Create new user - Creates user with temporary password
- Set permanent password - Updates user to permanent password
- List all users - Shows all users and their status
- Add user to group - Assigns users to
Admin
orViewer
groups - Delete user - Removes user from pool
# 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
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/"
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
This architecture supports federated identity providers such as Okta, Auth0, PingFederation, Azure AD, and other SAML/OIDC providers through Cognito's identity federation capabilities.
- Single Sign-On (SSO): Users authenticate once across all corporate applications
- Centralized User Management: Leverage existing corporate directories
- Multi-Organization Support: Mix and match providers for unified login across multiple companies or business units
- Compliance: Meet enterprise security and audit requirements
- Zero-Trust Architecture: Fine-grained access control through IAM policies
- Scalability: Handle millions of users across multiple identity sources
The architecture supports enterprise authentication patterns through Cognito's federation capabilities.
- JWT Verification: Implement proper JWT signature verification in Lambda
- Password Policy: Strengthen password requirements
- MFA: Enable multi-factor authentication
- Advanced Security: Enable Cognito advanced security features
- Custom Certificates: Set up custom SSL certificates and ensure TLS 1.2+ for enhanced security
- Rate Limiting: Implement API Gateway throttling and usage plans
- Input Validation: Add comprehensive input validation in Lambda functions
- CloudWatch: Monitor API Gateway and Lambda metrics
- CloudTrail: Track authentication events
- WAF: Add Web Application Firewall for additional protection
"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 andmatch
statements) - Update Python:
python --version
should show 3.11 or higher
-
Check Terraform outputs:
terraform output
-
Test API endpoints manually:
python test_auth.py
-
Verify Cognito configuration:
aws cognito-idp describe-user-pool --user-pool-id <pool-id>
-
Check browser console for detailed error messages
- AWS Cognito User Pools
- AWS Cognito Identity Pools
- API Gateway IAM Authentication
- AWS Amplify Authentication
- JWT.io - JWT token decoder
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
This project is released into the public domain under The Unlicense.