Fine-grained authorization with Amazon Cognito user pool and API Gateway


Shifting from monolith architecture to microservices are full of challenges where one primary is how to manage security and access control properly. As each microservice is running indenpendly and communicates with each other to perform a specific function, we need to authenticate the incoming request to ensures that it comes from a legitimate service.

This post shows how we implemented a fine-grained system-to-system authorization for microservices application using Amazon Cognito user pool and API Gateway.

 

Solution


Amazon Cognito is a powerful service for application authentication, authorization, and user management. The idea here is to use its user directory service User Pools to authenticate calls between microservices as API Gateway is natively integrated with it to validate identities of consumer.

Architecture diagram: auth_flow

 

Key components

There are three key components here:

  • Cognito Resource server: Each service has a dedicated resource server with pre-defined scopes for its resources(API method, Lambda etc)
  • Cognito App client: Each service has a dedicated app client with limited scopes it needs to access external resources
  • API GW Cognito user pool authorizer: Each API resource has an Cognito authorizer configured for authenciation and authorization

 

Cognito Resource server


Resource server is what we can use to manage the scopes that are required for accessing our REST resources. For example, we have scope customer.read from resouce server customers-v1-resource-server for resource /v1/customers/{customerId} in service customers-v1, indicating that to access the resource a JWT token with this scope is required in the request.

Resource server definition:

  CognitoUserPoolResourceServer:
    Type: 'AWS::Cognito::UserPoolResourceServer'
    DeletionPolicy: Retain
    Properties:
      Identifier: 'customers-v1-resource-server'
      Name: 'customers-v1-resource-server'
      Scopes:
        - ScopeDescription: 'Customer Read Scope' # to be used in below method resource
          ScopeName: 'customer.read'
      UserPoolId: '1ds23esfa'

API method resource definition:

 CustomerGet:
    Type: 'AWS::ApiGateway::Method'
    Properties:
        HttpMethod: GET
        RestApiId: !Ref: TestApi
        ResourceId: !Ref: CustomerResource # /v1/customers/{customerId}
        AuthorizationType: COGNITO_USER_POOLS
        AuthorizerId: !Ref: CognitoUserPoolAuthorizer # defined below
        AuthorizationScopes:
            - 'customers-v1-resource-server/customer.read' # scope from resource server above
        Integration: ...

 

Cognito App client


App client is where we define the auth flow, and where we specify the allowed scopes for the current service to access external resources. for example, we have app client accounts-v1-app-client with allowed scope customers-v1-resource-server/customer.read(a resource server and scope combination from the target service), indicating that accounts-v1 service can only access resource /v1/customers/{customerId} in service customers-v1.

CognitoUserPoolClient:
    Type: 'AWS::Cognito::UserPoolClient'
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Properties:
        AllowedOAuthFlows:
            - 'client_credentials'
        AllowedOAuthScopes:
            - 'customers-v1-resource-server/customer.read' # allowed custom scopes
        AllowedOAuthFlowsUserPoolClient: true
        ClientName: 'accounts-v1-app-client'
        ExplicitAuthFlows:
        - ALLOW_REFRESH_TOKEN_AUTH
        GenerateSecret: true
        UserPoolId: 1ds23esfa

 

API GW Cognito user pool authorizer


When receiving an API call request, the authorizer will authenticate the request by checking whether an access token is supplied and whether it’s valid, after that it will authorize the call based on the specified custom scopes for the access-protected resources. For example, a request with access token having customers-v1-resource-server/customer.read scope will pass the authentication and will be authorized to access resource /v1/customers/{customerId} in service customers-v1.

CognitoUserPoolAuthorizer:
    Type: 'AWS::ApiGateway::Authorizer'
    Properties:
    AuthorizerResultTtlInSeconds: 300
    IdentitySource: 'method.request.header.Authorization'
    Name: 'cognito-user-pool-authorizer'
    RestApiId: !Ref ApiGatewayRestApi
    Type: COGNITO_USER_POOLS
    ProviderARNs:
        - 'arn:aws:cognito-idp:4234234234:324423523525:userpool/1ds23esfa'

 

Challenges

  1. Deployment dependency

This pattern introduces a service dependency which requires target service resources and scopes to be created/updated first prior to the consumer service. For example, service accounts-v1 can not be deployed unless the relied scope has been created by deploying service customers-v1.

Solution: use release command in Bamboo-on-Teams to deploy services in sequential batches

  2. App client credentials management

The service-to-service interaction starts with a user pool sign-in with the app client credentials and a JWT token will be returned from Cognito to the initiator for external resource access. We are creating the secure parameters in Amazon SSM manually to store the client credentials for each microservice after the deployment. With the increasing number of µservices, we need a tool to do this securely and automatically for us.

Solution: https://www.serverless.com/plugins/serverless-app-client-credentials-to-ssm

 

Conclusion


With this fine-grained access control mechanism, we now can guarantee that only authenticated services will be allowed to access the resource, and only resources with specified scopes can be accessed by the service.

 
 


See also