CognitoUserPoolと CognitoIdPoolで、完全サーバーレスな APIGateway API 認可を実現する方法!IAM ロールを用いた超セキュアな API 認可を解説😎

CognitoUserPoolと CognitoIdPoolで、完全サーバーレスな APIGateway API 認可を実現する方法!IAM ロールを用いた超セキュアな API 認可を解説😎

こんにちは!

APIGateway は、AWS での RestAPI 開発でデファクトスタンダードな製品です。既存システムのクラウドリフトアップから新規開発問わずどんなユースケースでも導入が可能で、AWS で RestAPI 開発する際には真っ先に導入が検討される製品です。サーバーレスな API 開発から、プロキシー的な位置付けで導入を検討されるお客様も多くいらっしゃいます。

そんな RestAPI 開発の製品として魅力的な API Gateway ですが、私たちが思う APIGateway の真価は、その幅広いセキュリティ機能にあると感じています。

本記事では、CognitoUserPool、CognitoIDPool、IAM Role、APIGateway を使用したコードを一切書かないサーバーレスなAPI認可を実現する方法を解説します。(少々上級者向けの内容となっております…)

想定する読者

  • APIGateway のセキュリティについて検討しているヒト
  • APIGateway を使用して開発を行ったことがあるヒト
  • CognitoUserPool による認証を行ったことがあるヒト
  • CognitoIDPool による認可を行ったことがあるヒト

はじめに

前提知識

本記事は、AWS のセキュリティに関する基本的な知識を要する方向けの内容となっています。以下の言葉についてピンとこない方は、AWS の製品 HP などで仕様確認の上ご覧ください。

  • APIGateway
  • Cognito User Pool
  • Cognito ID Pool
  • IAM Role
  • AssumeRole
  • Cognito ID Poolを使用したIAM RoleへのAssumeRole
  • JWT Token
  • STS Token

本記事で紹介する CloudFormation は ServerlessFramework3 形式

ネイティブの CloudFormation や SAM のコードは紹介しません。ServerlessFramework3 を用いた CloudFormation のコードを紹介します。(とは言いつつ、基本的には通常の CloudFormation ですので引用しやすいかと思います)

本記事で紹介する認可の全体像

サインイン後に ID プールを使用して AWS サービスへアクセスする - Amazon Cognito

本記事で紹介する認可の APIGateway のワークフローは以下になります。(詳細は後述)

  1. Cognito User Pool で認証トークンを得る
  2. Cognito ID Pool から STS (認可)トークンを得る
  3. STS トークンを使用して APIGateway へリクエスト

上記を実現するにあたり各 AWS 製品の細かいテクニックを使用しますが、その中心にいるのはIAMロールです。最終的なゴールは、各ユーザーが適切な IAM ロールの認可トークンを得ることです。

それでは、CloudFormation のソースコードを交えて実装方法を解説していきます。

STEP1. IAMロールの作成

本記事のサンプルでは、IAM ロールを用いて以下のような API に対する認可を実現していきます。以下のサンプルAPI は、シンプルに従業員と管理者、それぞれが呼び出せる API として理解してもらえたらと思います。

  • GET /staff
  • GET /admin

管理者向けの IAM ロールと従業員向けの IAM ロールを作成し、上記 API の認可を実現します。IAM ロールのCloudFormation は以下です。

# 以下は管理者向けのロール
AuthenticatedAdminRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: "example-congito-apigateway-AuthenticatedAdminRole"
    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 CognitoIDPool
            ForAnyValue:StringLike:
              cognito-identity.amazonaws.com:amr: authenticated
    Policies:
      - PolicyName: "policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Sid: APIGateway
              Effect: Allow
              Action:
                - execute-api:Invoke
              Resource:
                - !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*/GET/admin*

# 以下は従業員向けのロール
AuthenticatedStaffRole:
  Type: AWS::IAM::Role
  Properties:
    RoleName: "example-congito-apigateway-AuthenticatedStaffRole"
    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 CognitoIDPool
            ForAnyValue:StringLike:
              cognito-identity.amazonaws.com:amr: authenticated
    Policies:
      - PolicyName: "policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Sid: APIGateway
              Effect: Allow
              Action:
                - execute-api:Invoke
              Resource:
                - !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*/GET/staff*

# 以下はCognitoIDプール向けのロール
CognitoIdentityPoolAuthenticatedRole:
  Type: AWS::IAM::Role
  UpdateReplacePolicy: Retain
  Properties:
    RoleName: "example-congito-apigateway-CognitoIdentityPoolAuthenticatedRole"
    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 CognitoIDPool
            ForAnyValue:StringLike:
              cognito-identity.amazonaws.com:amr: authenticated
    Policies:
      - PolicyName: "example-congito-apigateway-CognitoIdentityPoolAuthenticatedRole-policy"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Sid: Cognito
              Effect: Allow
              Action:
                - cognito-identity:GetCredentialsForIdentity
                - cognito-identity:GetId
              Resource: "*"

STEP2. CognitoUserPoolの作成

CognitoUserPool の作成では、CognitoUserPool にユーザープールグループを設定するのがポイントです。各ユーザープールグループに対して IAM ロールの ARN を指定しますが、これを後述の CognitoIDPool が AssumeRole 対象のIAM ロールとして使用します。

CognitoUserPool:
  Type: "AWS::Cognito::UserPool"
  Properties:
    AccountRecoverySetting:
      RecoveryMechanisms:
        - Name: "verified_email"
          Priority: 2
    AutoVerifiedAttributes:
      - "email"
    AliasAttributes:
      - "email"
      - "preferred_username"
    Policies:
      PasswordPolicy:
        MinimumLength: 8 # 10文字以上
        RequireLowercase: false # 小文字必須
        RequireNumbers: false # 数字必須
        RequireSymbols: false # 記号必須
        RequireUppercase: false # 大文字必須
    UserPoolName: "example-congito-apigateway-UserPool"

CognitoUserPoolClient:
  Type: "AWS::Cognito::UserPoolClient"
  Properties:
    CallbackURLs:
      - "http://localhost:3000"
    ClientName: "example-congito-apigateway-UserPoolClient"
    DefaultRedirectURI: "http://localhost:3000"
    ExplicitAuthFlows:
      - ALLOW_USER_SRP_AUTH
      - ALLOW_REFRESH_TOKEN_AUTH
      - ALLOW_ADMIN_USER_PASSWORD_AUTH
    GenerateSecret: false
    LogoutURLs:
      - "http://localhost:3000"
    PreventUserExistenceErrors: ENABLED
    ReadAttributes:
      - email
    RefreshTokenValidity: 10
    SupportedIdentityProviders:
      - COGNITO
    UserPoolId: !Ref CognitoUserPool

UserPoolAdministoratorGroup:
  Type: "AWS::Cognito::UserPoolGroup"
  Properties:
    Description: "管理者"
    GroupName: Admin
    Precedence: 1
    UserPoolId: !Ref CognitoUserPool
    RoleArn: !GetAtt AuthenticatedAdminRole.Arn

UserPoolStaffGroup:
  Type: "AWS::Cognito::UserPoolGroup"
  Properties:
    Description: "一般"
    GroupName: Staff
    Precedence: 2
    UserPoolId: !Ref CognitoUserPool
    RoleArn: !GetAtt AuthenticatedStaffRole.Arn

以下の箇所がユーザープールグループの設定箇所です。先ほど作成した従業員と管理者の IAM ロールが設定されていると思います。これにより、認証トークンに対してcognito:preferred_roleが付与され、後述の CognitoIDPool がAssumeRole する対象の IAM ロールとして認識できます。(詳細は後述)

UserPoolAdministoratorGroup:
  Type: "AWS::Cognito::UserPoolGroup"
  Properties:
    Description: "管理者"
    GroupName: Admin
    Precedence: 1
    UserPoolId: !Ref CognitoUserPool
    RoleArn: !GetAtt AuthenticatedAdminRole.Arn

UserPoolStaffGroup:
  Type: "AWS::Cognito::UserPoolGroup"
  Properties:
    Description: "一般"
    GroupName: Staff
    Precedence: 2
    UserPoolId: !Ref CognitoUserPool
    RoleArn: !GetAtt AuthenticatedStaffRole.Arn

STEP3. CognitoIDPool の作成

それでは、CognitoIDPool を作成します。CognitoIDPool は CognitoUserPool で認証したトークンを使用して認可トークンを得ることができます。

CognitoIDPool:
  Type: AWS::Cognito::IdentityPool
  DependsOn:
    - CognitoUserPool
    - CognitoUserPoolClient
  Properties:
    IdentityPoolName: "example-congito-apigateway-IDPool"
    AllowUnauthenticatedIdentities: false
    CognitoIdentityProviders:
      - ClientId: !Ref CognitoUserPoolClient
        ProviderName: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}

IdentityPoolRoleAttachment:
  Type: AWS::Cognito::IdentityPoolRoleAttachment
  DependsOn:
    - CognitoIDPool
    - CognitoUserPool
    - CognitoUserPoolClient
    - AuthenticatedStaffRole
  Properties:
    IdentityPoolId: !Ref CognitoIDPool
    Roles:
      "authenticated": !GetAtt CognitoIdentityPoolAuthenticatedRole.Arn
    RoleMappings:
      CognitoUserPool:
        AmbiguousRoleResolution: AuthenticatedRole
        IdentityProvider: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}:${CognitoUserPoolClient}
        Type: Token 

RoleMappings の箇所を見てください。先ほど作成した CognitoUserPool の Web クライアントを認証プロバイダーに指定し、Type を Token に指定しています。ここで、TypeをTokenにすることが重要です。TypeをTokenにすることで、認証トークンの内容から自動的にAssumeRoleするIAMロールを判定することができます。

RoleMappings:
  CognitoUserPool:
    AmbiguousRoleResolution: AuthenticatedRole
    IdentityProvider: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}:${CognitoUserPoolClient}
    Type: Token

TypeをTokenに指定することで、CognitoUserPoolGroupで指定されているロール(cognito:roles and cognito:preferred_role)を使用してロールをマッピングすることが可能です。これにより、完全サーバーレスでIAMロールを使用したAPI認可を実現できます。

TIPS

APIGateway の認証タイプを IAM 認証モードにした場合、認証トークンの内容は取得できる?

結論、取得できません。

APIGateway の認証タイプが IAM 認証になっている場合は、フロントエンドは CognitoIDPool から取得した STS トークンを使用したシグネチャーを生成し、APIGateway へリクエストを行います。つまり、認証トークンを API が知る術がないということです。

APIGateway から Lambda を呼び出した際に取得可能な Identity 情報は?

IAM 認証時においても、リクエストユーザーを特定できる情報を取得したいケースは多いです。以下は、APIGatewayの認証タイプが IAM 認証の時に、Lambda で取得できる Identity の情報です。実装時に参考にしてください!

{
  "identity":
    {
      "cognitoIdentityPoolId": "ap-northeast-1:xxxxx-xxxxx-xxxxx",
      "accountId": "643022257822",
      "cognitoIdentityId": "ap-northeast-1:xxxxx-xxxxx-xxxxx",
      "caller": "xxxxx-xxxxx-xxxxx:CognitoIdentityCredentials",
      "sourceIp": "xxx.xxx.xxx.xxx",
      "principalOrgId": "xxxxx-xxxxx-xxxxx",
      "accessKey": "XXXXXXXXXXXX",
      "cognitoAuthenticationType": "authenticated",
      "cognitoAuthenticationProvider": "cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_6zELGQFml,cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_6zELGQFml:CognitoSignIn:{ cognito user pool sub }",
      "userArn": "arn:aws:sts::643022257822:assumed-role/example-congito-apigateway-AuthenticatedAdminRole/CognitoIdentityCredentials",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
      "user": "xxxxx-xxxxx-xxxxx:CognitoIdentityCredentials",
    },
}

cognitoAuthenticationProvider の「:」の後に CognitoUserPool の Sub が設定されています。これにより、IAM 認証時においてもユーザーの特定が可能となります。

まとめ

本記事では、各 AWS 製品のメリットを存分に活かした APIGateway のサーバーレスな IAM 認証実装方法を解説しました。

もちろん、Lambda コードで愚直に認可処理を実行することも可能なので、プロジェクトに合わせて認可処理の実現方法を検討してみてください。

AWSサーバーレスフロントエンドの開発はお気軽にお問い合わせください。