Using AWS Lambda and API Gateway as a Cognito Callback URL
Overview
When implementing authentication with Amazon Cognito, you need to specify callback URLs for your application. Amazon Cognito has a limit of 100 callback URLs per user pool client, which becomes a challenge when dealing with multiple subdomains or environments, especially in multi-tenant applications where each tenant has its own subdomain.
The Callback URL Limit Problem
Cognito’s 100 callback URL limit can be quickly reached in scenarios such as:
- Supporting multiple subdomains (
app1.yourdomain.com,app2.yourdomain.com, etc.) - Different callback URLs for different buyers, clients, or auction applications.
Updated Solution
This solution uses a single API Gateway endpoint backed by a Lambda function that processes the callback and encrypts the authorization code. The encrypted code is then forwarded to the frontend, where the frontend exchanges the code for tokens. This approach bypasses the Cognito callback URL limit by dynamically handling subdomain-specific redirects.
Architecture
The solution uses the following AWS services:
- Amazon Cognito – For user authentication and management.
- API Gateway – Provides a single callback endpoint.
- Lambda Function – Processes the callback, encrypts the authorization code, and redirects to the target system.
Authentication Flow
-
User initiates login from the frontend
- The frontend redirects the user to the Cognito hosted UI with the following parameters:
client_id: Your Cognito app client ID.redirect_uri: API Gateway endpoint URL.state: Encoded information about the original domain and path.response_type:code.scope: Requested OAuth scopes.
-
User authenticates with Cognito
-
Cognito redirects to API Gateway with the authorization code
- API Gateway triggers the Lambda function.
-
Lambda function:
- Receives the callback with the
authorization codeandstate. - Encrypts the authorization code.
- Extracts the original domain and path from the
stateparameter. - Redirects the user to the original domain with the encrypted code in the query string.
- Receives the callback with the
-
Frontend exchanges the encrypted code for tokens.
- Frontend securely decrypts the code and exchanges it for tokens with Cognito.
- Tokens are stored securely (e.g., in
localStorageor cookies).
Federated login flow with the Api as a callback url
Frontend Implementation
// Frontend code example
async function exchangeCodeForToken(authCode) {
try {
const response = await fetch(`https://${COGNITO_DOMAIN}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code: authCode,
client_id: your_client_id,
client_secret: your_client_secret,
redirect_uri: `api_endpoint`,
grant_type: 'authorization_code',
}),
});
const data = await response.json();
if (data.access_token) {
// Store tokens and user info securely
localStorage.setItem('accessToken', tokens.access_token);
localStorage.setItem('idToken', tokens.id_token);
localStorage.setItem('refreshToken', tokens.refresh_token);
} else {
console.error('Token exchange failed:', data);
}
} catch (error) {
console.error('Error exchanging code:', error);
}
}
Lambda Implementation
const crypto = require('crypto');
const algorithm = 'aes-256-cbc';
const secretKey = process.env.SECRET_KEY;
const iv = crypto.randomBytes(16);
exports.handler = async (event) => {
const queryParams = event.queryStringParameters || {};
const code = queryParams.code;
const state = queryParams.state;
if (!code || !state) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Missing code or state' })
};
}
const originalDomain = JSON.parse(Buffer.from(state, 'base64').toString('utf-8')).tenant;
// Encrypt the code
const cipher = crypto.createCipheriv(algorithm, Buffer.from(secretKey, 'hex'), iv);
let encrypted = cipher.update(code);
encrypted = Buffer.concat([encrypted, cipher.final()]);
const encryptedCode = iv.toString('hex') + ':' + encrypted.toString('hex');
const redirectUrl = `${originalDomain}?code=${encodeURIComponent(encryptedCode)}`;
return {
statusCode: 302,
headers: {
Location: redirectUrl,
'Cache-Control': 'no-cache'
}
};
};
Security Considerations
- Token Security:
- Never expose tokens in URL parameters.
- State Parameter Security:
- Use a nonce and validate the state parameter to prevent CSRF.
- Encryption Best Practices:
- Use strong encryption algorithms (e.g., AES-256-CBC).
- Rotate encryption keys periodically.
- CORS and Domain Validation:
- Configure CORS headers properly.
- Validate allowed domains and enforce HTTPS.
Benefits
- Subdomain Flexibility:
- Single API Gateway endpoint for all subdomains.
- Centralized Logic:
- Token handling and state validation in a single Lambda function.
- Security:
- API Gateway adds an additional security layer.
- Tokens are exchanged only after encrypted validation.
- Maintenance:
- Easier to update and monitor a single callback handler.
Conclusion
This solution effectively bypasses the Cognito callback URL limit by implementing a centralized callback handler using API Gateway and Lambda. It provides a scalable and secure way to handle authentication for multiple subdomains or environments while maintaining security and user experience.
Key benefits:
- Eliminates the 100 callback URL limit
- Maintains security of the OAuth flow
- Provides flexibility for multi-tenant applications
- Scales automatically with AWS serverless architecture
