digitalspeed logo

Optimizing Security in AWS API-Gateway with Cognito API

Securing API_Gateway

It’s common for developers to extract user IDs directly from the API request body or parameters, but this is a security risk, even if API Gateway is integrated with Amazon Cognito

Amazon Cognito: Acts as a user identity and access management solution. It handles user registration, sign-in, and authentication. Cognito stores user data securely and provides features like social login integration and multi-factor authentication.

Amazon API Gateway: Functions as a single entry point for your application’s APIs. It allows you to manage, secure, and monitor APIs. API Gateway handles authorization by integrating with Cognito. It checks if a user has the necessary permissions (tokens) from Cognito to access specific API resources.

This post delves into strategies for optimizing security when enabling an API Gateway authorizer for a Cognito user pool. We’ll explore potential vulnerabilities, and mitigation techniques to ensure your APIs remain robust and user access stays controlled.

Let’s build a secure and scalable authorization layer for your backend and ensure users can access only their data.

Get to know what Amazon API-GateWay is – https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html

Do Not Use API keys as a Security Mechanism

Before I go deeper into the security of API Gateway, You should know that you should not use API keys as a security mechanism for API gateway, which I suppose most developers already know, here’s the statement from AWS documentation

‘API Gateway API Keys is not a security mechanism and should not be used for authorization unless it’s a public API. It should be used primarily to track a consumer’s usage across your API and could be used in addition to the authorizers previously mentioned in this section

Why you shouldn’t use user ID directly from API Request Body

Typically, when developers build their APIs with API Gateway and enable authorization with Cognito, they rely solely on Cognito to handle the authorization

They then pass the request body or parameters to their backend program, assuming the data returned, which might include the user ID, is valid. This data is then used to execute necessary commands to the database

Let’s go through the operation of the API gateway when integrated with Cognito

  1. it receives the user API request
  2. it confirms the token is present and checks if the token is valid using Cognito
  3. It sends the request to your backend (EC2, Lambda, Load balancer, etc)
  4. Your Lambda function receives the request and executes the command based on the user API data like the
    user ID passed in the API call, this command could return, update, create, or delete data for this
    ID (from API request body) on a database.

“To use an Amazon Cognito user pool with your API, you must first create an authorizer of the COGNITO_USER_POOLS type and then configure an API method to use that authorizer. After the API is deployed, the client must first sign the user into the user pool, obtain an identity or access token for the user, and then call the API method with one of the tokens, which are typically set to the request’s Authorization header. The API call succeeds only if the required token is supplied and the supplied token is valid, otherwise, the client isn’t authorized to make the call because the client did not have credentials that could be authorized” gotten from
– https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html

Integrate API-Gateway with Cognito Userpool

A bad actor (malicious user) can register an account on your website, access your API URL from the browser dev tool, access their valid access token from the browser dev tool, and make a request to your API with
another user ID to execute any of the CRUD processes on your database, they just need to get the user
ID or possibly make a random search for any possible user ID.

This is a serious breach of user security, the API call can be carried out in various ways PostMan, ThunderClient, or programmatically. This process is not very intuitive for many programmers

Invoke Rest API gateway with Cognito userpool

API gateway integration with Lambda cognito
API-Gateway integration with Lambda, Cognito

Below I share two ways you can prevent this issue;

  • Accessing the user ID from the access token after API Gateway validation
  • Use a Lambda authorizer function

Accessing the User ID from the access token after API GateWay Validation

You should get the user ID from the access token rather than from the API request Body or parameter, You might not necessarily have to validate the user token again because that has been done by API gateway

When you set up a Lambda proxy request, API-Gateway passes the API request data, headers, body, parameters,
query, and method to your backend server. Hence you should set a Lambda proxy for API Gateway to pass the request headers to the backend instance

API Gateway runs at scale, it is serverless so it can handle a large number of API requests, and it can easily filter all the non-authenticated API requests passing only authenticated requests to your backend hence
reducing the load on your backend servers.

Check out code examples for calling API gateway

Here’s a simple code to decode the user ID from the user access token

//get the token from the authorizer header
const token = req.headers.authorization && req.headers.authorization.split(" ")[1];
if (!token) { return res.status(400).json({ error: "User need a token to access the API" }); }
const decodedHeader = jwt.decode(token, { complete: true });
const signingKey = await getKey(decodedHeader.header);

//get the user ID
decodedProfile = new Promise((resolve, reject) => {
jwt.verify(
token,
signingKey,
{
aud: APP_CLIENT_ID,
issuer: https://cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID},
algorithms: ["RS256"],
},
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
}
);
});
profileId = decodedId.sub

From the code above you can get the user ID from the authentication token using the ‘aws-jwt-verify’ library

You can then use the user ID for the actions to be executed on the user data rather than the ID passed on to the request

Using a Lambda Authorizer function

A Lambda authorizer function is one that is invoked through the API gateway Authorizer when users try to access a route on your API, the Lambda function receives the user authentication token, processes the Token, and then returns a valid response to the API Gateway which allows the request to go through

Lambda Authorisers were originally included for users to allow custom API gateway authorization when they have
an external Identity Provider (IdP), then they can use an API Gateway Lambda authorizer to invoke a Lambda function to authenticate or validate a given user against their IDP.

You can use a Lambda authorizer for custom validation logic based on identity metadata like in this case retrieving a user ID from the token and returning it back to API Gateway which will be sent to your backend.

A Lambda authorizer can send additional information derived from a bearer token or request context values
to your backend service. For example, the authorizer can return a map containing user IDs, usernames, and scope.

By using Lambda authorizers, your backend does not need to map authorization tokens to user-centric data, allowing you to limit the exposure of such information to just the authorization function.

Note that you can use a Lambda authorizer from another AWS account to carry out the authorization this makes it easy to centrally manage and share a central Lambda authorizer function across multiple API Gateway APIs.

learn how to use a Lambda authorizer from another account

Setting up a Lambda authorizer using an API gateway is quite easy, follow the documentation to do that – https://docs.aws.amazon.com/apigateway/latest/developerguide/configure-api-gateway-lambda-authorization.html

API token source header name is usually sent as Auth2 Bearer token, so use ‘authorization’ as the source header name

You can enable Token caching for a TTL and also REGEX expression to validate token expression
The docs referenced above are passed to the Lambda authorizer function. The lambda function verifies the token and response to API Gateway with an allow or deny response.

Your Lambda Authoriser should not do anything else than validate the access token in whichever method you want
and note that the API gateway does not pass the body or parameters to the Lambda AUthoriser

API gateway sends the request to your backend if the lambda authorizer function replies with an allow, you should also allow API gateway to send the access token to the Lambda function when you set the Lambda proxy request

On your Backend server or backend Lambda function, you should first get the user ID of the authorized user from the token rather than from the user ID from the API request as body, parameter, or query.

//get the token from the authorizer header
const token = req.headers.authorization && req.headers.authorization.split(" ")[1];
if (!token) { return res.status(401).json({ error: "User need a token to access the API" }); }
const decodedHeader = jwt.decode(token, { complete: true });
const signingKey = await getKey(decodedHeader.header);

decodedProfile = new Promise((resolve, reject) => {
jwt.verify(
token,
signingKey,
{
aud: APP_CLIENT_ID,
issuer: https://cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID},
algorithms: ["RS256"],
},
(err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
}
);
});
profileId = decodedId.sub

Your Lambda function should return the user ID and other data from the authentication to the API Gateway which will be returned to your backend.

Now you have the user profile ID and you use that to carry out operations (GET, PUT, POST, DELETE)
because now you are sure the operation is coming from a user of the ID and is being executed on the authenticated user data.

Note that you only need to remove the user ID from the body because that’s the data you shouldn’t trust, you can use other data on the body or parameter.

Check out the doc for verification of Cognito ID without necessarily using Cognito API

Check out other referenced documentation

Tell me of other ways you would improve your API and what other security concern I did not touch

Conclusion

You’ve fortified your backend by leveraging API Gateway and authorization strategies, ensuring
only authorized users can access specific data. Now, unauthorized attempts are blocked, safeguarding
user data and accounts

Please share this article at the top of the page under the share this button. Thanks

Recent Post

Send Us A Message

Related Post

Join our newsletter to stay updated

digitalspeed-logo

At DIGITALSPEED, you can get updates, reviews and learn about new digital tools and features on existing tools. check us on social media.

Get In Touch

Lagos, Nigeria

DIGITALSPEED © All Rights Reserved.

2024

Scroll to Top

Seach for Articles