Chaliceのすゝめ その2 WebApiに認証を追加する

# 本記事の目的について
前回記事では`Chalice`というAWSのサービスについて記載しました。
そこでは`Chalice`のインストールから、簡単なデバッグ方法までの説明をしてますのでまだ`Chalice`に触れたことがない方はそちらをご覧ください。

**[前回記事](https://qiita.com/tukkyr/items/b1f54b467375ba29facf)**

本記事ではWebApiに認証をつける方法について紹介したいと思います。

# 環境
本記事の環境は

- MacOS Mojave 10.14.1
- Python 3.6.7
- chalice 1.6.1
- docker 18.09.0

で作成し、httpクライアントは

- httpie          1.0.0

を使います

# ChaliceのWebApiの認証に関する機能

[ここ](https://github.com/aws/chalice/blob/master/docs/source/topics/authorizers.rst)に記載がある通り、AWSでは一般的である IAM, CognitoUserPoolに加え、Custom Authorizerを使っての認証を設定することができます。
これは APIGateway が持っている オーサライザーの設定項目と同じようです。

**ApiGatewayのオーサライザー設定コンソール**
<img width="927" alt="スクリーンショット 2018-11-24 18.20.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/92230/d932542c-3dc0-027b-a748-c268f7f670fe.png">

本記事では、CognitoUserPool を使った認証について記載したいと思います。

# CognitoUserPoolの準備
ChaliceにはCognitoUserPoolを作成する機能がないのでCloudFormationを使って作成したいと思います。

[cloudformation ドキュメント](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html)
[AWS::Cognito::UserPool](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html)
[AWS::Cognito::UserPoolClient](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpoolclient.html)

template.yaml

```yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: ChaliceUserPool
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireLowercase: True
          RequireUppercase: True
          RequireNumbers: False
          RequireSymbols: False
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: True
  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: ChalicePoolClient
      UserPoolId: !Ref CognitoUserPool
      GenerateSecret: False
Outputs:
  CognitoUserPoolArn:
    Value: !GetAtt CognitoUserPool.Arn
```

was-cliが有効な環境で下記のコマンドを実行します。
S3には適当なバケットを作成してください。
上記のファイルが存在するフォルダで下記のコマンドを実行してください。

```sh
$ aws cloudformation deploy --template-file template.yaml --stack-name ChaliceAuthCognitoUserPool --capabilities CAPABILITY_IAM

# Waiting for changeset to be created..
# Waiting for stack create/update to complete
# Successfully created/updated stack - ChaliceAuthCognitoUserPool
```

`Successfully...` と表示されればCognitoUserPoolが作成され、
またCloudformationのstackにCognitoUserPoolのArnが下記のように設定されているはずです。

<img width="739" alt="スクリーンショット 2018-12-01 16.32.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/92230/3fbab94b-1427-161f-2365-442c6b1e99e0.png">

後ほどChaliceの設定に使いますのでメモっておいてください。

# 認証付きWebApiの実装
認証の実装は

- プロジェクトの作成
- 環境変数の登録、読み込み
- Authorizerの実装

のステップを踏むことになります。
※ 以下のステップは`chalice`コマンドが使えることを前提に記載しています。
※ 環境のClalice構築方法については[前回記事](https://qiita.com/tukkyr/items/b1f54b467375ba29facf)を参考にしてください。

## プロジェクトの作成

下記のコマンドを実行してください。

```sh
chalice new-project auth-project
```

```sh
$ tree -ar .
.
├── requirements.txt
├── app.py
├── .gitignore
└── .chalice
    └── config.json
```
のように４つのファイルが出来上がります。

## 環境変数の登録
Chaliceは`config.json`に`environment_variables`のエントリーを登録するとlambdaで環境変数が呼び出せるようになります。

config.json

```json
{
  "version": "2.0",
  "app_name": "auth",
  "stages": {
    "dev": {
      "api_gateway_stage": "api",
      "environment_variables" : {
        "USER_POOL_ARN": "取得したUserPoolArnを指定"
      }
    }
  }
}
```

上記の設定で`USER_POOL_ARN`という環境変数がlambdaの環境に登録されます。

```sh
$ chalice deploy
```
実行後、出来上がったlambdaを見ると

<img width="1110" alt="スクリーンショット 2018-12-01 17.32.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/92230/b9935305-e253-affc-da94-6161f32f5c6b.png">


のように環境変数が登録されます。

## 環境変数の呼び出し
`python`から環境変数を呼び出すには以下のように実装します。

```py
import os

os.environ['USER_POOL_ID']
```

## Authorizerの実装
すべてのパーツが揃いましたのでどんどん実装していきます。

app.py

```py
import os

from chalice import Chalice
from chalice import CognitoUserPoolAuthorizer

app = Chalice(app_name='auth')

authorizer = CognitoUserPoolAuthorizer(
    'ChaliceUserPool', provider_arns=[os.environ['USER_POOL_ARN']]
)


@app.route('/', methods=['GET'], authorizer=authorizer)
def authed_index():
    # print(os.environ['USER_POOL_ARN'])
    return {'success': True}
```

このように、用途に合わせたauthorizer(今回はCognitoUserPoolAuthorizer)をインスタンス化して`@app.route`のデコレータの引数として渡せば良いようです。
非常に簡単ですね。

# 動作確認

まずはローカルで動作確認をしてみましょう

```sh
$ chalice local
$ http localhost:8000

# {
#    "success": true
# }
```

ローカルサーバーを立ち上げて、Authorizerを設定したApiを呼び出すと、呼び出せてしまいました。どうやらローカルではAuthorizerの検証ができないようです。

次にAWSにデプロイしてみます。

```sh
$ chalice deploy
$ http $(chalice url)

# HTTP/1.1 401 Unauthorized
# ...
# {
#    "message": "Unauthorized"
# }
```

のように今度はApiの呼び出しが `401` の認証エラーになったようです。

ApiGateWayを確認してみると

<img width="554" alt="スクリーンショット 2018-12-01 17.01.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/92230/2aa16f93-05e9-25bf-b716-daf8189d8a99.png">

<img width="445" alt="スクリーンショット 2018-12-01 17.04.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/92230/b02bae95-9ea4-a320-2552-7f69e011222d.png">


のようにApiGatewayにオーサライザーが作成され、対象のApiにはそのオーサライザーが `COGNITO_USER_POOL` で設定されていることが確認できます。

次にCognitoUserPoolのトークンを取得して `Authorization` ヘッダーに CognitoUserPool が発行したトークンを付加してリクエストを送信してみましょう。

## トークンの取得
トークンの取得、今回はあらかじめユーザーをコンソール上で作成してそのユーザーに対してNodo.js用のAWSのSDKを使ってトークンを取得します。(Clientの用途に合わせて、SDKを選択してください。今回は記載していませんが、React, Android, iOS で実装したい場合は[amplify](https://aws-amplify.github.io)が良さそうです。)

package.json

```json
{
  "name": "nodejs",
  "version": "1.0.0",
  "description": "get token form your userpool",
  "main": "get_tokne.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "tukky",
  "license": "ISC",
  "dependencies": {
    "amazon-cognito-identity-js": "^2.0.27",
    "node-fetch": "^2.2.0"
  }
}
```

get_token.js

```js
const AmazonCognitoIdentity = require('amazon-cognito-identity-js');
global.fetch = require('node-fetch');

const poolData = {
  UserPoolId: '<Set Your UserPoolId>',
  ClientId: '<Set Your UserPool Client Id>'
};

const userName = '<set your user name>'

const authenticationData = {
  Username: userName,
  Password: '<set your user pool password>'
};

const userData = {
  Username: userName,
  Pool: new AmazonCognitoIdentity.CognitoUserPool(poolData)
}
const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);

cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess: (result) => {
      const idToken = result.getIdToken().getJwtToken();
      console.log(idToken);
    },
    onFailure: (err) => {
      console.log(err);
    },
    newPasswordRequired: (userAttributes, requiredAttributes) => {
      cognitoUser.completeNewPasswordChallenge(authenticationData.Password, {}, this);
    }
});
```

`get_token.js`に対象となる

- UserPoolId
- UserPoolClientId
- UserName
- UserPassword

を`get_token.js`に設定してください。
`package.json` と `get_token.js` を同一のフォルダに配置し

```sh
$ npm install
$ node get_token.js
# eyJ...
```

と実行すると`eyJ`から始まる目的のトークンが取得できます。

## トークン付きリクエストの発行

先ほど取得したトークンを付加してリクエストしてみましょう

```sh
$ http $(chalice url) Authorization:eyJ...

# HTTP/1.1 200 OK
# {
#    "success": true
# }
```

のように認証が成功し無事Apiが呼び出すことができました。

# まとめ
Chaliceは非常に便利なのですが、機能を使いこなすためには他のAWSに関する知識も必要になります。
また、今回はCloudformationを使ってUserPoolの生成をしましたが、CI/CDツールで運用したり、デプロイ担当と開発担当が分かれているとCloudformationの実行手順や、環境変数の手順ななどのルールを決めないと実際の運用時に問題になりそうです。
(その際はconfig.jsonに自動で環境変数を追加するようなスクリプトが必要になると思います。)

このような問題を抱えながらも自分は`Chalice`を積極的に使っていきたいと思います。
理由は開発者観点からすると、コードと環境構築が一体になっているのは魅力的で、作りたいものがすぐに環境構築を気にせずに作れるからです。

次回はDynamoDBとS3の連携に関して記述できればと考えています。

ここまで読んでいただきありがとうございました。
開発に少しでも役立てば幸いです。

# おまけ
このおまけはNodejsの環境の構築に関するものです、すでにNodejsの環境がある場合はそちらをお使いください。
Nodejsの環境構築にDockerを使うと便利です。
Nodejsの環境にはnode公式のコンテナの`8.14.0-alpine`を利用しました。

[docker インストール for mac](https://docs.docker.com/docker-for-mac/install/)
[docker インストール for windows](https://docs.docker.com/docker-for-windows/install/)
[dockerhub nodejs](https://hub.docker.com/_/node/)

Token取得用のDockerfile

```docker
FROM node:8.14.0-alpine

COPY src /node/src

WORKDIR /node/src

RUN npm install

CMD node /node/src/get_token.js
```

フォルダ構成

```sh
$ tree .
#.
# ├── Dockerfile
# └── src
#    ├── get_token.js
#    └── package.json
```

Docker コンテナの実行

```sh
$ docker build -t token-test:1.0 .
$ docker run --rm token-test:1.0
```

これで目的のtokenの取得ができます。
コンテナ上で作業したい場合は

```sh
$ docker run --rm -it token-test:1.0 /bin/sh 
```

を実行するとshでコンテナにログインできます。

## Dockerについての参考
https://qiita.com/rawHam/items/80ba13d2d2d56dba411e
https://qiita.com/yuki_ycino/items/b94ae2bf7d78685cd6f5