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

この記事は[ハンズラボ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/handslab)の18日目です。

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

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

IAMで認証する場合、クライアントでは[sigV4](https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html)署名を行い、それをリクエストに含める必要があります。
Pythonでそれをいちから実装する方法が[こちら](https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-signed-request-examples.html)に載っていますがかなりつらそうなので今回はモジュールを使って実装しました。

### 使用したモジュール
#### [capless/warrant](https://github.com/capless/warrant)
ユーザー名/パスワードを使ってCognitoユーザープール認証ができます。
#### [jmenga/requests-aws-sign](https://github.com/jmenga/requests-aws-sign)
sigV4を使用したAWSリソースへのアクセスをシンプルに実装できます。

# リソースの設定
### IAMロール
権限はこんな感じで設定しました。

#### UnauthenticatedRole

- 非認証ユーザーに割り当てられるロール
- すべてのAPIにアクセス不可

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": "*",
            "Effect": "Deny"
        }
    ]
}
```

#### authenticatedRole
- ログイン済みのユーザーに割り当てられるロール
- `/hello`はアクセス可
- `/admin`はアクセス不可

```json
{
    "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にアクセス可

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "execute-api:Invoke"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```

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

グループを作成します。ここでは`clientGroup`と`adminGroup`を作成しています。`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://qiita.com/tuki0918)さんです！！

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