AWS Amplify+IAMで未認証ユーザでもAppSyncを使えるようにする

# 背景
AWS Amplify CLIでバックエンドの自動構築をしようとすると、APIの認証タイプを選ぶとき(Choose an authorization type for the API)に「API KEY」か「Amazon Cognito User Pool」しか選べません。

```
? Please select from one of the below mentioned services GraphQL
? Choose an authorization type for the API 
❯ API key 
  Amazon Cognito User Pool 
```
「Amazon Cognito User Pool」を選択すると認証していないユーザはAppSyncを利用できません。
未認証ユーザがAppSyncを使えるようにするためにはAPIの認証タイプにIAMを選択する必要があります。
Amplify CLIを使わずに各リソースを手動で設定することで、認証ユーザと未認証ユーザの両方がAppSyncを使えるようなバックエンド環境の構築を目指します。(ついでにAmplify CLIの裏での挙動を理解したいと思います。)

# 構成
## AWSリソース（バックエンド）
- Cognito ユーザープール: アプリケーションを利用するユーザの認証に使います。
- Cognito IDプール: 認証したユーザがどのAWSリソースにアクセスできるのかを管理します。また認証していないユーザのアクセス制限も行います。
    - 今回はAWS AppSyncのアクセス管理を行います。
- AppSync: GraphQLを用いたDynamoDBへのアクセスのためのAPIとして使います。
    - AuthItemとUnauthItemの２つのタイプを作成し、認証したユーザーは両方にアクセス可能で、未認証ユーザーはUnauthItemのみにアクセスできるようにします。
- DynamoDB: データソースとして利用します。

## フロントエンド
- AWS Amplify
- Vue.js

## 構成図
こんな感じのことを実現したい

<table>
<tr>
<td>認証したユーザ</td>
<td>認証していないユーザ</td>
</tr>
<tr>
<td">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107090/8fb055e9-58bd-66e5-f519-2ca964e5d7da.png" width="3000px">
</td>
<td>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107090/315852f8-b2a5-3311-ef96-cda4f2c82e31.png" width="3000px">
</td>
</tr>
</table>

# 環境
## バックエンド(AWS)
### AppSync
認証したユーザのみがアクセスできるAPI(AuthItem)と全てのユーザがアクセスできるAPI(UnauthItem)を作成します。
自動的にクエリとDynamoDBでAuthItem tableとUnauthItem tableが生成されます。

### ユーザープール
一人ユーザーを作成しておきます。
パスワードルールや必須情報などは今回はこだわりません。
ただクライアントアプリケーションとしてVue.jsを利用するのでシークレットキーの生成はオフにします。

### IDプール
認証されていない ID を有効にしたいので、「認証されていない ID に対してアクセスを有効にする」にチェックをして「プールを作成」します。
#### ロールの設定
IDプールには「認証されたロール」と「認証されていないロール」が保存されており、ユーザーの承認リクエストのたびにユーザの認証状態に応じてどちらかのロールが自動的に利用されます。
つまり、「認証されたロール」にAuthItemに関するAPIへのアクセス権限が、「認証されていないロール」にはUnauthItemに関するAPIへのアクセス権限を与えます。
#### ロールにアタッチするポリシー
IAM → ポリシー → ポリシーの作成、から以下のJSONでポリシーを作成します

**認証されたロールのためのポリシー**

```
{
 "Version": "2012-10-17",
  "Statement": [
   {
    "Effect": "Allow",
    "Action": [
     "appsync:GraphQL"
     ],
    "Resource": [
     "arn:aws:appsync:[your region]:[account id]:apis/[AppSync API ID]/types/Query/fields/getAuthItems",
     "arn:aws:appsync:[your region]:[account id]:apis/[AppSync API ID]/types/Query/fields/getUnauthItems"
    ]
   }
  ]
}
```
- Actionに"appsync:GraphQL"を設定します
- Resourceにアクセスを許可するAWSリソースをarn形式で追加します
    - /types以降に許可するqueriesを指定できます。(上の例はAuthItemの取得)
        - 例えば認証したユーザにAuthItemの作成を許可したい場合は/types/Mutation/fields/CreateAuthItem
    - 参考: [AppSyncのAWS_IAM認証](https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/security.html#aws-iam-authorization)

このポリシーがアタッチされたロールをIDプールの「認証されたロール」に設定します。

**認証されていないロールのためのポリシー**

```
{
 "Version": "2012-10-17",
  "Statement": [
   {
    "Effect": "Allow",
    "Action": [
     "appsync:GraphQL"
     ],
    "Resource": [
     "arn:aws:appsync:[your region]:[account id]:apis/[AppSync API ID]/types/Query/fields/getUnauthItems"
    ]
   }
  ]
}
```

## フロントエンド(Vueプロジェクト)
vueプロジェクトを作成し、```$ amplify codegen```([Amplify codegen](https://aws-amplify.github.io/docs/cli/codegen))を利用しAppSyncの設定を反映します。
### AuthとApiの認証情報の設定
自分が作成したAWSリソースの情報をmain.jsで読み込みます。

```javascript:main.js
import Amplify from 'aws-amplify';

Amplify.configure({
  "aws_project_region": "XX-XXXXXXXX-X",
  "aws_cognito_identity_pool_id": "XX-XXXXXXXX-X:XXXXXXXXXXXXXXXXXXXXXXXXX",
  "aws_cognito_region": "XX-XXXXXXXX-X",
  "aws_user_pools_id": "XX-XXXXXXXX-X_XXXXXXXX",
  "aws_user_pools_web_client_id": "XXXXXXXXXXXXXXXXXXXXX",
  "oauth": {},
  "aws_appsync_graphqlEndpoint": "https://XXXXXXXXXXXXXXXXXX.appsync-api.XX-XXXXXXXX-X.amazonaws.com/graphql",
  "aws_appsync_region": "XX-XXXXXXXX-X",
  "aws_appsync_authenticationType": "AWS_IAM"
})
```
"aws_appsync_authenticationType": "AWS_IAM"が今回設定したかった項目ですね

### 検証

```javascript:test.js
import * as queries from '@/graphql/queries'
import { Auth, API, graphqlOperation } from 'aws-amplify'

async function test () {
 await Auth.signIn("[test mail address]", "**********") // サインインします
  .then( user => console.log(user)) // CognitoUser {username: "*** ...", ... }

 /* 認証ユーザでAppSyncへアクセス */
 await Auth.currentSession() // JWT Token の取得
  .then( session => console.log(session)) // CognitoUserSesssion {idToken: CognitoIdToken, ... }

 await Auth.currentCredentials() // AWS Credentials の取得
  .then( credentials => console.log(credentials)) //CognitoIdentityCredentials {sessionToken: "**** ...", ...}

 await API.graphql(graphqlOperation(queries.listAuthItems)) // AuthItemの一覧を取得
  .then( items => console.log(items)) // {data: listAuthItems: {items: ...}}

 await API.graphql(graphqlOperation(queries.listUnauthItems)) // UnauthItemの一覧を取得
  .then( items => console.log(items)) // {data: listUnauthItems: {items: ...}}

 await Auth.signOut() // サインアウトします

 /* 未認証ユーザでAppSyncへアクセス */
 try {
  await Auth.currentSession()
  } catch(error) { // 当然 JWT Token の取得はできません
  console.log(error) // No current user
 }

 await Auth.currentCredentials() // サインインしていませんが、AWS Credentials の取得はできます
  .then( credentials => console.log(credentials)) // CognitoIdentityCredentials {sessionToken: "**** ...", ...}

 try {
  await API.graphql(graphqlOperation(queries.listAuthItems))
 } catch(error) { // 権限がないのでAuthItemの取得は失敗します
  console.log(error) // {data: {...}, errors: : Array(1)}
 }

 await API.graphql(graphqlOperation(queries.listUnauthItems)) // 権限があるのでUnauthItemの一覧を取得は成功します
  .then( items => console.log(items)) // {data: listUnauthItems: {items: ...}}
}
```

# 感想
最初はAmplify CLIで爆速でアプリケーションを作ってましたが、認証関連でつまったので色々調べてみました。
amplify codegenでcloudformation使わざるを得ない点がちょっとモヤりますが、まだ使いきれてないとこも多いと思うのでドキュメント漁ってみたいと思います。