API GatewayでIAM認証を行う

Pocket

こんにちは、DX推進室の安齋です。

今回はAWSサーバーレスリソースを構築した中で、ハマった箇所や工夫した箇所を全6回に分けてご紹介します。

第1回は全体構成とAPI GatewayでIAM認証を行った時にハマった箇所の話をします。

対象読者

  • AWSサーバーレスリソースを作りたい方
  • CloudFormationを使っている方
  • SAMを使っている方

全体構成

全体構成は以下の通りです。

  1. ユーザーは一般と管理者に分かれていて、一つのCognitoユーザープール内の一般グループと管理者グループにそれぞれ所属している
  2. Cognitoアプリクライアントを使用して、認証前ユーザーがログインやパスワード変更をできるようにする
  3. Cognito IDプールを使用して、Cognitoユーザープールによって認証されたユーザーに、自身が所属するグループに紐づくIAMロールの一時的認証情報を渡す
  4. フロントエンドコンテンツはS3をオリジンとして、CloudFrontが配信している
  5. APIは一般用と管理者用の二つがあり、一般ユーザーは一般用のみを呼び出すことがき、管理者ユーザーは一般用と管理者用を呼び出すことができる
  6. 一般用API GatewayはCognitoオーソライザーを使用している
    • CognitoオーソライザーはCognitoユーザープールによって認証されたユーザーに、IDトークンを発行している
  7. 一般用APIはCloudFrontが配信している
  8. 管理者用API GatewayはIAM認証を使用している
  9. 管理者用APIはAPI Gatewayが配信している

serverless-1-1

今回のテーマ

上記構成図の赤枠内の管理者用API GatewayにIAM認証をかけて、管理者ユーザーのみが管理者用APIを呼び出せるようにします。

なぜ管理者用APIをCloudFrontで配信しなかったか

API GatewayのIAM認証は一時的認証情報を使用した呼び出しになります。
そのため、Cognitoユーザープールによって発行されたIDトークンを、Authorizationリクエストヘッダに付与するCognitoオーソライザーとはリクエスト内容が異なります。具体的には一時的認証情報を使用して、Hostリクエストヘッダ等に署名をするのですが、CloudFrontがHostヘッダをオリジンに転送する際に、Hostヘッダは書き換えても、署名までは書き換えないため、API Gatewayは不正な署名として扱ってしまうのです。

これを回避するためには、

  1. CloudFrontとAPI Gatewayに同じカスタムドメインを当てる
  2. CloudFrontにLambda Edgeを設置して署名を書き換える

の二通りの方法があります。今回は工数の都合上、管理者用APIを直接API Gatewayから配信することにしました。

参考資料

  • 署名バージョン 4 の署名プロセス
    • https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html
  • WebSocket API Gateway の前にCloudFrontを置く
    • https://dev.classmethod.jp/articles/cloudfront-in-front-on-websocket-api-gateway/
  • Does API Gateway behind CloudFront not support AWS_IAM authentication?
    • https://stackoverflow.com/questions/48815143/does-api-gateway-behind-cloudfront-not-support-aws-iam-authentication

ハマった箇所

まず、API GatewayでCognitoオーソライザーとIAM認証を併用して、CloudFrontで一般用と管理者用APIを配信する方法の調査から始め、「なぜ管理者用APIをCloudFrontで配信しなかったか」に行きつくまでに時間がかかりました。

どうやらこのままではCloudFrontで配信ができないらしい、と分かった後、どうやって署名付きリクエストを送るのか?ということにも少し時間がかかりました。

  • 解決方法はこちら
    • [Python] Boto3以外でV4署名リクエストを行う
      • https://dev.classmethod.jp/articles/python-v4-signature-without-boto3/

さて、無事にAPI Gatewayにリクエストを送ることができたのですが、無慈悲な500エラーが返り、しかもAPI Gatewayの後段のLambdaにはリクエストが届いていないことが判明しました。

{"status":500, "message":"Internal Server Error"}

原因は、Cognitoユーザープールの管理者グループに紐づくIAMロールに、管理者用APIのLambda起動権限がないことでした。SAMを使用してAPI GatewayとLambdaを構築する場合、Lambdaパーミッションを明示的に記載せずに、API GatewayからLambdaを起動できるのですが、IAM認証の場合はユーザーにAPI Gateway起動権限の他、Lambda起動権限が必要でした。API Gatewayからは403が返ってきても良さそうなものが、500で返ってきてたので何が起きているのか直ぐには分かりませんでした。

以降、今回のテーマに該当するSAMとCloudFormationの記載となります。今回のポイントとなる箇所に ★★★ポイント★★★ を付けています。

SAMテンプレート

管理者用API GatewayとLambdaの箇所を抜粋します。

API GatewayのCloudWatch Logsの設定にもハマったので、次回以降のテーマとして扱い、その箇所のテンプレートも掲載します。

#===============================
# Globals Selection
#===============================
Globals:
#===============================
# API Gateway
#===============================  
  Api:
    CacheClusterEnabled: false
    EndpointConfiguration:
      Type: EDGE
    MethodSettings:
      - CachingEnabled: false
        DataTraceEnabled: true
        HttpMethod: '*'
        LoggingLevel: INFO
        MetricsEnabled: true
        ResourcePath: '/*'
        ThrottlingBurstLimit: 1000
        ThrottlingRateLimit: 1000
    TracingEnabled: true

#===============================
# Lambda
#===============================
  Function:
    Runtime: go1.x
    Timeout: 27
    VpcConfig:
      SubnetIds:
        - <サブネットID>
        - <サブネットID>
      SecurityGroupIds:
        - <セキュリティグループID>
    EventInvokeConfig:
      MaximumRetryAttempts: 0
    KmsKeyArn: <KMS ARN>
    MemorySize: 256
    Tracing: Active

Resources:
#===============================
# API Gateway for Admin
#===============================
  AdminApiResource:
    Type: AWS::Serverless::Api
    Properties:
      Auth:
        AddDefaultAuthorizerToCorsPreflight: false
        ApiKeyRequired: false
        DefaultAuthorizer: AWS_IAM # ★★★ポイント★★★
      Cors:
        AllowMethods: "'*'"
        AllowHeaders: "'*'"
        AllowOrigin: "'*'"
        AllowCredentials: true # ★★★ポイント★★★
      GatewayResponses:
        DEFAULT_4XX:
          ResponseParameters:
            Headers:
              Access-Control-Allow-Origin: "'*'"
              Access-Control-Allow-Headers: "'*'"
              Access-Control-Allow-Methods: "'*'"
          ResponseTemplates:
            'application/json': '{"message":$context.error.messageString}'
        DEFAULT_5XX:
          ResponseParameters:
            Headers:
              Access-Control-Allow-Origin: "'*'"
              Access-Control-Allow-Headers: "'*'"
              Access-Control-Allow-Methods: "'*'"
          ResponseTemplates:
            'application/json': '{"message":$context.error.messageString}'
      Name: !Sub ${ServiceName}-${Env}-admin
      StageName: api

#===============================
# Lambda for Admin
#===============================
  GetTech4All:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${ServiceName}-${Env}-get_tech4all
      CodeUri: src/get_tech4all/
      Handler: get_tech4all
      Role: !ImportValue LambdaBackendRoleArn
      Events:
        CatchAll:
          Type: Api
          Properties:
            Path: /tech4all
            Method: GET
            RestApiId: !Ref AdminApiResource

#==============================
# Outputs
#==============================
Outputs:
#=======================================
# Admin API Gateway ID
#=======================================
  AdminApiResourceId:
    Value: !Ref AdminApiResource
    Export:
      Name: AdminApiResourceId

参考資料

  • ApiAuth
    • https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-api-apiauth.html
  • CorsConfiguration
    • https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-property-api-corsconfiguration.html

CloudFormationテンプレート

SAMテンプレートにはAPI GatewayとLambdaのみを記載し、それ以外のAWSリソースは全てCloudFormationに記載しています。今回は、Lambda用のIAMロールとCognitoユーザープールの管理者グループに紐づくIAMロールの箇所を抜粋しています。

Cognitoにまつわる箇所は次回以降に掲載します。

Resources:
#===============================
# Lambda Backend IAM Role
#===============================
  LambdaBackendRole:
    Type: AWS::IAM::Role
    UpdateReplacePolicy: Retain
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      RoleName: !Sub ${ServiceName}-${Env}-lambda-backend-role
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess
        - arn:aws:iam::aws:policy/service-role/AWSLambdaENIManagementAccess

#===============================
# Cognito Authenticated Admin IAM Role
#===============================
  CognitoAuthenticatedAdminRole:
    Type: AWS::IAM::Role
    UpdateReplacePolicy: Retain
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRoleWithWebIdentity
            Effect: Allow
            Principal:
              Federated: cognito-identity.amazonaws.com
            Condition:
              StringEquals:
                cognito-identity.amazonaws.com:aud: !Ref IdentityPool
              ForAnyValue:StringLike:
                cognito-identity.amazonaws.com:amr: authenticated
      RoleName: !Sub ${ServiceName}-${Env}-cognito-authenticated-admin-role
      Policies:
        - PolicyName: !Sub ${ServiceName}-${Env}-cognito-authenticated-admin-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Sid: Cognito
                Effect: Allow
                Action:
                  - cognito-identity:GetCredentialsForIdentity
                  - cognito-identity:GetId
                Resource: "*"
              - Sid: APIGateway
                Effect: Allow
                Action: execute-api:Invoke # ★★★ポイント★★★
                Resource:
                  !Join
                    - ''
                    - - !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}
                      - ':'
                      - !ImportValue AdminApiResourceId
                      - '/api/GET/tech4all'
              - Sid: Lambda
                Effect: Allow
                Action: lambda:InvokeFunction # ★★★ポイント★★★
                Resource: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ServiceName}-${Env}-get_tech4all

#==============================
# Outputs
#==============================
Outputs:
#===============================
# LambdaBackend IAM Role
#===============================
  LambdaBackendRoleArn:
    Value: !GetAtt LambdaBackendRole.Arn
    Export:
      Name: LambdaBackendRoleArn
Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です