top of page

Authorization in machine-to-machine integrations using Amazon Cognito

By Piotr Grzywa, Backend Engineer


Tech is one of the pillars of Kitopi’s success. One cannot imagine modern software systems without a cloud provider. So, it is for Kitopi’s case - we are using AWS, utilizing heavily services provided by Amazon. In this article I am going to show you how to set up an authorization for machine-to-machine integration in AWS, considering usage of Amazon Cognito as a service for authentication and authorization. In this article you will set up a mock resource behind API Gateway, then configure Cognito user pool supporting client credentials flow, and lastly connect it with the API. Examples provided have infrastructure’s code written in Terraform, so basic knowledge of the tool is needed to go through them. Article ends with a brief description of alternatives to authorization based on OAuth 2.0. 


Authorizing clients of M2M integration with OAuth2.0 

OAuth 2.0 protocol has a dedicated flow which is suitable for M2M scenarios where the client application is trusted and there is no user involvement in the authentication process. The flow (or grant as it is called in the protocol) is called Client Credentials. It provides a direct and efficient way for the client to obtain an access token from the authorization server and access protected resources. As with any OAuth 2.0 grant, the token expires within configured time, and (based on the implementation) offers an option to revoke it. However, it is important to ensure that the client credentials (client ID and client secret) are securely stored and transmitted. 


API Gateway setup 

To check the setup, we should prepare a resource (unprotected at the beginning) that we are going to share with the other side of the integration. We will achieve it by setting up a mock endpoint, which will return a successful message of welcome to the world every time it gets called. To achieve that, we need to set up multiple terraform resources as given below (use api_gateway.tf as filename). Once applied with terraform against your AWS infrastructure, you should see a new API called example-api and single resource /hello-world, returning a mocked response every time it gets called with the GET method. 

 

provider "aws" { 
 region = "eu-west-1" 
} 
 
resource "aws_api_gateway_rest_api" "example_api" { 
 name = "example-api" 
} 
 
resource "aws_api_gateway_resource" "example_resource" { 
 rest_api_id = aws_api_gateway_rest_api.example_api.id 
 parent_id   = aws_api_gateway_rest_api.example_api.root_resource_id 
 path_part   = "hello-world" 
} 
 
resource "aws_api_gateway_method" "example_method" { 
 rest_api_id   = aws_api_gateway_rest_api.example_api.id 
 resource_id   = aws_api_gateway_resource.example_resource.id 
 http_method   = "GET" 
 authorization = "NONE" 
} 
 
resource "aws_api_gateway_integration" "example_integration" { 
 http_method = aws_api_gateway_method.example_method.http_method 
 resource_id = aws_api_gateway_resource.example_resource.id 
 rest_api_id = aws_api_gateway_rest_api.example_api.id 
 type        = "MOCK" 
 
 request_templates = { 
   "application/json" = <<EOF 
 {"statusCode": 200} 
EOF 
 } 
} 
 
resource "aws_api_gateway_method_response" "example_method_response" { 
 http_method = aws_api_gateway_method.example_method.http_method 
 resource_id = aws_api_gateway_resource.example_resource.id 
 rest_api_id = aws_api_gateway_rest_api.example_api.id 
 status_code = "200" 
} 
 
resource "aws_api_gateway_integration_response" "example_integration_respnse" { 
 http_method = aws_api_gateway_method.example_method.http_method 
 resource_id = aws_api_gateway_resource.example_resource.id 
 rest_api_id = aws_api_gateway_rest_api.example_api.id 
 status_code = aws_api_gateway_method_response.example_method_response.status_code 
 
 response_templates = { 
   "application/json" = <<EOF 
{"message": "hello world!"} 
EOF 
 } 
} 
 
resource "aws_api_gateway_deployment" "example_deployment" { 
 rest_api_id       = aws_api_gateway_rest_api.example_api.id 
 stage_description = md5(file("api_gateway.tf")) 
 lifecycle { 
   create_before_destroy = true 
 } 
} 
 
resource "aws_api_gateway_stage" "example_stage" { 
 deployment_id = aws_api_gateway_deployment.example_deployment.id 
 rest_api_id   = aws_api_gateway_rest_api.example_api.id 
 stage_name    = "test" 
 depends_on    = [aws_api_gateway_deployment.example_deployment] 
} 

 

To test the setup, get the API URL by checking Stages > test > Invoke URL. While calling the endpoint you should get the similar response to this: 

 

 

AWS Cognito 

Now, we are going to set up an authorization mechanism protecting our super confidential resources. We will achieve it in two steps.

Firstly, we will build a user pool in Amazon Cognito. The service supports OAuth 2.0’s client credentials grant - so we can use it for our machine-to-machine integration. Within this user pool, we will not have any users, but app clients instead. We are also pointing out the resource server that will be protected by this authorization. Now we need to spin off a few resources with the help of Terraform to make it work. After applying you should see example-integrations user pool created. 

 

resource "aws_cognito_user_pool" "example_user_pool_integrations" { 
 name = "example-integrations" 
 account_recovery_setting { 
   recovery_mechanism { 
     name     = "admin_only" 
     priority = 1 
   } 
 } 
 admin_create_user_config { 
   allow_admin_create_user_only = true 
 } 
} 
 
resource "aws_cognito_user_pool_domain" "example_user_pool_integrations_domain" { 
 domain       = "example-user-pool-integrations" 
 user_pool_id = aws_cognito_user_pool.example_user_pool_integrations.id 
} 
 
resource "aws_cognito_user_pool_client" "example_user_pool_integrations_3rd_party_client" { 
 name                                 = "3rd-party" 
 user_pool_id                        
= aws_cognito_user_pool.example_user_pool_integrations.id 
 access_token_validity                = 24 
 allowed_oauth_flows_user_pool_client = true 
 allowed_oauth_flows                  = ["client_credentials"] 
 allowed_oauth_scopes                
= aws_cognito_resource_server.example_user_pool_integrations_resource_sereer.scope_identifiers 
 enable_token_revocation              = true 
 generate_secret                      = true 
} 
 
resource "aws_cognito_resource_server" "example_user_pool_integrations_resource_server" { 
 identifier   = format("%s%s", aws_api_gateway_deployment.example_deployment.invoke_url, aws_api_gateway_stage.example_stage.stage_name) 
 name         = "Mock API" 
 user_pool_id = aws_cognito_user_pool.example_user_pool_integrations.id 
 scope { 
   scope_name        = "hello-world" 
   scope_description = "Hello world endpoints" 
 } 
} 

 

As stated in configuration, Cognito generates a client secret for a specified app client – for the purpose of generating the token, we need to get the value of the secret. You can do so from the Amazon Cognito console. Open the user pool, then go to App Integration, at the bottom you should see 3rd-party app client - inside you should see the value of client id in plain text and client secret hidden behind show client secret toggle. 


What is left is connecting the previously created API with the user pool - we are achieving that by creating an authorizer and configuring the API with it. Let us get back to terraform with API gateway definition. Add authorizer for it: 

resource "aws_api_gateway_authorizer" "example_authorizer" { 
 name          = "example-authorizer" 
 rest_api_id   = aws_api_gateway_rest_api.example_api.id 
 type          = "COGNITO_USER_POOLS" 
 provider_arns = [aws_cognito_user_pool.example_user_pool_integrations.ar
] 
} 

And modify aws_api_gateway_method resource, by specifying alternative authorization type this time and linking it with the authorizer. 

resource "aws_api_gateway_method" "example_method" { 
 rest_api_id          = aws_api_gateway_rest_api.example_api.id 
 resource_id          = aws_api_gateway_resource.example_resource.id 
 http_method          = "GET" 
 authorization        = "COGNITO_USER_POOLS" 
 authorizer_id        = aws_api_gateway_authorizer.example_authorizer.id 
 authorization_scopes = aws_cognito_resource_server.example_user_pool_integrations_resource_server.scope_identifiers 
} 

After applying such changes, the response from API gateway should be different - indicating unauthorized access to the resource as we do not provide any authorization data. 

To gain the access we need to generate the token.

For this, let us hit the authorization server with basic authorization (specifying client id and secret) scheme, proper grant, and scope: 

$ curl -X POST --location "https://example-user-pool-integrations.auth.eu-west-1.amazoncognito.com/oauth2/token" \ 
-H "Authorization: Basic $(echo -n '<<client id>>:<<client secret>>' | base64)" \ 
-H "Content-Type: application/x-www-form-urlencoded" \ 
-d "grant_type=client_credentials&scope=https%3A%2F%2Fyour-invoke-url.execute-api.eu-west-1.amazonaws.com%2Ftest%2Fhello-world" 
 
{"access_token":"...","expires_in":86400,"token_type":"Bearer"}  

Get the JWT from the access_token field of the response and include it as Authorization header - as a result we are authorized to get the requested resource. We successfully configured authorization based on OAuth2 client credentials flow with the help of Amazon Cognito. 

$ curl -X GET --location "https://blwp57a3jd.execute-api.eu-west-1.amazonaws.com/test/hello-world" \ 
-H "Authorization: Bearer <<access token>>" 
 
{"message": "hello world!"} 

 

Documentation 

If you had a chance to get familiar with one of our earlier blog posts, written by Michał Łoza, you know that at Kitopi we utilize OpenAPI with its UI to document HTTP endpoints.

Specification supports OAuth2 as one of security schemes:  

"securitySchemes": { 
 "OAuth2": { 
   "type": "oauth2", 
   "name": "oauth2", 
   "flows": { 
     "clientCredentials": { 
       "tokenUrl": "https://example.auth.eu-west-1.amazoncognito.com/oauth2/token", 
       "scopes": { 
         "https://example.com/resource": "" 
       } 
     } 
   } 
 } 
} 

Similarly, Swagger UI provides a way to generate the token and authorize requests with OAuth2 

  

Other authorization methods to consider for M2M integrations  

Mutual TLS uses digital certificates to authenticate the client and the server in a communication exchange. With mTLS, both the client and the server must present valid and trusted certificates, ensuring strong mutual authentication. mTLS provides an elevated level of security and integrity by encrypting the communication channel and verifying the identity of both parties. It is well-suited for scenarios where strong and direct client-server authentication is required, without relying on third-party authorization servers or additional access tokens. Consider additional complexity around certificate provisioning, validity, revocation, and key management. Disable default endpoint so your API can be accessed through custom domain name only. For more information on how to configure mutual TLS in API gateway refer to this guide. 


Another option, widespread in the context of authorization in machine-to-machine communication is relying on an API key as an authentication token. This is considered insecure as the secret is shared with the resource server directly and it does not provide advanced security features like expiration or revocation. Usage of API keys should be limited to publicly available resources or playing supportive role for enforcing usage policies (throttling, billing). If usage of this type of authentication is constrained by some factors (strict rules imposed by client to be the most common), consider further protecting the resources (i.e., IP whitelisting). 

 

I hope you will find this article helpful when choosing appropriate mechanisms for authorization in machine-to-machine integrations. Worth noting is that authorization is only one of multiple aspects we should consider while protecting our APIs - other factors include IP whitelisting, rate limiting, monitoring and alerting to name a few. 

 

 

 

Comments


bottom of page