Python
AWS
cognito

API GatewayのアクセスをCognito User Poolのグループで制御する

この記事はハンズラボ Advent Calendar 2017の18日目です。

からっきーです。
今回はCognito User Poolに作成したグループ単位で認証済みユーザーがアクセス可能なAPIエンドポイントをコントロールしてみました。

API Gatewayには認証方式が3種類用意されているのですが、IAMロールベースで認可するサービスをコントロールしたい場合はIAM認証を選択します。

IAMで認証する場合、クライアントではsigV4署名を行い、それをリクエストに含める必要があります。
Pythonでそれをいちから実装する方法がこちらに載っていますがかなりつらそうなので今回はモジュールを使って実装しました。

使用したモジュール

capless/warrant

ユーザー名/パスワードを使ってCognitoユーザープール認証ができます。

jmenga/requests-aws-sign

sigV4を使用したAWSリソースへのアクセスをシンプルに実装できます。

リソースの設定

IAMロール

権限はこんな感じで設定しました。

UnauthenticatedRole

  • 非認証ユーザーに割り当てられるロール
  • すべてのAPIにアクセス不可
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": "*",
            "Effect": "Deny"
        }
    ]
}

authenticatedRole

  • ログイン済みのユーザーに割り当てられるロール
  • /helloはアクセス可
  • /adminはアクセス不可
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": "arn:aws:execute-api:ap-northeast-1:000000000000:xxxxxxxx:/*/GET/hello",
            "Effect": "Allow"
        },
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": "arn:aws:execute-api:ap-northeast-1:000000000000:xxxxxxxx:/*/GET/admin",
            "Effect": "Deny"
        }
    ]
}

adminGroupRole

  • ログイン済みかつ管理者権限グループに所属しているユーザーに割り当てられるロール
  • すべてのAPIにアクセス可
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

Cognito User Poolの設定

※説明を簡略にするためCognito User Poolは作成済みでユーザーが存在する前提で進めます。

グループを作成します。ここではclientGroupadminGroupを作成しています。clientGroupは一般ユーザーでadminGroupは管理者ユーザーを入れます。

先ほどのIAMロールを以下のように紐付けます。

  • clientGroup

    • authenticatedRole
  • adminGroup

    • adminGroupRole

Cognito Identity Poolの設定

ロールの設定

ロールをそれぞれ以下のとおりに紐付けます。

  • 「認証されていないロール」
    • UnauthenticatedRole
  • 「認証されたロール」
    • authenticatedRole

認証プロバイダーの設定

  • ユーザープール ID
    • [作成済みのCognito User PoolのID]
  • アプリクライアントID
    • [作成済みのCognito User PoolのアプリクライアントID]

WebコンソールのIDプールの編集から「認証プロバイダー」を開き、下記のとおりに設定します。(CFnで書いてみたのですが、エラーがでるのでWebコンソールから設定しました)

  • 「認証されたロールの選択」
    • 「トークンからロールを選択する」
  • 「ロールの解決」
    • 「デフォルトの認証されたロールを使用する」にします。

APIを叩いてみる

検証で使用したコードはこちらです。

ログインしたあとcredentialsを発行しています。
その後/hello/adminを叩いて、ステータスコードを出力しています。

api_request.py
# -*- coding: utf-8 -*-

import yaml
import requests
import boto3
from warrant.aws_srp import AWSSRP
from requests_aws_sign import AWSV4Sign

env = 'dev'

with open('serverless.env.yml') as file:
    config = yaml.load(file)

ACCOUNT_ID = str(config[env]['account_id'])
REGION = config['region']
API_ENDPOINT = config[env]['api_endpoint']
USER_POOL_ID = config[env]['user_pool_id']
CLIENT_ID = config[env]['application_client_id']
IDENTITY_POOL_ID = config[env]['identity_pool_id']

def login(username, password):
    # cognito user poolで認証してID Tokenを取得する
    aws = AWSSRP(username=username, password=password, pool_id=USER_POOL_ID, client_id=CLIENT_ID)
    tokens = aws.authenticate_user()
    id_token = tokens['AuthenticationResult']['IdToken']

    logins = {'cognito-idp.' + REGION + '.amazonaws.com/' + USER_POOL_ID: id_token}

    client = boto3.client('cognito-identity', region_name='ap-northeast-1')
    cognito_identity_id = client.get_id(
        AccountId=ACCOUNT_ID,
        IdentityPoolId=IDENTITY_POOL_ID,
        Logins=logins
    )

    credentials = client.get_credentials_for_identity(
        IdentityId=cognito_identity_id['IdentityId'],
        Logins=logins
    )

    session = boto3.session.Session(
        aws_access_key_id=credentials['Credentials']['AccessKeyId'],
        aws_secret_access_key=credentials['Credentials']['SecretKey'],
        aws_session_token=credentials['Credentials']['SessionToken'],
        region_name=REGION
    )

    return session.get_credentials()


def get_requests(service, url, credentials):
    headers = {"Content-Type": "application/json"}
    auth = AWSV4Sign(credentials, REGION, service)

    response = requests.get(url, auth=auth, headers=headers)

    return response


def main():
    username = 'testuser'
    password = 'Password1234'
    credentials = login(username, password)
    service = 'execute-api'

    # /hello
    request_path = '/hello'
    url = API_ENDPOINT + request_path

    response = get_requests(service, url, credentials)

    print('\nGET', request_path, response.status_code)

    # /admin
    request_path = '/admin'
    url = API_ENDPOINT + request_path

    response = get_requests(service, url, credentials)

    print('\nGET', request_path, response.status_code)

    return


if __name__ == '__main__':
    main()

こちらが設定ファイルです。

serverless.env.yml
defaultStage: dev
region: ap-northeast-1

dev:
  profiles: myaws
  user_pool_id: ap-northeast-1_xxxxxxxxxxx
  application_client_id: xxxxxxxxxxxxxxxxxxxxxxxxxx
  identity_pool_id: ap-northeast-xxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx
  account_id: 000000000000
  api_endpoint: https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

実行してみる

ここで使用しているtestuserは現時点でclientGroupに入っています。
実行すると以下のように出力されます。

GET /hello 200

GET /admin 403

/adminへのリクエストに対しては403 forbiddenがレスポンスされていることがわかります。

権限を変更して実行

続いて、testuserをadminGroupに入れて同様に実行してみます。

GET /hello 200

GET /admin 200

/adminへのリクエストが許可されるようになりました。

まとめ

実際のアプリでPythonからAPI Gatewayを叩くこともあるかもしれないですが、私の場合は主にAPI Gatewayを通したインテグレーションテスト用として使うことのほうが多そうです。
この機会に試せて良かったです。

明日はtuki0918さんです!!

参考

https://github.com/awslabs/aws-serverless-auth-reference-app