Securing Serverless HTTP APIs: Integrating Amazon Cognito with Custom Authorizers
In our previous article, we explored the basics of custom authorizers for API Gateway HTTP APIs. Today, we’ll expand on that knowledge by integrating Amazon Cognito, adding robust user management and authentication to our serverless HTTP API.
Introduction
While custom authorizers provide flexibility in API security, managing user authentication can be complex. Amazon Cognito simplifies this process by handling user sign-up, sign-in, and access control. By combining Cognito with custom authorizers for HTTP APIs, we can create a powerful, scalable authentication system.
What We’ll Cover
1. Brief recap of custom authorizers for HTTP APIs
2. Introduction to Amazon Cognito
3. Setting up Cognito User Pools for HTTP API integration
4. Implementing a custom authorizer for Cognito and HTTP APIs
5. Configuring HTTP API routes with the custom authorizer
6. Implementing role-based access control
7. Best practices and security considerations
Quick Recap: Custom Authorizers for HTTP APIs
Custom authorizers for HTTP APIs are Lambda functions that control access to your API endpoints. They run before your main API function and can implement complex authorization logic.
Introducing Amazon Cognito
Amazon Cognito is a fully managed service that provides:
- User sign-up and sign-in
- Token-based authentication
- Integration with social identity providers
- Scalable user management
By using Cognito, we can offload much of the authentication complexity from our custom authorizer.
Setting Up Cognito User Pools
Let’s set up a Cognito User Pool in our serverless.yml:
resources:
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ${self:service}-${sls:stage}-user-pool
UsernameAttributes:
- email
AutoVerifiedAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: true
RequireUppercase: true
Schema:
- Name: role
AttributeDataType: String
Mutable: true
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
ClientName: ${self:service}-${sls:stage}-user-pool-client
UserPoolId:
Ref: CognitoUserPool
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
PreventUserExistenceErrors: ENABLED
RefreshTokenValidity: 30
AccessTokenValidity: 1
IdTokenValidity: 1
TokenValidityUnits:
RefreshToken: DAYS
AccessToken: HOURS
IdToken: HOURS
This configuration creates a User Pool where users can sign up with their email, sets a strong password policy, and adds a custom ‘role’ attribute. It also sets up a User Pool Client with specific authentication flows and token validity periods.
Implementing a Custom Authorizer for Cognito and HTTP APIs
Here’s our custom authorizer implementation:
const { CognitoJwtVerifier } = require("aws-jwt-verify");
const AWS = require("aws-sdk");
const COGNITO_USER_POOL_ID = process.env.COGNITO_USER_POOL_ID;
const COGNITO_CLIENT_ID = process.env.COGNITO_CLIENT_ID;
const verifier = CognitoJwtVerifier.create({
userPoolId: COGNITO_USER_POOL_ID,
tokenUse: "access",
clientId: COGNITO_CLIENT_ID,
});
async function canAccess(token, resource) {
try {
const payload = await verifier.verify(token);
console.log("Token is valid. Payload:", payload);
return payload;
} catch (error) {
console.log("Token not valid!");
console.error(error);
return null;
}
}
module.exports.customAuthorizer = async (event, context) => {
let response = {
isAuthorized: false,
};
const accessToken = event.headers["authorization"];
console.log("event.rawPath ::: ", event.rawPath);
let verifiedData = await canAccess(accessToken, event.rawPath);
const cognito = new AWS.CognitoIdentityServiceProvider({
apiVersion: "2016-04-18",
});
if (verifiedData) {
try {
var params = {
AccessToken: accessToken /* required */,
};
const thisUser = await cognito.getUser(params).promise();
const userAttributes = {};
if (thisUser.UserAttributes) {
thisUser.UserAttributes.forEach((UserAttribute) => {
console.log("UserAttribute ::: ", UserAttribute);
userAttributes[UserAttribute.Name] = UserAttribute.Value;
});
}
verifiedData["userAttributes"] = userAttributes;
console.log("verifiedData ::: ", verifiedData);
response = {
isAuthorized: true,
context: verifiedData,
};
} catch (error) {
console.error("Error getting user data:", error);
}
} else {
console.log("Token verification failed");
}
return response;
};
This authorizer verifies the Cognito token, fetches additional user attributes, and returns an authorization response with user information in the context.
Configuring HTTP API Routes with the Custom Authorizer
Update your serverless.yml to use the new authorizer with HTTP API routes:
provider:
name: aws
runtime: nodejs14.x
httpApi:
authorizers:
cognitoAuthorizer:
type: request
functionName: customAuthorizer
identitySource: $request.header.Authorization
enableSimpleResponses: true
functions:
customAuthorizer:
handler: src/auth/customAuthorizer.handler
environment:
COGNITO_USER_POOL_ID: !Ref CognitoUserPool
COGNITO_CLIENT_ID: !Ref CognitoUserPoolClient
getUsers:
handler: src/handlers/getUsers.handler
events:
- httpApi:
path: /users
method: GET
authorizer:
name: cognitoAuthorizer
This configuration sets up the custom authorizer and applies it to the /users GET route.
Implementing Role-Based Access Control
With the user’s attributes available in the authorizer context, you can implement role-based access control in your API functions. Here’s an example of how you might use this in a Lambda function:
exports.handler = async (event) => {
const authorizer = event.requestContext.authorizer;
// Access user information from the context
const userInfo = authorizer.lambda;
// Access user attributes
const userAttributes = userInfo.userAttributes;
// Assuming you have a 'custom:role' attribute in your user pool
const userRole = userAttributes['custom:role'];
if (userRole !== 'admin') {
return {
statusCode: 403,
body: JSON.stringify({ message: 'Access denied' })
};
}
// Process the request for admin users
// ...
};
This function checks the user’s role and only allows access if the user is an admin. Remember to set up the ‘custom:role’ attribute in your Cognito User Pool and assign roles to your users.
Best Practices and Security Considerations
1. Token Handling: Always use access tokens for API authorization. Never send refresh tokens to your API.
2. HTTPS: Ensure all communication with your API and Cognito is over HTTPS.
3. Least Privilege: Assign the minimum required permissions to your Lambda functions and API Gateway.
4. Regular Audits: Regularly review your Cognito and API Gateway settings for any security vulnerabilities.
5. Monitoring: Set up CloudWatch alarms to alert you of unusual activity, such as a high number of failed authentications.
6. Token Expiration: Adjust token expiration times based on your security requirements. Shorter-lived tokens are generally more secure but may impact user experience.
Conclusion
By integrating Amazon Cognito with custom authorizers for HTTP APIs, we’ve enhanced our API’s security while simplifying user management. This approach provides a scalable, managed authentication service combined with the flexibility of custom authorization logic, specifically tailored for HTTP APIs.
Remember, security is an ongoing process. Regularly review and update your authentication and authorization mechanisms to protect against evolving threats.
Have you integrated Cognito with your HTTP APIs? What challenges did you face? Share your experiences in the comments!
Further Reading
- Previous Article: Custom Authorizers in API Gateway HTTP APIs
- Amazon Cognito Developer Guide
- Amazon API Gateway — Working with HTTP APIs
- JSON Web Tokens Introduction
Keywords: AWS, Serverless, Cognito, Custom Authorizer, HTTP API, API Gateway, Lambda, Security, Authentication, Authorization, Token Verification
Related AWS Services: Amazon Cognito, AWS Lambda, Amazon API Gateway (HTTP APIs), AWS Identity and Access Management (IAM)