【Nuxt.js】Auth Moduleでログイン機能を実装する （ + Ruby on Rails・devise_token_authで認証を行う）

## この記事の目的

Nuxt.jsアプリのAuth Module使ってログイン機能を実装するための記事です。

また、記事の最後にはRuby on Railsの`devise_token_auth`を使ってトークンベースで認証する方法も記載しています。

## 前提
- Nuxt.jsのアプリが用意されていること
- Nuxt.jsアプリとログインAPI用のエンドポイントは`POST`メソッド・`/auth/sign_in`とする

## ログインの流れ
1. **【ユーザ】** Nuxtアプリのログイン画面でEメールとパスワードを入力し、「送信」ボタンを押す
3. **【Nuxtアプリ】** パスワードとEメール情報を付与し、APIの`/auth/sign_in`にHTTPリクエストを送信する
4. **【API】** NuxtアプリからHTTPリクエストを受け取り、パスワードとEメールで認証をする。認証が完了し、HTTPレスポンスを返す
5. **【Nuxtアプリ】** APIからHTTPレスポンスを受け取る
6. **【Nuxtアプリ】** レスポンスヘッダー情報を`Vuex store`に保存する
7. **【Nuxtアプリ】** ユーザをログイン画面からホーム画面へリダイレクトする
8. **【Nuxtアプリ】** `Vuex store`を確認し、ログイン済みであればログインしていないとアクセスできないページへのアクセスを許可する。ログイン済みでなければ、ログインページにリダイレクトする。

## 事前知識
### Auth Moduleとは

**[Auth Module](https://auth.nuxtjs.org/)**とは、Nuxt.jsプロジェクトにJWT（JSON Web Tokens）またはOAuth認証を使ってログイン機能を簡単に実装できるライブラリです。

ログイン機能は一から実装すると色々なことに気を使わないといけなくて大変なのですが、複雑な仕事を担ってくれているのがこの**Auth Module**です。

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


## ステップ

### Axiosを導入
Axiosを使用するので、Axiosを導入していない場合は下記のコマンドを実行してください。

```terminal
$ yarn add @nuxtjs/axios
または
$ npm install @nuxtjs/axios
```

### Auth Moduleを導入

下記コマンドでAuth Moduleを導入します。



```terminal
$ yarn add @nuxtjs/auth
または
$ npm install @nuxtjs/auth
```

### Vuex store用に`index.js`ファイルを作成
Auth moduleは[Vuex store](https://vuex.vuejs.org/ja/guide/)を使ってユーザの情報やログイン情報を管理します。そのためVuex store用のファイルを用意しておく必要があります。nuxtアプリのルートディレクトリに`Vuex store`というディレクトがあるので、そこに`index.js`という名前のファイルを作成しておきます。`index.js`の中はからっぽで大丈夫です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/6f5891d7-2cd0-cc1e-177a-6a550fb38780.png)


### `nuxt.config.js`を修正

`nuxt.config.js`のmodulesに下記を加えます。

```js:nuxt.config.js
modules: [
  '@nuxtjs/axios',
  '@nuxtjs/auth'
]
```

さらに、`nuxt.config.js`に下記を追加します。

```nuxt.config.js
  axios: {
    baseURL: 'http:localhost:3000'
  },
  auth: {
    redirect: {
      login: '/login', 
      logout: '/login',
      callback: false,
      home: '/'
    },
    strategies: {
      local: {
        endpoints: {
          login: { url: '/auth/sign_in', method: 'post', propertyName: false },
          logout: false,
          user: false
        }
      }
    }  
  }

```

#### **`axios`**
`baseURL`を設定する事で、HTTPリクエストを送信するベースとなるURLが自動で設定されます。
ログインする際のRailsアプリのURLは`/auth/sign_in`のみですが、NuxtアプリでHTTPリクエストを送信する時に、axiosが`http:localhost:3000/auth/sign_in`にURLを自動的に変えてくれます。これを設定しないとうまくいかないので、忘れない様にしましょう。

#### **`redirect`**

- `redirect`: ユーザの状態（ログインしているか否か）でどこにリダイレクトするかなどを設定します。
- `login`: ユーザが未ログイン時、ログインしていないとアクセスできないページにアクセスした際のリダイレクトURLです。
- `logout`: ユーザがログアウトした時のリダイレクトURLです。
- `callback`: Oauth認証等で必要となるコールバック用URLです。今回はOauth認証は使わないので`false`です。
- `home`: ログイン成功後にリダイレクトするURLです。

#### **`strategies`**

- `strategies`: Auth Moduleのどの認証ロジックを使うかを指定します。JWTとCookieを使う`local`と、OAuthを使う`social`の２種類があります。今回はJWTを使って認証を行うので、`local`です。
- `endpoints`: どのメソッドが呼ばれた際にAPIのどのエンドポイントに飛ばすかを指定します。（`logout`はログアウト用のAPIエンドポイントを指定しますが、今回は実装しないのでfalseです。同様にユーザ情報を取得するための`user`もfalseにしています。)


### ログイン画面の作成

次にログイン画面を作っていきます。（今回は、[Vuetify](https://vuetifyjs.com/ja/)というマテリアルデザインフレームワークを使っているので、HTMLタグはVuetify仕様になっています。）

```vuejs:login.vue
<template>
  <div class="mt-3">
    <v-card class="mt-5 mx-auto" max-width="600">
      <v-form ref="form" v-model="valid" lazy-validation>
        <v-container>
          <v-row justify="center">
            <p cols="12" class="mt-3 display-1 grey--text">
              ログイン
            </p>
          </v-row>
          <v-row justify="center">
            <v-col cols="12" md="10" sm="10">
              <v-text-field
                v-model="email"
                label="Eメールアドレス"
              />
              <p class="caption mb-0" />
            </v-col>
          </v-row>
          <v-row justify="center">
            <v-col cols="12" md="10" sm="10">
              <v-text-field
                v-model="password"
                type="password"
                label="パスワード"
              />
            </v-col>
          </v-row>
          <v-row justify="center">
            <v-col cols="12" md="10" sm="10">
              <v-btn
                block
                class="mr-4 blue white--text"
                @click="loginWithAuthModule"
              >
                ログイン
              </v-btn>
            </v-col>
          </v-row>
        </v-container>
      </v-form>
    </v-card>
  </div>
</template>

<script>
export default {
  data () {
    return {
      password: '',
      email: ''
    }
  },
  methods: {
    async loginWithAuthModule () {
      await this.$auth.loginWith('local', {
        data: {
          email: this.email,
          password: this.password
        }
      })
        .then((response) => {
          return response
        },
        (error) => {
          return error
        })
    }
  }
}
</script>


```

すると、下記の様な画面が出来上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/ca16dbc6-fb44-7f90-9bb8-dab3818b2c2e.png)

ここでAPIのUsersテーブルに保存してあるメールアドレスとパスワードを入力しログインボタンを押します。パスワードとメールアドレスは下記の様なJSONとしてAPIに送信されます。

```json
{
  "email": "momoko@test.xom",
  "password": "password"
}
```

認証が通りAPIからHTTPレスポンスが返ってくると、ホーム（`/`）ページにリダイレクトされるはずです。

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

ブラウザのディベロッパーズツールのコンソールで`$nuxt.$store`を見てみます。ログイン後は`$nuxt.$store.state.auth.loggedIn`が`true`になっていることがわかります。

**ログイン前**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/983f7095-6be5-5aa8-e35d-3017ca75e47b.png)



**ログイン後**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/b259301a-c673-705c-6889-d9a1786c2408.png)


### `middleware`を設定する

middlewareを設定する事で、下記を自動化することができます。

- ログインしていなかったら、強制的にログイン画面へリダイレクトする
- ログインしていている状態でログイン画面にアクセスしようとするとホームへリダイレクトする

`nuxt.config.js`でmiddlewareの設定を加えます。

```javascript:nuxt.config.js
  router: {
    middleware: ['auth']
  }
```

これだけ！これでログインしている状態で`/login`にアクセスしようとしてもホームにリダイレクトされるはずです。（サーバの再起動を忘れずに！）

ログインしてない時もこのページにはアクセスさせたい！という場合があると思います。そんなときは、そのページのVueファイルのscriptに`auth: false`を入れてあげましょう。

```vuejs
<script>
export default {
  auth: false,
}
</script>

```

## Ruby on Railsの`devise_token_auth`でログイン実装・認証をする

### ログイン・認証の流れ
Auth Moduleと`devise_token_auth`を使ってのログイン・認証の流れは下記です。

1. **【ユーザ】** Nuxtアプリのログイン画面でEメールとパスワードを入力し、「送信」ボタンを押す
2. **【Nuxtアプリ】** パスワードとEメール情報を付与し、Railsアプリの`/auth/sign_in`にHTTPリクエストを送信する
3. **【Nuxtアプリ】** 認証されるとRailsアプリからレスポンスが返ってくる。レスポンスヘッダーの`accept-token`、`uid`、`client`を情報を`Vuex store`に保存する
4. **【Nuxtアプリ】** ユーザをログイン画面からホーム画面へリダイレクトする
5. **【Nuxtアプリ】** `axios`でHTTPリクエストを送る時に`accept-token`、`uid`、`client`をリクエストヘッダーに付与し認証済みであることをRailsアプリに教える

これが基本的な認証の流れです。

### HTTPレスポンスのヘッダーにアクセスしたい・・・！

さて、**ここから超ハマったパート**です。Ruby on Railのdevise_token_authでユーザ認証をやりとりするには、ログイン成功後にRailsアプリから返ってくるHTTPレスポンスの`access-token`、`client`、`uid`を取り出しVuex storeに保存、axiosでHTTPレスポンスを送信する度にこの3つをヘッダーに付与する必要があります。

この3つを付与せずにHTTPリクエストを送信する場合、Railsアプリは認証されていないと判断し、`401 Unauthorized`のエラーを返します。

ということで、ログインが成功したらHTTPレスポンスのヘッダーにある`access-token`、`client`、`uid`にアクセスし、Vuex storeに保存してあげたい。でも、**この３つにどうやってアクセスしたらええんや！？**

・・・めちゃくちゃはまりました。


調べた結果、[Auth ModuleのIssue](https://github.com/nuxt-community/auth-module/issues/76)に対応策が書いてありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352778/4ddeb3ec-0772-e8e9-9608-f4aaa2a170f9.png)

どうやらAuth Moduleは`localStorage（Vuex store)`に`access-token`、`client`、`uid`などの認証情報を自動的に保存してくれる様です。そのため、ログイン成功時は、何もする必要はありません。

ただし、HTTPリクエストを送る時に`localStorage`からこの３つのデータを取り出し、HTTPリクエストのヘッダーにセットする必要がありますよね。

ということで、`plugins`ディレクトリに`axios.js`を作成し下記のコードを書いたところ、無事認証がされる様になりました！

```javascript:nuxt.config.js
 // configの設定も忘れずに！
  plugins: [
    { src: '~/plugins/axios.js', ssr: false }
  ]
```

```javascript:plugins/axios.js
export default function({ $axios }) {
  $axios.onRequest(config => {
    config.headers.client = window.localStorage.getItem("client")
    config.headers["access-token"] = window.localStorage.getItem("access-token")
    config.headers.uid = window.localStorage.getItem("uid")
    config.headers["token-type"] = window.localStorage.getItem("token-type")
  })

  $axios.onResponse(response => {
    if (response.headers.client) {
      localStorage.setItem("access-token", response.headers["access-token"])
      localStorage.setItem("client", response.headers.client)
      localStorage.setItem("uid", response.headers.uid)
      localStorage.setItem("token-type", response.headers["token-type"])
    }
  })
}
```

他にも方法があるかとは思いますが、今のところはこれが一番簡単そうです。別の方法知ってるよ〜という方がいらっしゃったらぜひ教えてください。