Controlling access in service-to-service communications with Cognito - Part 2
This is the second (and last) part of the secure service-to-service communication with Cognito mini-series. If you haven’t read Part 1, consider skimming through the post before continuing with this content.
1. The scenario - refresher
Bob has looked for a solution for authorizing requests between the microservices he and his team are developing. He decided to use Amazon Cognito and the client_credentials
grant type. This grant is specifically for controlling access in service-to-service communications and follows the OAuth 2.0 standards.
Last week Bob managed to set up everything Cognito, and he was able to get the access token in Postman.
He will now create a Cognito User Pool authorizer in API Gateway. Bob will then add it to the endpoint he wants to protect, and will define the required permission (scope) for the token validation.
2. API Gateway settings
For simplicity, let’s assume that Bob only has one endpoint in the API. After he has created more endpoints, he can follow the same steps to integrate API Gateway with Cognito for each endpoint. He could even use different authorization methods (for example, Lambda authorizers) for the each endpoint.
2.1. Creating the authorizer
First, we need to create an authorizer in API Gateway. Let’s call it s2s-authorizer
.
We have to select Cognito
for Type and specify the user pool.
2.2. Protecting the /files endpoint
Next, we should go to the Method Request on the GET /files
endpoint.
In the Authorization section, select the name of the Cognito authorizer (s2s-authorizer
).
After clicking the little tick icon, the API Gateway UI will automatically offer the OAuth Scopes section. We must specify the permission that API Gateway will look for in the access token. In this case, let the scope be demo/read.file
.
Our token will contain this scope (and demo/write.file
too), so the request to the endpoint should be successful.
2.3. Testing the endpoint in Postman
We can now test the endpoint in Postman.
In the Authorization tab, we select Bearer Token
and paste the access token we received from Cognito. Don’t write Bearer
in the box because Postman will convert it to Bearer Bearer <TOKEN>
, and it won’t work.
If we get the Invoke URL
from Stages in API Gateway and call the <INVOKE_URL>/files
endpoint with the access token in the header, we should get a successful response from the API.
3. axios call
Obviously, Bob won’t use Postman to get the token and invoke the API. Instead, one application will call the other service, so he will have to implement the same logic in a Lambda function.
The first application, which consists of the Lambda function, will invoke the URL of the API Gateway of the second service. For simplicity, we can create another Lambda function behind the endpoint. The endpoint handler can return some fake data. In reality, the integration will probably fetch some data from a database or perform business logic.
The caller service will use axios to make the request.
3.1. Fetching the token
We must convert the steps from Part 1 to get the access token from Cognito.
This part of the code can look like this:
interface CognitoClientCredentials {
access_token: string;
expires_in: number;
token_type: string;
}
const getCredentials = async (): Promise<CognitoClientCredentials> => {
const authUrl = `${COGNITO_DOMAIN}/oauth2/token`;
const authRequestBody = new URLSearchParams({
grant_type: "client_credentials",
});
const authParams = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${APP_CLIENT_ID}:${APP_CLIENT_SECRET}`
).toString("base64")}`,
},
};
const { data } = await axios.post<CognitoClientCredentials>(
authUrl,
authRequestBody,
authParams
);
console.log("Successfully received access token");
return data;
};
The getCredentials
function fetches the token from Cognito. COGNITO_DOMAIN
, APP_CLIENT_ID
, and APP_CLIENT_SECRET
are environment variables in the Lambda function, which will call the second service.
Content-Type
must be application/x-www-form-urlencoded
, so we must stringify the request body that contains the grant type.
3.2 Calling the other service
Now that we have the access token, we will place it in the Authorization
header of the second request.
The code can be the following:
export const handler = async () => {
try {
const { access_token: accessToken } = await getCredentials();
const { data: files } = await axios.get(FILES_SERVICE_URL, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
console.log("Successfully received data: ", files);
return files;
} catch (error) {
console.log("An error occurred", error);
throw error;
}
};
As we mentioned earlier, the access token, which contains the required scope (demo/read.file
) API Gateway needs, will be in the Authorization
header.
The request should succeed, and we can process the response.
4. Summary
API Gateway integrates with Cognito and validates access tokens based on their scopes. We can quickly set up a proof-of-concept for an internal service-to-service authorization pattern.
The key steps are to use grant_type: client_credentials
to fetch the access token from Cognito and set up the required scope for the Cognito authorizer in API Gateway.
5. Further reading
Github repo - The stack in CDK
Control access to a REST API using Amazon Cognito user pools as authorizer - More info on the topic in the documentation