APIGatewayのリソースパスをCognitoで認可制御したい件

## 動機

APIGatewayとLambdaで、こんなAPIエンドポイント`GET /users/{uid}/tasks`を作って、認証済みユーザーのuidが111のときに以下のような制御をしたいものとします。

| リクエスト | Allow/Deny | 応答コード |
|:---------------------:|:------:|:------------:|
|GET /users/111/tasks   | Allow  | 200  |
|GET /users/222/tasks   | Deny  |  403 |

これをCognito使って認可制御したいなと思ったのがきっかけです。

## ところでCognitoは3つある

単にCognitoと行っても３つの機能があるので整理しておきます。

[![AWS Black Belt 資料より](https://image.slidesharecdn.com/20170517aws-blackbeltamazoncognito-170517101907/95/slide-12-1024.jpg)](https://www.slideshare.net/AmazonWebServicesJapan/20170517awsblackbeltamazoncognito/12)

本書では以下のように使い分けます。

| 名前                | 本書での略称 |
|:-------------------|:--------|
|Federated Identities| Fed ID| 
|User Pools          |User Pools   |
|Sync                |使わない   |

以下にサラッと概要を書いておきます。

### Federated Identities

http://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html

#### できること

* 一時AWSクレデンシャルを発行する
  * 例) ログイン成功したらスマホアプリにIAM Roleを付与してS3のGETできる 
* 複数の外部認証プロバイダ（Facebook認証、Twitter認証など）を取りまとめて統一IDを管理する

自分はPublic Client[^1]向けの一時クレデンシャル発行機として扱ってます。

[^1]: OAuth,OpenID Connectの言葉。  ネイティブアプリやSPAなどでアクセスキーを安全に保持できないタイプのクライアントを指す。

### User Pools

https://aws.amazon.com/jp/blogs/news/amazon-cognito-your-user-pools-now-generally-available/

#### できること

* ユーザー管理一式の機能提供
    * ID/パスワードでのユーザー認証（Sign in)
    * ユーザーの登録(Sign up)
* OpenID Connectに準拠したIdP
  * [OIDCトークン](http://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06)発行機

この子すごいんですよ。認証つきのアプリならつきもののユーザー管理機能一式がマネージドで使えちゃうんです。
SMSで認証コードを送って確認コードを入れさせるとかも簡単にできちゃいます。

### Sync

https://docs.aws.amazon.com/mobile/sdkforandroid/developerguide/cognito-sync.html

* ゲームデータなどの保持するのに便利っぽい
  * DynamoDB用意するほどでも無いとき用か？
* 本書では扱わない

## で、さまよった足跡

### トライ１：IAM認証+Cognito Fed ID

これを一番最初に思いつきました。だって、[公式ドキュメント](http://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/iam-roles.html)にS3へのユーザー別認可制御が簡単そうに書いてあるんですもの。
ポリシードキュメントのResource指定を`${cognito-identity.amazonaws.com:sub}`変数使って書けばいいんでしょって。

```json
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::mybucket/${cognito-identity.amazonaws.com:sub}/*"]
    }
```

これを真似して、

```json
    {
        "Effect": "Allow",
        "Action": [
            "execute-api:Invoke"
        ],
        "Resource": [
            "arn:aws:execute-api:ap-northeast-1:757XXXXXX:nxcXXXX/*/*/users/${cognito-identity.amazonaws.com:sub}/*"
        ]
    }
```

こんなのでいいかなって。

#### ダメだったポイント

一言で言うと**`:`という文字がパーセントエンコードされる**ということです。

* CognitoのID形式はこんな感じ  
  `ap-northeast-1:3c5d3ea1-19b2-48a6-9cb2-fa91261cfe7d`
  * つまりは `GET /users/ap-northeast-1:3c5d3ea1-19b2-48a6-9cb2-fa91261cfe7d/tasks` というリクエストになる
* `:`は`%3A`にエンコードされる([RFC3986](http://freak-da.hatenablog.com/entry/20080321/p1))

要するに、

ポリシー定義的に認可OKなARN

`arn:aws:execute-api:ap-northeast-1:757XXXXXX:nxcXXXX/*/*/users/ap-northeast-1:3c5d3ea1-19b2-48a6-9cb2-fa91261cfe7d/tasks`

実際にリクエストされたARN

`arn:aws:execute-api:ap-northeast-1:757XXXXXX:nxcXXXX/*/*/users/ap-northeast-1%3A3c5d3ea1-19b2-48a6-9cb2-fa91261cfe7d/tasks`

で、`:`と`%3A`が違うやんけという話で蹴られてしまうんですね。

### トライ２：じゃあCustomAuthorizerでやろう

APIGatewayのエンドポイント認可の方式にはもうひとつ、[CustomAuthorizer](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/use-custom-authorizer.html)というものがあります。
この機能は認可チェック用のロジックをLambdaファンクションとして実装できるものです。

APIGatewayの[マッピングテンプレートで使用可能な変数のリファレンス](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference)を見ると`$context.identity.cognitoIdentityId`というのがあります。
この値をチェックすればいいんじゃね？と。

#### ダメだったポイント

一言で言うと**CustomAuthorizerでは`cognitoIdentityId`を知る手段がない**です。

CustomAuthorizerに渡されるイベントは以下の形式です。

```json
{
    "type":"TOKEN",
    "authorizationToken":"<caller-supplied-token>",
    "methodArn":"arn:aws:execute-api:<regionId>:<accountId>:<apiId>/<stage>/<method>/<resourcePath>"
}            
```

もらえるとしたら`authorizationToken`なのですが、ここには入ってきません。

正確に言うとcaller側でHTTPヘッダに`cognitoIdentityId`をセットしてもらえれば、できます。
が、前提として、

* AWS署名v4使いたい
    * リプレイアタック対策とか便利だから
* caller側はAPIGatewayが吐き出すSDKを使いたい

というのがあったのでこの方法は断念しました。

### トライ３：じゃあ、Cognito User Pools認証を使う

APIGatewayの認可の方式には更にもうひとつ、[Cognito User Pools](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html)があります。

#### ダメだったポイント

だめだったというか、やってません。
なぜかというと、認証方式としてTwitterやFB認証をサポートする予定だったのでFed IDの方を使いたかったのです。

## じゃあ結局どうしたのか

おとなしく、バックエンド側のLambdaファンクションの中でCognitoIDをチェックするようにしましたとさ。。