AWS Amplify を使って React クライアントから AppSync に認証リクエストをなげる

## はじめに
認証をつけた状態でReactのクライアントから GraphQL API を使うときに簡単に実現する方法を書いてみました。認証ページでサインアップ、ログイン、トークンを使ってGraphQL APIにリクエストまで完了します。
<img width="815" alt=" .png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/6479df4d-584c-24c9-89c8-b3998cb49108.png">


## React の開発環境の構築

```
$ npm install -g create-react-app
$ create-react-app amplify-sample
```

create-react-app でプロジェクトのベースとなる部分を生成します。

デフォルトのプロジェクトが作成されたら、動作確認をしてみます。

```
$ cd amplify-sample
$ yarn start
```

これでローカルにnodeサーバーが立ち上がり http://localhost:3000/ で動作確認をできるようになります。

## AWS Amplify のインストールと AWS へのアクセス設定

amplify の設定のタイミングでこのアプリケーション用に権限が払い出されます。これを実行する前に、ブラウザで　AWS にログインしておきます。regionは今回tokyo (ap-northeast-1) を選択します。
amplify :  https://aws-amplify.github.io/amplify-js/media/quick_start?platform=purejs

```
$ npm install -g @aws-amplify/cli
$ amplify configure
```
<img width="657" alt="スクリーンショット_2018-10-01_16_32_04.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/3e8622d1-c25a-1c73-7480-d9cd751788a1.png">

username を選択したあとブラウザで AWS 画面が開かれます。アプリからAWS にアクセスするための権限を払い出します。今回はサンプルなので一旦強めの権限でアカウントを払い出します。デフォルトのまま進めると Admin 権限のユーザーが払い出されます。

<img width="992" alt="スクリーンショット 2018-10-01 16.16.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/3943fbe3-efb0-e084-e0ad-fa569c15aefc.png">

### AWS のアプリ用権限の払い出しと、access key の設定。
ユーザーが作成されると、CLIでaccessKey、secretAccesKeyがブラウザ上で表示されます。amplify cliにこれらのkeyを入力します。
<img width="1028" alt="スクリーンショット_2018-10-02_15_36_28.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/adb8b0b8-9ad8-db2f-9165-a22b9ee16088.png">

<img width="462" alt="スクリーンショット_2018-10-01_16_31_33.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/13bf1a9b-f5d9-a9e8-22a6-9e7e6baed051.png">

今回作成したユーザーはadmin権限がついているはずですので、githubなどに公開してしまうと他人にadmin権限を渡すことになります。他人には見せないようキーの扱いには注意が必要です。

### amplify cli を使って AWS の環境を構築
```
$ amplify init
$ amplify push
```

以上で。AWS のバックエンドを操作する準備と、AWS への接続準備が完了しました。最終的にamplify pushにより src/aws-exports.js というファイルが作成されるのがゴールになります。aws-exports.js には接続先情報が記載されており、このファイルさえあれば aws への接続は可能です。AWS 環境を自分で構築して、aws-exports.js を自分で書いてしまえば amplify cli の作業は必要ありません。
amplify cli の詳細はこちら
https://github.com/aws-amplify/amplify-cli

### aws-exports.js の中身

```
import Amplify from 'aws-amplify';

Amplify.configure({
    Auth: {
        // REQUIRED - Amazon Cognito Identity Pool ID
        identityPoolId: 'XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab', 
        // REQUIRED - Amazon Cognito Region
        region: 'XX-XXXX-X', 
        // OPTIONAL - Amazon Cognito User Pool ID
        userPoolId: 'XX-XXXX-X_abcd1234',
        // OPTIONAL - Amazon Cognito Web Client ID
        userPoolWebClientId: 'XX-XXXX-X_abcd1234', 
    }
});
```

## AWS AppSync バックエンドの構築

amplify-cli を利用して マネージド GraphQL サービスである AWS AppSyncの環境を構築します。
AppSync 公式 : https://aws.amazon.com/jp/appsync/ 
AppSync 説明資料 : https://www.slideshare.net/keisuketsukagoshi/rest-api-graphql

```
$ amplify add api
```
途中、data typeを新しく作るか聞かれるので新しいtypeを定義しましょう。

```
type City @model {
  id: ID!
  name: String!
  description: String
  location: String
}
```

```
$ amplify push
```

以上で、GraphQL のエンドポイントの構築が完了しました。実際にできているものを確認するためにAppSyncのコンソールをブラウザでひらいてみます。先ほど定義した data type のスキーマが出来上がっているのが確認できます。
<img width="1109" alt="スクリーンショット 2018-10-03 22.30.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/1d0dc6bb-3488-6df0-601d-31b3c7394ab2.png">

実際に GraphQL の動作を確認してみます。Query の画面を開いて、定義されている Operation をここで試すことができます。どんな Operation が定義されているかは <Docs を開くことで確認できます。
<img width="1185" alt="スクリーンショット_2018-10-03_22_38_20.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/46a095bf-7eb4-01c8-bb85-b47a21d12ff3.png">

データの追加とデータの取得を何パターンか試してみます。

### データの追加

```
mutation create {
    createCity(input:{
    name:"fukuoka"
    description:"nice food"
  }){
    id name description
  }
}
```

### データの取得

```
query list {
  listCitys{
    items{
      id name description
    }
  }
}
```

### データのフィルター

```
query filter {
  listCitys(filter:{
    description:{
      contains:"food"
    }
  }){
    items{
      id name description
    }
  }
}
```

## クライアントから Amplify を使って AppSync に接続
コンソールでプロジェクトのトップにいき、amplify-js のライブラリを追加します。

```
$ yarn add aws-amplify aws-amplify-react
```


index.js にamplifyの設定を組み込みます。aws-exportsに接続先情報が格納されています。

```
...省略...
import Amplify from "aws-amplify";
import config from "./aws-exports";
Amplify.configure(config)

...省略...
```

App.js を編集してさっき作成したGraphQL endpointを叩いてみます。component が読み込まれたタイミングでgraphqlのAPIをたたいて、結果を console.log に出力するだけのコードになっています。

### データの取得

```
...省略...
import { API, graphqlOperation } from "aws-amplify";

const ListCities = `
query list {
  listCitys {
    items{
      id name description
    }
  }
}
`;

class App extends Component {
  async componentDidMount() {
    const cities = await API.graphql(graphqlOperation(ListCities))
    console.log('cities:', cities)
  }
  ...省略...
```

ブラウザで consoleを開いてもらうとデータが取得できているのが確認できます。
<img width="1279" alt="スクリーンショット 2018-10-03 23.23.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/bc01782a-e63c-cbb2-82aa-0ac29532afa1.png">

次に、取得できた値を画面に表示してみます。

```
...省略...
class App extends Component {
  state = {cities: []}
  async componentDidMount() {
    const cities = await API.graphql(graphqlOperation(ListCities))
    console.log('cities:', cities)
    this.setState({cities: cities.data.listCitys.items})
  }
  render() {
    return (
      <div className="App">
        {
          this.state.cities.map((content, index) => (
            <div key={index}>
              <h3>{content.name}</h3>
              <p>{content.description}</p>
            </div>
          ))
        }
      </div>
    );
  }
...省略...
```
<img width="641" alt="スクリーンショット 2018-10-03 23.38.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/3fc872a7-f214-8269-603d-c72b285ab80a.png">

以上で、データの取得は完了です。

## データの更新と購読を実装

### データの更新

graphQLのオペレーションを書きます。List の時と同じような内容ですが、mutationの場合パラメーターがはいるのでそちらに注意する必要があります。App.js を編集します。

```
...省略...
const CreateCity = `
mutation($name: String!, $description: String) {
    createCity(input:{
    name:$name
    description:$description
  }){
    id name description
  }
}
`
...省略...
```

App Component に入力フィールドと graphQL を叩く部分を実装します。ここも API.graphql の第２引数にパラメーターをセットします。

```
...省略...  
class App extends Component {
  state = {cities: [], name:"", description:""}
  async componentDidMount() {
    ...省略...
  }
  
  onChange = e => {
    this.setState({[e.target.name] : e.target.value})
  }
  

  //ここが graphQL を叩く部分
  createCity = async() => {
    if ((this.state.name === '') || (this.state.description === '')) return 
    const city = {name: this.state.name, description: this.state.description}
    try {
      const cities = [...this.state.cities, city]
      this.setState({cities, name:"", description:""})
      await API.graphql(graphqlOperation(CreateCity, city))
      console.log('success')
    } catch (error) {
      console.log('error: ', error)
    }
  }

  render() {
    return (
      <div className="App">
        <div>
          <input value={this.state.name} name="name" onChange={this.onChange} />
          <input value={this.state.description} name="description" onChange={this.onChange} /> 
          <button onClick={this.createCity}>Create City</button>
        </div>
        
    ...省略...
```
<img width="627" alt="スクリーンショット 2018-10-04 0.08.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/ce5d1637-8f89-1dba-4fa6-a2dbfbd17900.png">


### データの購読

graphQL のオペレーションを作成します。

```
const SubscribeToCities = `
subscription {
  onCreateCity {
    id name description
  }
}
`;
```

Listと同じようにMountされたタイミングで購読を開始します。

```
...省略...
  async componentDidMount() {
    const cities = await API.graphql(graphqlOperation(ListCities))
    console.log('cities:', cities)
    this.setState({cities: cities.data.listCitys.items})
    
    //Cityが作られたイベントを購読
    API.graphql(
      graphqlOperation(SubscribeToCities)
    ).subscribe({
      next: (eventData) => console.log('eventData: ', eventData)
    });
  }
...省略...
```

購読しているデータに更新があった場合に、Stateを更新するよう修正。これにより更新をリアルタイムで画面に表示することが可能になります。

```
...省略...
    //Cityが作られたイベントを購読
    API.graphql(
      graphqlOperation(SubscribeToCities)
    ).subscribe({
      next: (eventData) => {
        const city = eventData.value.data.onCreateCity
        const cities = [...this.state.cities.filter(content => {
          return ((content.name !== city.name) && (content.description !== city.description))
        }), city]
        console.log('eventData : ', eventData)
        this.setState({cities})
      }
    });
...省略...
```
<img width="1197" alt="スクリーンショット 2018-10-04 0.29.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/f371619c-9c32-8700-202c-b5e50261e3bd.png">

以上で、Query / Mutation / Subscription のすべての実装が完了しました。


## 認証機能の追加

Amazon Cognito という認証サービスを利用して認証機能を追加していきます。
https://aws.amazon.com/jp/cognito/
ターミナルから amplify cli を使ってcognitoを作成します。amplify add authの後に設定をきかれますが、今回は default configuration を使います。

```
$ amplify add auth
$ amplify push
```

ブラウザでcognitoのマネージメントコンソールを確認すると新たにユーザープールができていることが確認できます。
これで認証のバックエンドの準備が完了しました。ソースコードを修正して、認証していない場合はログインページを表示するという実装をおこないます。amplify にはHOCでAuthenticatorが準備されているため、これを利用します。

App.jsを変更していきます。export default Appを 以下のように変更します。

```
import { withAuthenticator } from "aws-amplify-react";

...　省略 ...

export default withAuthenticator(App, {includeGreeting: true});
```

これで、認証していない場合はログインページが表示され、ログインしていると最初のページが表示されるようになりました。
<img width="815" alt=" .png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/f21003d7-8884-3f34-1289-f508122b2a19.png">
<img width="1102" alt="スクリーンショット 2018-10-04 0.57.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/28614/094eb843-ce0a-95a5-7a5b-26390f8f4e07.png">

ここまでで、認証機能の実装とAppSyncへのリクエストがそれぞれ完了しています。ただ、AppSyncは現状API Keyでのリクエストになっており、この認証と連動していません。最後に認証ユーザーのみを許可するような形にAppSyncを修正していきます。

## AppSyncを未認証では接続できないよう修正

amplify-cli で AppSyncを更新します。authorization typeをCognito User Poolで選択します。これにより先ほど設定したUser Pool で認証するよう AppSync を更新します。

```
$ amplify update api
? Please select from one of the below mentioned services GraphQL
? Choose an authorization type for the API Amazon Cognito User Pool
```


確認のため、SignOut をします。さらに未認証状態で叩けることを確認すうために App.js から withAuthenticator を外します。

```
// export default withAuthenticator(App, {includeGreeting: true});
export default App;
```

この状態でブラウザを開くとデータがとってこれていないことが確認できます。
最後に withAuthenticatorに戻して終了です。

## 免責
本投稿は、個人の意見で、所属する企業や団体は関係ありません。
また掲載しているsampleプログラムの動作に関しても保障いたしませんので、参考程度にしてください。