AWS AmplifyでサーバレスWebアプリの構築(Cognito + API Gateway + IAM認証)

# サーバレスWebアプリケーションを手軽に構築

※この記事は2017年のServerless Advent Calendarの記事として投稿したものです。そのため、AWS AmplifyとAPI Gatewayの組み合わせでの認証を交えた使い方を説明したものとなっています。
AWS Amplify + AWS AppSyncでの認証を交えた使い方などをお調べの方は、2018年のServerless Advent Calendarの記事として投稿した[こちら](https://qiita.com/nakayama_cw/items/6d3514b51c5fbf9ba450)の記事をご確認ください。

## これまでの問題
- WebアプリでCognito Identity Pool + Cognito User Poolを使う場合、複数のJSを読み込む必要がある
- API GatewayのIAM認証をしている場合、SignV4での署名が複雑になる
- API Gatewayで生成されるSDKを使ってリクエストをする場合、複数のAPIを使用することがやりづらい

特に署名についてはあまりドキュメントも多くなく、試行錯誤されている方を多く見かけます。

## AWS Amplifyが出ました！！

そんな悩めるフロントエンドエンジニアに待望のライブラリが発表されました。

- [モバイルまたはウェブアプリケーションを使用するクラウド開発用の宣言型 JavaScript ライブラリ、AWS Amplify を導入](https://aws.amazon.com/jp/about-aws/whats-new/2017/11/introducing-aws-amplify-a-declarative-javascript-library-for-cloud-development-with-mobile-or-web-applications/) 
- [aws/aws-amplify: A declarative library for application development using Cloud services with JavaScript](https://github.com/aws/aws-amplify)

## AWS Amplifyの機能

AWS Amplifyは下記の機能を備えているようです。

- Authentication
    - Cognito Identity Pool と Cognito User Poolを使用したユーザ認証、ユーザサインアップ等のAPIを提供します
- Analytics
    - Amazon Pinpointを使用したアクセス解析を行う機能を提供します。Pinpointの設定を行うだけでセッションデータの集計は可能。
    - カスタムイベントやユーザ属性等の送信を簡単に行うAPIが提供されます。
- API
    - API GatewayへのGET、POST、PUT、DELETE、HEADリクエストを簡単に行うAPIを提供します
    - AWS_IAM認証を行っているAPIに対して、Cognitoにより認証したクレデンシャルを使用し、AWS Signature Version 4による署名を自動で行います。
- Storage
    - S3へのオブジェクトのPUT、GETを簡単に行うAPIを提供します。
    - Private,PublicのACLを容易に設定できます。
- Cache
    - キーバリュー構造のデータを容易にLRUキャッシュとして管理することができるAPIを提供します。
    - Webで使用する場合、LocalStorageにキャッシュデータが保存されます。
- Utilities
    - 多言語対応を行うためのl18nモジュール
    - ロガーモジュール
    - moduleとcomponent間でイベントをpub/subにより共有できるhubモジュール
 
# 使ってみた

## 今回使うもの
Cognito + API Gateway（AWS_IAM認証）をWebアプリケーションとして実装したいと思います。
そのため、下記のモジュールを使用します。

- Authentication
- API

## 環境

- Windows10
- Visual Studio Code
- JavaScript(TypeScript)

## インストール
今回は、ブラウザ上で実行するWebアプリケーションを構築するため、
必要なファイルは [aws-amplify.js](https://github.com/aws/aws-amplify/blob/master/packages/aws-amplify/dist/aws-amplify.js) のみとなります。
github上からダウンロードしてくるか、下記の通りnpmにてインストールし、該当ファイルを取得してください。

```bash
npm install aws-amplify --save
```

## ファイル構成

```
/
├─ index.html
├─ main.ts
├─ package.json
├─ require.js
├─ style.css
├─ tsconfig.json
└─ src
 　└─ aws-amplify.js
```

## index.html
index.html内では、TSファイルの読み込みを行うため、require.jsにてロードさせる

```html
<script data-main="./src/main.js" src="require.js"></script>
```

## main.ts
今回の対象スクリプトはこのファイルに記述する
ファイルの先頭でAWS Amplifyの読み込みを行う

```js:main.ts
import Amplify,{ Auth,API } from 'aws-amplify';
```

## Cognito Identity Pool / Cognito User Poolの作成

Cognito Identity Pool と Cognito User Poolを作成します。
設定内容を合わせるため、要点を記載します。

### Cognito User Pool
サインインの方法として今回はEメールアドレスを使用する形で設定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/6bf0af86-c78e-4876-3996-b11d4c1ce91d.png)

アプリクライアントの作成をする部分では、クライアントシークレットの作成を行わないようチェックを外してください

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/177c1103-2212-54a1-5ed6-54b231331ede.png)

その他は基本的にデフォルトの内容で作成します。

### Cognito Identity Pool

Cognito Identity Poolについては、認証プロバイダーに先程作成したUser Poolの情報を入力し、認証されていないIDに対するアクセスも許可するように設定を行ってください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/3f8eb80f-e1f2-6a4d-0967-e366e3919cc3.png)

また、今回は認証済ユーザのみAPI GatewayへのAPIを実行できる権限を持たせるため、
認証済みユーザのIAMポリシーを下記のように設定します。

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

## API Gatewayの作成
今回は下記の内容で、設定を行います。

作成するリソース

- GET /guest
    - リクエストタイプ Mock
    - 認証なし
- GET /member
    - リクエストタイプ Mock
    - AWS_IAM認証あり

統合レスポンスとしてそれぞれ下記の通りの設定をしました

```json:guest
{
    "Message": "Hello! Guest"
}
```
```json:member
{
    "Message": "Hello! Member"
}
```

Webからリクエストを行えるようにするため、CORSを有効に設定します。

設定が完了したら下記のリソースになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/fed73751-9630-5078-9a62-11dfb9e272b0.png)

ステージ名をv1としてデプロイしました

## Amplify.configure
Amplify.configure()によって、各種設定情報を与えます。
今回使用するのはCognitoとAPI Gatewayなので、AuthモジュールとAPIモジュールの設定を行います。

※2018/4/2 @thotgamma さんにご指摘いただき、最新版の仕様に合わせてAPI.endpointsの設定にregionを追加

```js:main.ts
Amplify.configure({
  Auth: {
      identityPoolId: 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', //REQUIRED - Amazon Cognito Identity Pool ID
      region: 'ap-northeast-1', // REQUIRED - Amazon Cognito Region
      userPoolId: 'ap-northeast-1_xxxxxxxxx', //OPTIONAL - Amazon Cognito User Pool ID
      userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxx', //OPTIONAL - Amazon Cognito Web Client ID
  },
  API: {
    endpoints: [
      {
          name: "AmplifyTest",
          endpoint: "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1",
          region: "ap-northeast-1"
      }
    ]
  }
});
```

## Authモジュール
### サインアップ
今回の設定では、メールアドレスをユーザー登録時の必須属性としています。
その為、メールアドレスとパスワードにてサインアップを行うことができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/5d6cbf5c-e494-7c7a-5e89-a1fd4549b620.png)

適当なフォームを作成し、入力を受け付けます。

```js:main.ts
Auth.signUp(email, password)
.then((data) => { 
  alert('登録メールアドレスに検証コードを送信しました。');
})
.catch((err) => {
  console.log(err);
  alert('エラーが発生しました');
  return;
});
```
サインアップを行うためのAPIは、
Auth.signUp(username, password, email, phone_number)
となり、今回はusernameの代わりにemailを使用しているため、
email, passwordの2つを渡すことでサインアップが行われます。

メールアドレスが必須となっているため、MFAによりメールアドレス宛に
検証コードが送信されます。
送信された検証コードを受け付けるフォームを表示します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/129c31c9-095e-9b3b-f3ca-6f86602e9daf.png)

```js:main.ts
Auth.confirmSignUp(email, code)
.then((data) => { 
  alert('ユーザ登録が完了しました。');
})
.catch((err) => {
  console.log(err);
  alert('検証に失敗しました');
  return;
});
```
検証のためのAPIは
Auth.confirmSignUp(username , code)
となります。

とても明快で簡潔に処理が記述できました。
ユーザー登録が完了しました。
AWSコンソールのCognito User Poolからユーザとグループの参照を行うと、
先程登録したユーザが存在することがわかります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/a30fc739-9c0d-b986-efe2-7344aa5f3841.png)

### サインイン
続いてサインインです。
メールアドレスとパスワードを入力してサインインをするフォームを作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/9260b3f2-dbd1-7566-54ed-4376027cbd25.png)

```js:main.ts
Auth.signIn(email, password)
.then((data) => { 
  alert('サインインに成功しました');
})
.catch((err) => {
  console.log(err);
  alert('サインインに失敗しました');
  return;
});
```

サインインを行うAPIは
Auth.signIn(username , password)
です。
サインインに成功するとCognitoUserの情報が取得できます。

### サインアウト
認証の最後にサインアウトを実装します。

```js:main.ts
Auth.signOut()
.then((data) => { 
  alert('サインアウトしました');
})
.catch((err) => {
  console.log(err);
  alert('サインアウトに失敗しました');
  return;
});
```

ここまでで、Cognito User Poolを使用したサインアップ、サインイン、サインアウトがとても簡単に実装できました。

## APIモジュール
### 確認
今回の実装では未認証時はAPI GatewayのInvoke権限がないため、AWS_IAM認証が必要な /member へはコールができない想定です。
また、認証を実装していない /guest へは認証の有無に関わらずAPIコールが行える想定となります。

まずはAPI Gatewayの設定が正しく行われているか確認をするため、ブラウザから直接エンドポイントを叩いてみます。

/v1/guest:
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/1d6dfd6e-dbb5-5a38-485c-775647a4d898.png)

/v1/member:
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/7682bc4e-3bc3-9eef-b246-06026f2879e0.png)

上記の通り、/guest は正常にレスポンスが返ってきており、/member は署名がされていないリクエストのため、リクエストは拒否されます。

### /guest へのリクエスト
Authモジュールにて作成したページで、サインアウトした状態で、
下記のリクエストを実行します。

```js:main.ts
let apiName = 'AmplifyTest';
let guest_path = '/guest'; 

API.get(apiName, guest_path)
.then(response => {
  console.log(response);
})
.catch(err => {
  console.log(err);
});
```

結果はこちら
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/ad518dab-5e2b-e7e7-9465-057f9a219638.png)

正常にAPIからの応答を表示できています。

### 未認証時の /member へのリクエスト

続いて未認証時の /member へリクエストを行います。
先程とほぼ変わらず下記のコードとなります。

```js:main.ts
let apiName = 'AmplifyTest';
let member_path = '/guest'; 

API.get(apiName, member_path)
.then(response => {
  console.log(response);
})
.catch(err => {
  console.log(err);
});
```
結果はこちら
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/d3a33267-d69e-ac23-d2b5-235c75242f41.png)

403ステータスが返されてエラーとなっています。

### 認証済みの /member へのリクエスト
それではサインインを行った状態で再度 /member へのリクエストを同じコードで実行してみましょう。

結果は下記の通り
![image.png](https://qiita-image-store.s3.amazonaws.com/0/138161/24fc2aff-44a5-ab08-9d5a-9f25cf3106b7.png)

見事にHello! Memberと返ってきてAWS_IAM認証を必要とするAPIへのコールに成功しています。

このように、AWS Amplifyを使用すると、Sign V4の署名リクエストを意識することなく簡単に行うことができます。

# まとめ

- AWS Amplifyを使うことでCognitoを使用したサーバレスWebアプリケーションを簡単に実装することができる
- AWS_IAM認証を行うことが簡単！
- 今回実装したもの以外にも多くの機能が使用可能

また、APIも今回紹介したもの以外に数多く存在します。
https://aws.github.io/aws-amplify/packages/aws-amplify-react-native/docs/index.html

# サンプル
今回実装したアプリのソースコードを下記に置いておきました。
https://github.com/cwknakayama/amplify-test