Authorization sử dụng Amazon Cognito, API Gateway và IAM (Phần 1)

Lời nói đầu

Chào các bạn, mình là Duy Nam - Solution Architect Engineer VTI Japan. Hôm nay mình xin giới thiệu về Authorization sử dụng Amazon Cognito, API Gateway và IAM.
Việc phân quyền người dùng dựa trên một Group Membership là một best practice. Nếu bạn đang tạo API sử dụng Amazon API Gateway và bạn cần control chi tiết access cho người sử dụng của bạn, bạn có thể dùng Amazon Cognito.
Amazon Cognito cho phép bạn sử dụng group để tạo một nhóm người dùng, và dựa trên group này để phân quyền cho người sử dụng.
Trong bài viết này mình sẽ chỉ cho các bạn làm sao để tạo việc phân quyền chi tiết để bạo vệ API sử dụng Amazon Cognito, API Gateway và IAM.

Nội dung bài viết

  1. Solution Overview
  2. Tạo API Service không sử dụng Authorizer
  3. Tạo API Service sử dụng Authorizer

I. Solution Overview

Để đến với bài viết này, bạn phải chuẩn bị trước về kiến thức xác thực người dùng sử dụng JWT (JSON Web Token). Hoặc bạn chờ bài viết tiếp theo mình sẽ giới thiệu phần này nhé.

1. Architecture Overview

OverView

2. User request flow

  1. User login và lấy Amazon Cognito JWT ID Token, access token và refresh token

  2. RestAPI request được tạo ra và kèm theo một token - trong giải pháp lần này một access token sẽ được đưa vào header.

  3. API Gateway sẽ forward request đến Lambda authorizer (Ủy quyền) - sẽ hiểu là một Custom Authorizer

  4. Lambda authorizer sẽ xác nhận Amazon Cognito JWT sử dụng Amazon Cognito public key. Lần đầu khi lambda được gọi, public key sẽ được tải xuống và được cache lại. Các lần gọi sau sẽ sử dụng public key từ cache.
    Note: Để tối ưu hóa Lambda authorizer, authorization policy có thể được lưu vào cache hoặc không tùy thuộc vào nhu cầu của bạn.

  5. Lambda authorizer sẽ tìm kiếm Amazon Cognito Group mà người dùng thuộc về trong JWT và thực hiện tra cứu trong Amazon DynamoDB để lấy policy được ánh xạ tới nhóm.

  6. Lambda sẽ trả về policy, optionally (ngữ cảnh), tùy chọn (context) cho API Gateway. Context là một map nội dung dang key-value để bạn có thể pass đến các service phía sau. Nó có thể được thêm thông tin về user, service hoặc bất cứ điều gì mà người phát triển đưa vào và chuyển đến các service phía sau.

  7. Dựa trên API Gateway policy engine đánh giá policy nhận được.
    Note: Lambda không chịu trách nhiệm hiểu và đánh giá policy. Trách nhiệm đó thuộc về phạm vi của API Gateway.

  8. Yêu cầu được chuyển tiếp đến service tiếp theo.

Chúng ta hãy xem kỹ hơn một ví dụ về policy được lưu trữ trong DynamoDB

{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Sid":"PetStore-API",
         "Effect":"Allow",
         "Action":"execute-api:Invoke",
         "Resource":[
            "arn:aws:execute-api:*:*:*/*/*/petstore/v1/*",
            "arn:aws:execute-api:*:*:*/*/GET/petstore/v2/status"
         ],
         "Condition":{
            "IpAddress":{
               "aws:SourceIp":[
                  "192.0.2.0/24",
                  "198.51.100.0/24"
               ]
            }
         }
      }
   ]
}

3. IAM policy sẽ được đánh giá dựa trên API Gateway

Dựa trên chính sách mẫu này, người dùng được phép thực hiện các cuộc gọi tới API petstore.
Đối với phiên bản v1, người dùng có thể đưa ra yêu cầu đối với bất kỳ động từ nào và bất kỳ đường dẫn nào, được thể hiện bằng dấu hoa thị (*).
Đối với v2, người dùng chỉ được phép đưa ra yêu cầu GET cho đường dẫn / trạng thái.

II. Tạo API Service không sử dụng Authorizer

1. Chuẩn bị trước

Bạn cần chuẩn bị trước các nội dung sau đây:

  • Install AWS Command Line Interface (CLI) và configure để sử dụng.
  • Python3.6 trở đi để package code cho Lambda.
  • Một IAM Role hoặc user với đầy đủ quyền để tạo Amazon Cognito User Pool, IAM Role, Lambda, IAM Policy, API Gateway và DynamoDB table.
  • Lambda source code và Cloudformation source code mình sẽ để phía cuối bài viết.

2. Tạo Lambda Service và API Gateway, Deploy Resource không có Authorizer

2.1 Architecture Overview

Architecture Overview

2.2 Tạo PetAPI cho người dùng

  • Lambda source code (lambda.py)
import json

pets = [
    {
        "id": 1,
        "name": "Birds"
    },
    {
        "id": 2,
        "name": "Cats"
    },
    {
        "id": 3,
        "name": "Dogs"
    },
    {
        "id": 4,
        "name": "Fish"
    }
]

def handler(event, context):
    print(event)

    try:
        path = event['path']
        http_method = event['httpMethod']

        if path == '/petstore/v1/pets' and http_method == 'GET':
            return response_handler({'pets': pets}, 200)
        elif '/petstore/v1/pets/' in path and http_method == 'GET':
            pet_id = path.split('/petstore/v1/pets/')[1]
            for pet in pets:
                if pet['id'] == int(pet_id):
                    return response_handler(pet, 200)
        elif path == '/petstore/v2/pets' and http_method == 'GET':
            return response_handler({'pets': pets}, 200)
        elif path == '/petstore/v2/status':
            return response_handler({'status': 'ok'}, 200)
        else:
            return response_handler({}, 404)

    except Exception as e:
        print(e)
        return response_handler({'msg': 'Internal Server Error'}, 500)

def response_handler(payload, status_code):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": json.dumps(payload),
        "isBase64Encoded": False
    }
  • Tiến hành Package Lambda
mkdir -p cf-lambdas
cd ./Lambda/pets-api
zip pets-api.zip lambda.py && mv pets-api.zip ../../cf-lambdas
cd ./../..
  • Tạo S3 lưu trữ Lambda
CF_STACK_NAME="cognito-api-gateway"
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
STACK_REGION=$(aws configure get region)
S3_BUCKET_NAME="${CF_STACK_NAME}-${ACCOUNT_ID}-${STACK_REGION}-lambdas"

aws s3api create-bucket \
      --bucket "${S3_BUCKET_NAME}" \
      --region "${STACK_REGION}" \
      --create-bucket-configuration LocationConstraint="${STACK_REGION}" > /dev/null
  • Upload source lên S3
aws s3 cp ./cf-lambdas/pets-api.zip s3://$S3_BUCKET_NAME

2.3 Sử dụng Cloudformation để triển khai Lambda Service và API Gateway

  • Cloudformation source code (api-resource.yaml)
AWSTemplateFormatVersion: 2010-09-09

#=====================================================================#
# Input Block
#=====================================================================#
# Parameters:

#=====================================================================#
# Resource Block
#=====================================================================#
Resources:
  #================================================================#
  # PetAPI Resource
  #================================================================#
  #----------------------------------------------#
  # IAM Policy for PetAPI Lambda
  #----------------------------------------------#
  ApiServiceIAMPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      Roles:
        - !Ref ApiServiceIAMRole
      PolicyName: ApiServiceIAMPolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/ApiServiceLambdaFunction:*

  #----------------------------------------------#
  # IAM Role for PetAPI Lambda
  #----------------------------------------------#
  ApiServiceIAMRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: ApiServiceIAMRole
      AssumeRolePolicyDocument: |-
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Effect": "Allow",
              "Sid": ""
            }
          ]
        }

  #----------------------------------------------#
  # PetAPI Lambda LogGroup
  #----------------------------------------------#
  ApiServiceLambdaLogGr:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Sub /aws/lambda/ApiServiceLambdaFunction
      RetentionInDays: 7

  #----------------------------------------------#
  # PetAPI Lambda Function
  #----------------------------------------------#
  ApiServiceLambdaFunction:
    DependsOn:
      - ApiServiceLambdaLogGr
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: ApiServiceLambdaFunction
      Runtime: "python3.6"
      Handler: "lambda.handler"
      Role: !GetAtt ApiServiceIAMRole.Arn
      Code:
        S3Bucket: !Sub ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}-lambdas
        S3Key: "pets-api.zip"

  #----------------------------------------------#
  # PetAPI Lambda Permission
  #----------------------------------------------#
  ApiServiceLambdaFunctionPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt ApiServiceLambdaFunction.Arn
      Principal: "apigateway.amazonaws.com"

  #================================================================#
  # API Gateway Resource
  #================================================================#
  #----------------------------------------------#
  # Create API Gateway with RestApi type
  #----------------------------------------------#
  ApiGatewayRestApi:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: "MyApiGateway"

  #----------------------------------------------#
  # Create API Gateway Resource
  #----------------------------------------------#
  ApiGatewayResource:
    Type: 'AWS::ApiGateway::Resource'
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
      PathPart: "{api+}"

  #----------------------------------------------#
  # Create API Gateway Method
  #----------------------------------------------#
  ApiGatewayMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      HttpMethod: "ANY"
      ResourceId: !Ref ApiGatewayResource
      RestApiId: !Ref ApiGatewayRestApi
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: "POST"
        Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiServiceLambdaFunction.Arn}/invocations

  #----------------------------------------------#
  # Deploy API Gateway
  #----------------------------------------------#
  ApiGatewayDeploymentUnProtected:
    DependsOn:
      - ApiGatewayMethod
    Type: AWS::ApiGateway::Deployment
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      StageName: dev
      Description: unprotected api 

#=====================================================================#
# Output Block
#=====================================================================#
Outputs:
  ApiGatewayDeploymentUrlApiEndpoint:
    Value: !Sub https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/dev/petstore/v1/pets
  ApiGatewayDeploymentUrlApiEndpointV2:
    Value: !Sub https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/dev/petstore/v2/pets
  • Tạo Service sử dụng Cloudformation (Các bạn chú ý đường dẫn của file yaml)
CF_STACK_NAME="cognito-api-gateway"
S3_BUCKET_NAME="${CF_STACK_NAME}-${ACCOUNT_ID}-${STACK_REGION}-lambdas"

aws cloudformation deploy --template-file ./infrastructure/api-resource.yaml \
     --stack-name $CF_STACK_NAME \
     --s3-bucket $S3_BUCKET_NAME \
     --s3-prefix cfn \
     --capabilities CAPABILITY_NAMED_IAM

2.4 Tiến hành test API đã được tạo ra

  • Get API URL
API_URL=$(aws cloudformation describe-stacks \
    --stack-name ${CF_STACK_NAME} \
    --query 'Stacks[0].Outputs[1].OutputValue' --output text)

echo "${API_URL}"
  • Sử dụng Postman và API URL gọi đến api_v1 và api_v2

    • api-v1
      api-v1-request-success

    • api-v2
      api-v2-request-success

    • Ở đây chúng ta thấy cả 2 api đều có thể gọi được mà không yêu cầu bất cứ thông tin đăng nhập nào.

Kết luận

Bài viết này mình đã giới thiệu cho các bạn sử dụng Cloudformation để tạo một API Gateway và Lambda Service. Từ đó các bạn có thể mở rộng ra để thực hiện xử lý logic phía sau Lambda
Ở bài viết sau mình sẽ tiến hành đưa nội dung Authorization vào API mà chúng ta vừa tạo nên. Authorization sử dụng Amazon Cognito, API Gateway và IAM (Phần 2)
Hi vọng bài viết có ích cho các bạn trong công viêc và quá trình học tập về AWS.

Leave a Reply