Amazon API GatewayでCustom Authorizerを利用する

## 概要
[Amazon API Gateway](https://ap-northeast-1.console.aws.amazon.com/apigateway/home) と[AWS Lambda](http://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html) を利用してCustom Authorizerを利用します。

## API Gatewayを利用する為にIAMユーザーを作成する

AWS ルートアカウントによる作業はセキュリティ上よろしくないので、専用のIAMユーザーを作成し必要な権限のみを与えます。

[Identity and Access Management](https://console.aws.amazon.com/iam/home#/home)より「個々の IAM ユーザーの作成」を選択、以下の権限を付与します。

- AmazonAPIGatewayAdministrator
- AWSLambdaFullAccess
- AWSLambdaDynamoDBExecutionRole
- AmazonDynamoDBFullAccesswithDataPipeline

↓こんな感じ、グループ名等は分かりやすい名前であれば何でも良いです。
※「プログラムによるアクセス」と「AWS マネジメントコンソールへのアクセス」には両方チェックをつけておくのを忘れないようにしましょう。

<img width="1007" alt="add_user.png" src="https://qiita-image-store.s3.amazonaws.com/0/71899/65826b16-1892-94cd-f9da-c7e21ebfa90a.png">

## 必要なLambda関数を定義する

今回はサンプルとして以下のAPIを用意します。

- ```/users/{userId} - GET``` # ユーザーを1件取得する

これらの関数からアクセスする為のテーブルを2つ用意しておきます。
その他の項目はサンプルなので適当でOKです。

- users
    - id (String) ※primary、UUIDを生成して入れる
    - email（String）
- access_tokens
    - access_token (String) ※primary、UUIDを生成して入れる

※余談ですが、[命名ルールおよびデータ型](http://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html)を見ると、利用出来る文字列や予約語等の確認が出来ます。

一応、一部記号が使えますがAWSのドキュメント例だとパスカルケースの複数形で命名している例が多いので、規約等はそれに合わせるのが良いかもしれません。
（今回のサンプルではaccess_tokensとかしちゃいましたが・・・）

```js:/users/{userId}GET
'use strict';

const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();

exports.handler = (event, context, callback) => {

  const params = {
    TableName: 'users',
    FilterExpression : 'id = :val',
    ExpressionAttributeValues : {':val': event['userId']}
  };

  dynamo.scan(params, (error, data) => {
    if (error) {
      callback(
        Error('Fail. err:' + error)
      );
    } else {
      callback(null, data);
    }
  });
};
```

※サンプルなのでコードが雑なのはご了承下さい。

## API GatewayからLambda関数を呼び出す設定を行う

詳細な手順は省略します。
基本的に[Lambda 関数を公開するための API を作成する](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-create-api-from-example.html) を参考にして頂ければそれほど難しくはありませんでした。

[Lambda プロキシとして API を構築する](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/getting-started.html)を見ながら一通りの基本操作をやっておく事もオススメします。

最終的にAPI Gatewayの管理画面を以下のような状態にします。

<img width="199" alt="gateway1.png" src="https://qiita-image-store.s3.amazonaws.com/0/71899/ef5e1cac-8c50-a57d-30b6-6f349486f956.png">

## Custom Authorizerの概要

下記の図は公式ドキュメントに記載されている物です。

![custom-auth-workflow.png](https://qiita-image-store.s3.amazonaws.com/0/71899/792cc588-ad92-2dd2-b3e1-0287d248968f.png)

処理の流れは下記ようになります。

1. クライアントはAPIにリクエストを行います。（この時にAuthorizationヘッダにアクセストークン等のトークンを指定します）
1. API GatewayはLambda Auth Functionを呼び出します。
1. Lambda Auth FunctionはAPI Gateway にIAMポリシーを返します。
1. IAMポリシーに従って、API Gatewayが認可を実施します。

ちなみに一度認可処理を行ったIAMポリシーはキャッシュされます。（現在のところデフォルトで5分、最大1時間までの間でキャッシュの設定を行える）
キャッシュの設定は無効にする事も可能です。

## Custom Authorizer用のLambda 関数を定義する

以下のコードを定義します。
今回はAuthorizationのアクセストークンをaccess_tokensテーブルから取得し、取得出来たら、対象のAPIを利用する権限を与えます。

クライアントは以下のようにAuthorizationヘッダにアクセストークンを設定してリクエストを行います。

```bash:リクエスト例
curl -kv \
-H "Authorization: 6d8c4c95-d884-4735-858d-c1fd3b8d870d" \
https://my-api-id.execute-api.region-id.amazonaws.com/stage/users/{userId}
```

ちなみにAPI GatewayのURLの構成ですが、下記のような形になっています。

https://**my-api-id**.execute-api.**region-id**.amazonaws.com/**stage**

#### my-api-id
APIに割り当てられる識別子です。
API GatewayでAPIを作成したタイミング（もしくはデプロイしたタイミングだったかも）で作成されます。

#### region-id
対象のAPIが存在するリージョンを表します。

#### stage
develop,staging、production等の環境を表す値です。
stage名に関しては利用者が任意で付ける事が出来ます。
デプロイの際は利用者がどこのstageに対してデプロイを行うかを選択します。

それぞれのパラメータが下記のような値だった場合のURLは

- my-api-id
    - 77abcdef84
- region-id
    - us-east-1
- stage
    - test
- userId
    - abcd1234

https://77abcdef84.execute-api.us-east-1.amazonaws.com/test/users/abcd1234
となります。

```js:customAuthorize
'use strict';

const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();

exports.handler = (event, context, callback) => {

  const accessToken = event.authorizationToken;

  const params = {
    TableName: 'access_tokens',
    FilterExpression : 'access_token = :val',
    ExpressionAttributeValues : {':val': accessToken}
  };

  dynamo.scan(params, (error, data) => {

    if (accessToken === 'unauthorized') {
      callback(
        new Error('Unauthorized')
      )
    }

    if (error) {
      callback(
        new Error('Fail. err:' + error)
      );
    } else {
      if (data['Items'].length === 0) {
        callback(
          null,
          generatePolicy('user', 'Deny', event.methodArn)
        );
      } else {
        callback(
          null,
          generatePolicy('user', 'Allow', event.methodArn)
        );
      }
    }
  });
};

/**
 * ポリシーを生成する
 *
 * @param principalId
 * @param effect
 * @param resource
 * @returns {{}}
 */
const generatePolicy = function(principalId, effect, resource) {
  const authResponse = {};
  authResponse.principalId = principalId;
  if (effect && resource) {
    const policyDocument = {};
    policyDocument.Version = '2012-10-17'; // default version
    policyDocument.Statement = [];
    const statementOne = {};
    statementOne.Action = 'execute-api:Invoke'; // default action
    statementOne.Effect = effect;
    statementOne.Resource = resource;
    policyDocument.Statement[0] = statementOne;
    authResponse.policyDocument = policyDocument;
  }
  return authResponse;
};
```

### customAuthorizeの解説

#### event.authorizationToken
これでAuthorizationヘッダの中身を取得可能です。

#### generatePolicy
AWSのIAMポリシーを作成するメソッドです。
API GatewayはこのIAMポリシーを元に認可を実行するのでここでどのような値を返却するかで、リクエストを行ってきたクライアントにどのような権限を与えるかを定義します。

IAMのポリシードキュメント (policyDocument)は下記のような形式になっています。

```json:ポリシードキュメントの例
{
  "principalId": "yyyyyyyy", // The principal user identification associated with the token sent by the client.
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "execute-api:Invoke",
        "Effect": "Allow|Deny",
        "Resource": "arn:aws:execute-api:<regionId>:<accountId>:<appId>/<stage>/<httpVerb>/[<resource>/<httpVerb>/[...]]"
      }
    ]
  },
  "context": {
    "key": "value",
    "numKey": 1,
    "boolKey": true
  }
}
```

#### principalId
[公式ドキュメント](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/use-custom-authorizer.html) の記載から読み取ると、アクセストークンに紐づくユーザーの識別子を入れれば良いのではと思います。
サンプルでは手抜きして"user"という固定値を入れています。

##### 2016-12-19 追記
AWSの中の人に念の為確認しましたが、principalIdに入れるべき値に関してはトークンに紐付いたユーザーを識別出来る値であれば何でも良いそうです。

#### Effect
"Allow"または"Deny"を指定します。
意味は英単語の通りで許可する場合はAllowを許可しない場合はDenyを指定します。

#### Resource
ここでアクセス可能とする、ARN（対象リソースを識別する為の識別子）を定義します。
ARNはAPIGatewayの管理画面から確認が出来ます。（arn:aws:execute-api…みたいな値です）

サンプルではevent.methodArnの値をアクセス可能なリソースとして登録しています。
※event.methodArnは対象リソースのARNを取得します。
※厳密にはどうか分かりませんがARNは[URN](https://ja.wikipedia.org/wiki/Uniform_Resource_Name)の事だと私は解釈しています。

つまりこの場合は、下記のAPIを利用する為の権限をリクエストしてきたクライアントに対して付与します。
https://my-api-id.execute-api.region-id.amazonaws.com/stage/users/{userId}

※もしも複数のリソースに対して権限を付与する場合は以下のようにgeneratePolicy()の第3引数に配列で複数のARNを渡してあげればOKです。

```js:複数のARNを許可する例
generatePolicy(
  'user',
  'Allow',
  [
    'arn:aws:execute-api:region-id:666655555555:77abcdef84/*/GET/users/*',
    'arn:aws:execute-api:region-id:666655555555:77abcdef84/*/PUT/users/*'
  ]
);
```

このあたりの記載例に関しては[API 実行アクセス権限の IAM ポリシーの例](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-iam-policy-examples-for-api-execution.html)を参照すると良いでしょう。

ちなみに管理画面の「オーソライザー」から簡単にテストが可能です。
IDトークンの部分にDynamoDBに存在するアクセストークンをセットしてテストを押下すると、

<img width="991" alt="apigateway1.png" src="https://qiita-image-store.s3.amazonaws.com/0/71899/ee689228-9fff-e048-f9cf-eddfe42dfc75.png">

下記のようなポリシードキュメントの中身が確認出来ます。

```json:テストで返却されたポリシードキュメントの中身
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "execute-api:Invoke",
      "Effect": "Allow",
      "Resource": [
        "arn:aws:execute-api:us-east-1:200000000000:77abcdef84/*/POST/users",
        "arn:aws:execute-api:us-east-1:200000000000:77abcdef84/*/GET/users/*",
        "arn:aws:execute-api:us-east-1:200000000000:77abcdef84/*/PUT/users/*"
      ]
    }
  ]
}
```

## 接続確認を行う

アクセストークン : e4459aca-f01e-4a27-add0-05181ddc4122
ユーザーID : e649ddc9-f1d3-464b-aed3-2b88b5e55f01

これらの値はDynamoDBに存在しています。
よって期待値としては、正常に認可が行われAPIが利用出来るハズです。

```bash:テストケース（/users/{userId}GETが正常終了する）
curl -kv \
-H "Authorization: e4459aca-f01e-4a27-add0-05181ddc4122" \
https://77abcdef84.execute-api.us-east-1.amazonaws.com/test/users/e649ddc9-f1d3-464b-aed3-2b88b5e55f01
```

```http:レスポンス（/users/{userId}GETが正常終了する）
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 114
Connection: keep-alive
Date: Thu, 15 Dec 2016 15:56:01 GMT
x-amzn-RequestId: fa342be3-c2de-11e6-a907-03c47bdf205f

{
  "Items":[
    {
      "id":"e649ddc9-f1d3-464b-aed3-2b88b5e55f01",
      "email":"keitahoge@gmail.com"
    }
  ],
  "Count":1,
  "ScannedCount":7
}
```

意図した通りのレスポンスになりました。
試しに先程と同じアクセストークンを利用してユーザーIDの部分を他のユーザーIDに変更してリクエストします。
認可ロジック的にはユーザーID（e649ddc9-f1d3-464b-aed3-2b88b5e55f01）以外にはアクセスが出来ないので403エラーが返却されるハズです。

```bash:テストケース（/users/{userId}GETに対して先程とは違うユーザーIDを指定）
curl -kv \
-H "Authorization: e4459aca-f01e-4a27-add0-05181ddc4122" \
https://77abcdef84.execute-api.us-east-1.amazonaws.com/test/users/1e7727c8-4fe2-4a59-af26-8ca2e6816a71
```

```http:レスポンス（/users/{userId}GETに対して先程とは違うユーザーIDを指定）
HTTP/1.1 403 Forbidden
Content-Type: application/json
Content-Length: 60
Connection: keep-alive
Date: Thu, 15 Dec 2016 16:07:32 GMT
x-amzn-ErrorType: AccessDeniedException
x-amzn-RequestId: 95d4c1e0-c2e0-11e6-99ec-77ceb77f871b

{"Message":"User is not authorized to access this resource"}
```

意図した通りのエラーが発生しました。
先程のアクセストークンにはユーザーID（e649ddc9-f1d3-464b-aed3-2b88b5e55f01）以外のリソースにアクセスする権限がない事が確認出来ました。

## 感想

今回利用したサンプルコードは簡易的な物ですが、[OAuth2](https://tools.ietf.org/html/rfc6749)や[OpenIDConnect](http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html)に準拠した本格的な認可基盤を開発する事も可能だと思います。（完全に準拠するとなるとかなり大変だと思います。）

※サンプルコードは一応、[github](https://github.com/keita-nishimoto/aws-lambda-sample/blob/master/custom-authorize.js)に上げておきました。