Secure API Authentication with AWS S3 and Lambda
If you're hosting your React application as a static website on AWS S3, you'll need a secure way to handle API authentication without exposing sensitive credentials. This guide shows how to set up AWS Lambda and API Gateway to securely authenticate with APIs while keeping your tokens completely hidden from the client.
Architecture Overview
The architecture consists of four main components:
- S3 Static Website - Hosts your React application
- CloudFront - CDN that serves your static website
- API Gateway - Provides HTTP endpoints that your React app calls
- Lambda Functions - Contains your API tokens and makes authenticated API requests
How It Works
Request Flow:
- Users access your React application hosted on S3 (typically through CloudFront)
- Your React app makes requests to your API Gateway endpoint when it needs data
- API Gateway triggers your Lambda function
- The Lambda function retrieves your securely stored API token from environment variables
- Lambda makes an authenticated request to the external API using the token
- The response flows back through Lambda and API Gateway to your React app
Key Security Benefit: Your API token is only stored in Lambda environment variables and never exposed to the client-side code or browser, keeping it completely secure.
Step 1: Deploy Your React App to S3
First, create and configure your S3 bucket for website hosting:
# Create an S3 bucket for your website
aws s3 mb s3://your-website-bucket
# Configure the bucket for static website hosting
aws s3 website s3://your-website-bucket --index-document index.html --error-document index.html
# Set the bucket policy to allow public access
aws s3api put-bucket-policy --bucket your-website-bucket --policy '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-website-bucket/*"
}
]
}'
# Upload your built React application
aws s3 sync ./build/ s3://your-website-bucket --delete
Step 2: Create a Lambda Function for API Proxying
Next, create a Lambda function that will securely make API calls:
// recommendations-proxy.js
const https = require('https');
exports.handler = async (event) => {
// Extract query parameters from the request
const queryParams = event.queryStringParameters || {};
// Build URL search params
const params = new URLSearchParams();
Object.keys(queryParams).forEach(key => {
params.append(key, queryParams[key]);
});
// Your API token securely stored as a Lambda environment variable
const apiToken = process.env.API_TOKEN;
try {
// Make authenticated API request
const data = await new Promise((resolve, reject) => {
const apiUrl = `https://<PA_END_POINT>/3.0/recommendations?${params.toString()}`;
const options = {
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
}
};
https.get(apiUrl, options, (res) => {
let responseData = '';
res.on('data', (chunk) => {
responseData += chunk;
});
res.on('end', () => {
try {
resolve(JSON.parse(responseData));
} catch (e) {
reject(new Error('Failed to parse response as JSON'));
}
});
}).on('error', (error) => {
reject(error);
});
});
// Return the response to the client
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*', // Update this to your domain in production
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
};
} catch (error) {
// Handle errors
return {
statusCode: 500,
headers: {
'Access-Control-Allow-Origin': '*', // Update this to your domain in production
'Content-Type': 'application/json'
},
body: JSON.stringify({ error: 'Failed to fetch recommendations' })
};
}
};
You can deploy this using the AWS CLI, AWS Lambda console, or AWS SAM.
Step 3: Set Up Environment Variables in Lambda
In the Lambda console:
- Go to your function
- Click on the "Configuration" tab
- Select "Environment variables"
- Add a key-value pair:
- Key:
API_TOKEN
- Value:
your-actual-api-token
- Key:
This keeps your token secure and out of your code repository.
Step 4: Create an API Gateway Endpoint
- In the AWS Management Console, go to API Gateway
- Create a new REST API
- Create a new resource (e.g.,
/recommendations
) - Create a GET method for that resource
- Set the integration type to "Lambda Function"
- Select your Lambda function
- Enable CORS:
- Access-Control-Allow-Origin: Your S3 website URL
- Access-Control-Allow-Methods: GET, OPTIONS
- Access-Control-Allow-Headers: Content-Type, Authorization
- Deploy your API to a stage (e.g., "prod")
This will give you an API endpoint like:
https://abc123def.execute-api.us-east-1.amazonaws.com/prod/recommendations
Step 5: Update Your React App
Update your React app to call your new API Gateway endpoint instead of directly calling the API:
// React component
function ProductRecommendations({ productId }) {
const [recommendations, setRecommendations] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Call your API Gateway endpoint
const apiGatewayUrl = 'https://abc123def.execute-api.us-east-1.amazonaws.com/prod/recommendations';
fetch(`${apiGatewayUrl}?refId=${productId}¤tUrl=${window.location.href}`)
.then(res => res.json())
.then(data => {
setRecommendations(data);
setLoading(false);
})
.catch(err => {
console.error('Error:', err);
setLoading(false);
});
}, [productId]);
if (loading) return <div>Loading recommendations...</div>;
// Render recommendations...
return (
<div>
{/* Render your recommendations UI here */}
</div>
);
}
Step 6: Deploy with CloudFront (Optional but Recommended)
For better performance and HTTPS support, add CloudFront in front of your S3 bucket:
- Create a new CloudFront distribution
- Set the origin to your S3 website endpoint
- Configure cache behaviors
- Set up custom error responses (for SPA routing)
- Configure HTTPS settings
Performance Optimization
Implement Caching in Lambda
To reduce API calls and improve performance:
// Add caching using Lambda with Amazon DynamoDB
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
const queryParams = event.queryStringParameters || {};
const cacheKey = JSON.stringify(queryParams);
const ttl = 60 * 5; // 5 minutes cache
// Try to get from cache
try {
const cacheResult = await dynamodb.get({
TableName: 'ApiCache',
Key: { id: cacheKey }
}).promise();
// If cache hit and not expired
if (cacheResult.Item && cacheResult.Item.expiresAt > Math.floor(Date.now() / 1000)) {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
'X-Cache': 'HIT'
},
body: cacheResult.Item.data
};
}
} catch (e) {
console.log('Cache lookup error', e);
}
// Cache miss - fetch from API
// ... (same API fetch logic as before)
// Store in cache
try {
await dynamodb.put({
TableName: 'ApiCache',
Item: {
id: cacheKey,
data: JSON.stringify(data),
expiresAt: Math.floor(Date.now() / 1000) + ttl
}
}).promise();
} catch (e) {
console.log('Cache store error', e);
}
// Return response
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
'X-Cache': 'MISS'
},
body: JSON.stringify(data)
};
};
Security Best Practices
-
Restrict API Gateway access:
- Use API keys for additional security
- Set up AWS WAF to protect against common attacks
-
Lock down CORS settings:
- In production, restrict the Access-Control-Allow-Origin header to your domain only
-
Implement request validation:
- Add validation in your Lambda function to reject malformed requests
-
Set up monitoring and alerts:
- Use CloudWatch to monitor Lambda invocations and errors
- Set up alarms for unusual activity patterns
-
Regularly rotate API tokens:
- Implement a process to update your API tokens periodically
Cost Considerations
This architecture is cost-effective for most applications:
- S3 static website hosting: ~$0.50/month for 10GB storage + data transfer
- Lambda: First 1M requests/month free, then $0.20 per 1M requests
- API Gateway: $3.50 per million API calls
- CloudFront: ~$0.085 per GB of data transfer
For typical applications with moderate traffic, this setup costs just a few dollars per month.
Troubleshooting
CORS Issues
If you see CORS errors in your browser console:
- Ensure your API Gateway has CORS enabled
- Check that the 'Access-Control-Allow-Origin' header is correctly set
- For OPTIONS requests, ensure all required CORS headers are returned
5xx Errors from API Gateway
If your endpoint returns 5xx errors:
- Check Lambda CloudWatch logs for errors
- Verify your API token is correct and not expired
- Ensure your Lambda has proper permissions to access other AWS services
React App Can't Reach API Gateway
If your app can't reach the API Gateway:
- Verify the API Gateway URL is correct
- Check network tab in browser devtools for specific errors
- Ensure your API Gateway stage is properly deployed