Strapiに認証機能を追加する（特定のユーザしかAPIを呼べない仕様に変更する）


## Strapi APIに認証機能を追加してみる

前回の記事「[Strapi (Headless CMS)を使ってみたので使い方まとめ + GraphQL](https://qiita.com/mtoyopet/items/46d3f6edbaaed4f0065c)」でHeadless CMS「Strapi」を使って簡易APIを作ってみました。
今回は特定のユーザしかAPIを呼び出せない様に、APIに認証を付けていきたいと思います。

ちなみに「[公式サイト](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html)」に詳しく説明が載っているので、英語が読める方はこちらを読んで進めることを強くお勧めします。

この認証機能を使ったVueアプリのレポジトリーはこちらです
・ [レポジトリ](https://github.com/mtoyopet/strapi-vue-app-cdn)

## 事前知識
Strapiでは、「ロールと権限」というプラグインを使って認証を行います。（このプラグインは、デフォルトでインストールされいるはずです。）

このプラグインでは、「[JSON Web Token (JWT)](https://ja.wikipedia.org/wiki/JSON_Web_Token)」を使い認証を行います。

### JSON Web Tokenとは
JSON Web Token(JWT)とは、JSONというデータ構造で情報が表現されたフォーマットです。安全にデータを運ぶために使用されます。JWTの詳しい説明は、下記の記事がわかりやすいです。
・[JSON Web Token の効用](https://qiita.com/kaiinui/items/21ec7cc8a1130a1a103a)
・[認証におけるJWTの利用について
](https://qiita.com/shnmorimoto/items/a38690929d7d84bbdea6)

ということで、StrapiはJWTを使って認証を行います。
APIリクエストが送信されると、サーバが「認証ヘッダーがあるか」と「ユーザからのリクエストは、リソースにアクセス可能なものか」を確認します。JWTにユーザIDが含まれているため、ユーザがどのグループに入っているか確かめることができます。結果としてリクエストされているルートにアクセスが可能かどうかの判断ができます。

## ロールの種類
Strapiのロールには大きく分けて「public」と「authenticated」の２種類があります。それぞれのロールを説明していきます。

### Public
「認証ヘッダ」がリクエストに含まれていない時に、このロールが使用されます。「全ての人」がこのロールに設定したエンドポイントにアクセス可能です。「find」や「findOne」などはこのpublicロールを使うといいと思います。

### Authenticated
このロールでユーザがどのルートにアクセスできるかなど定義が可能です。ユーザを作成したときにデフォルトで付与されるのがこのロールです。

## ユーザを作成する
Authenticatedのロールを持つユーザを作成してみます。

[公式サイト](https://strapi.io/documentation/3.0.0-beta.x/plugins/users-permissions.html#registration)にも書いてますが、基本的には下記のコードでユーザを作成できます。簡単！

```javascript
import axios from 'axios';

axios
  .post('http://localhost:1337/auth/local/register', {
    username: 'Strapi user',
    email: 'user@strapi.io',
    password: 'strapiPassword',
  })
  .then(response => {
    // Handle success.
    console.log('Well done!');
    console.log('User profile', response.data.user);
    console.log('User token', response.data.jwt);
  })
  .catch(error => {
    // Handle error.
    console.log('An error occurred:', error);
  });
```

### 実装する
今回は私が練習用に作ったチェーン店メモアプリの「チェーン店めもったろう君」で上記の関数を実行してみます。（このアプリは[Vuetify](https://vuetifyjs.com/en/)を使っているので、htmlタグはVeutify仕様になっています。）

「ユーザ登録」ボタンを押すと、usernameが「momoko1」のユーザがadminに登録されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/27d5bbc9-1aa9-0253-b493-146f55c29c07.png)



```html:index.html
<v-row>
  <v-col cols="12" class="mt-4 d-flex justify-center">
    <v-btn 
    class="success"
    v-on:click="createUser"
    >
    ユーザ登録
    </v-btn>
  </v-col>
</v-row>
```

```main.js

async createUser(){
  axios
  .post('http://localhost:1337/auth/local/register', {
    username: 'momoko1',
    email: 'momoko1@strapi.com',
    password: 'password',
  })
  .then(response => {
    console.log('Well done!');
    console.log('User profile', response.data.user);
    console.log('User token', response.data.jwt);
  })
  .catch(error => {
    console.log('An error occurred:', error);
  });
}
```

「ユーザ登録」ボタンを押すと、User profileとトークンが返ってきました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/db7d134b-7c9b-3884-9731-74ba9aea33df.png)


strapiから「ロールと権限」→ Authenticatedにいくと・・・**できてる！**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/4c3f8ff4-a3a1-97cc-6d92-f0d6fa728346.png)

このようにしてAuthenticatedのロールを持つユーザ作成ができます。

## ログインしないとCRUD操作ができない様にする
それでは、ログインをしていないとチェーン店メモったろう君でチェーン店の登録も、データの取得もできない様にしていきます。

### Authenticatedに権限を付与
まずは「ロールと権限」→ Publicから権限を全て外します。その次にAuthenticatedの権限で必要なCRUD操作にチェックを入れます。これでログインしていないユーザは何も操作ができない様になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/0b9cf6f7-d2e8-bc28-f68d-bf59474a2422.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/d5131f33-3e68-d699-9472-32350d95efab.png)

### 実装する

```html:index.html
<v-col>
  <v-form>
    <v-row
      justify="center"
    >
      <v-col
      cols="10"
      md="4"
      >
      <v-text-field
        v-model="email"
        label="Eメールアドレス"
      ></v-text-field>
      </v-col>
      <v-col
        cols="10"
        md="4"
      >
        <v-text-field
          v-model="password"
          label="パスワード"
        ></v-text-field>
      </v-col>
      <v-col
        md="1"
      >
        <v-btn 
          class="success"
          v-on:click="login"
          >
          ログイン
        </v-btn>
      </v-col>
    </v-row>
  </v-form>
</v-col>
```

```javascript:main.js
async login() {
  axios
    .post('http://localhost:1337/auth/local', {
      identifier: this.email,
      password: this.password,
    })
    .then(response => {
      console.log('Well done!');
      console.log('User profile', response.data.user);
      console.log('User token', response.data.jwt);
    })
    .catch(error => {
      console.log('An error occurred:', error);
    });
}
```

先ほどのユーザ登録の際に使用したEメールアドレスとパスワードを使用してログインしてみます。ちなみに、``identifier``に入れるのはemailでもusernameでも良い様です。

[![Image from Gyazo](https://i.gyazo.com/07fc11374494b87141c73a83aa36bdf5.gif)](https://gyazo.com/07fc11374494b87141c73a83aa36bdf5)

ログインが成功すると、こんな感じでUser Tokenがレスポンスとして返ってきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/8362e95d-6bdb-709a-c55b-dbc12bbe11d5.png)


このトークンをリクエストヘッダに含めて認証を行い、認証が通ればCRUD操作ができる、という仕組みです。tokenという変数にresponse.data.jwtを格納する様にコードを修正します。

```javascript:main.js
async login() {
  axios
  .post('https://powerful-dusk-72165.herokuapp.com/auth/local', {
    identifier: this.email,
    password: this.password,
  })
  .then(response => {
    this.token = response.data.jwt
    this.getRestaurants() // ログインが成功したらレストランデータをとる関数を動かす
  })
  .catch(error => {
    console.log('An error occurred:', error);
  });
},
```

あとは、

```javascript:main.js
// 修正後
async getRestaurants(){
  try {
    var result = await axios({
      method: "POST",
      url: this.apiURL,
      headers: {
        Authorization: `Bearer ${this.token}`,
      },
      data: {
        query: `
          query getRestaurants {
            restaurants {
              id
              name
              description
            }
          }
        `
          }
      });
      this.restaurants = result.data.data.restaurants;
  } catch (error) {
    console.error(error);
  }
}   

// 修正前
async getRestaurants(){
  try {
    var result = await axios({
      method: "POST",
      url: this.apiURL,
      data: {
        query: `
          query getRestaurants {
            restaurants {
              id
              name
              description
            }
          }
        `
          }
      });
      this.restaurants = result.data.data.restaurants;
  } catch (error) {
    console.error(error);
  }
}    
```

**できた！**
[![Image from Gyazo](https://i.gyazo.com/1ba22a588dd71a52a1205231255a80e4.gif)](https://gyazo.com/1ba22a588dd71a52a1205231255a80e4)

Strapiでユーザ認証をしてAPIを呼び出す方法でした。結構簡単でびっくり。SNSログインもできるみたいなので、また今度Qiita記事を書きながら試してみたいと思います。
[公式サイト](https://strapi.io/documentation/developer-docs/latest/getting-started/introduction.html)のドキュメントがとても丁寧なのもStrapiの魅力ですね。好きになっちゃいそう・・・。