見出し画像

Rails devise token authとNuxt.jsを連携(Twitter認証)

くこの続きです☺️

環境
・Rails 6.0.0(APIモード)
・devise token auth 1.1.3
・vue/cli 4.2.3
・Nuxt.js 2.4.2

ようやく管理サイトのクライアント側実装にとりかかれました🥺
前回API側のトークン認証機能を作ったのでそれをNuxt.jsと連携させます
管理サイト(自分しか使わない)なので土台を1から作るのは面倒なので、Nuxt.js + Vuetifyで作られている管理サイトテンプレートないか探してみました↓

デザインフレームワークにVuetifyを選んだのはコンポーネントが多いからでそれ以外とくに気にしなかったです

テンプレートの導入はReadmeに載ってる手順をそのまま実行していけばOKでした
ただあらかじめnode、npm、vue/cli、vue/cli-init入れる必要があります

npm run devで起動してみるとポートが3000で起動するようで、Railsのデフォルトポートとかぶってたのとlocalhostだけバインドしてたので設定変えました

# nuxt.js.config
...

module.exports = {

...

 server: {
   port: 8080,
   host: '0.0.0.0'
 },

...

アクセスするとよさげな管理サイトが立ち上がりました

コメント 2020-03-09 211413

本題のdevise_token_authとNuxt.jsを連携させていきます

以下がとても参考になりました

Nuxt.js側構築

まずトークン認証させるために必要な(便利な)モジュールをインストール

$ npm i --save @nuxtjs/axios
$ npm i --save cookie-universal-nuxt

有効化とaxios基本設定

# nuxt.config.js
module.exports = {

  ...

  modules: [
    '@nuxtjs/axios',
    'cookie-universal-nuxt'
  ],

  ...

  axios: {
    host: 'localhost', // APIドメイン
    port: 3000,
   },

  ...

}

Vuexにユーザーと認証情報を持たせる

# store/index.js

export const state = () => ({
  user: null,
  auth: {},
  drawer: true
})

export const mutations = {
  user(state, value) {
    state.user = value
  },
  auth(state, value) {
    state.auth = value
  },
  toggleDrawer(state) {
    state.drawer = !state.drawer
  },
  drawer(state, val) {
    state.drawer = val
  },
}

そのまんま、userにユーザー情報、authにトークン達を入れとく箱です。

axiosの共通処理を加える

# plugins/axios.js

export default ({ $axios, store, app }) => {
  $axios.onRequest(config => {
    const headers = store.state.auth
    config.headers = headers
  })

  $axios.onResponse(response => {
    if (response.headers['access-token']) {

      const authHeaders = {
        'access-token': response.headers['access-token'],
        'client': response.headers['client'],
        'expiry': response.headers['expiry'],
        'uid': response.headers['uid']
      }
      store.commit('auth', authHeaders)

      const session = app.$cookies.get('session')
      if (session) {
        session.tokens = authHeaders
        app.$cookies.set('session', session, {
          path: '/',
          maxAge: 60 * 60 * 24 * 7
        })
      }

   }
 })

 $axios.onError(error => {
   return Promise.reject(error.response);
 });

}
# nuxt.config.js
module.exports = {

  ...

  plugins: [
   ...
   '~/plugins/axios',
 ],

  ...

}

onRequestでリクエスト時にヘッダーにトークンを入れてあげます
onResponseでレスポンス時にトークンがあればVuexとCookieに保存してあげます(クッキーに入れる理由は後述)

ログイン時の処理

まず元々あるログインページがちょいと派手なので簡素(?)にします

# pages/login.vue
<template>
 <v-app id="login" class="primary">
   <v-content>
     <v-container fluid fill-height>
       <v-layout align-center justify-center>
         <v-flex xs12 sm8 md4 lg4>
           <v-card class="elevation-1 pa-3">
             <v-btn :href="twitterLoginURL">
               Twitterログイン
             </v-btn>
           </v-card>
         </v-flex>
       </v-layout>
     </v-container>
   </v-content>
 </v-app>
</template>

<script>
 export default {
   layout: 'default',
   data: () => ({
     twitter: {
       url: 'http://localhost:3000/auth/twitter',
       redirectUrl: 'http://localhost:8080/oauth/twitter/callback'
     },
   }),

   computed: {
     twitterLoginURL() {
       return `${this.twitter.url}?auth_origin_url=${encodeURI(this.twitter.redirectUrl)}`

     }
   },

 };
</script>

API側のTwitter認証URLに遷移するTwitterログインボタンを設置しているだけです。
こんな感じのリンク↓
http://[API側ドメイン]/auth/twitter?auth_origin_url=[リダイレクト先URL]
リダイレクト先にはクライアント側のコールバック用URLに飛ばしています
で、その飛ばし先のページを作ります

# pages/oauth/twitter/callback.vue

<template>
</template>

<script>
export default {
 async fetch({ query, store, $axios, redirect, app }) {
   const authHeaders = {
     'access-token': query.auth_token,
     'client': query.client_id,
     'uid': query.uid,
     'expiry': query.expiry,
   }

   store.commit('auth', authHeaders)

   const { data } = await $axios.$get('/auth/validate_token')
   store.commit('user', data)

   const session = {
     tokens: authHeaders,
     user: data
   }

   app.$cookies.set('session', session, {
     path: '/',
     maxAge: 60 * 60 * 24 * 7
   })
   redirect(301, '/')
 }
}
</script>

レンダリングする必要ないのでfetch内で処理しています
APIがここへコールバックする時にURLパラメータにトークンが入っているのでそれを取得してVuexに保存。
で、そのトークン使って/auth/validate_tokenでユーザー情報持ってきてVuexに保存。
で、トークンとユーザー情報をクッキーに入れてTOPページにリダイレクトしています。
クッキーに入れる理由はページリロードしちゃうとVuexが初期化されるので、保持しておくtemp的役割を担っています。
LocalStorageに入れてもできますが、セキュリティ的にまずいのでクッキーのがよいです。

ページリロード時にクッキーをVuexに入れる

# pulgins/cookie.vue

export default ({ store, app, $axios }) => {
  const session = app.$cookies.get('session')
  if (session) {
    store.commit('user', session.user)
    store.commit('auth', session.tokens)
  }
}
# nuxt.config.js
module.exports = {

  ...

  plugins: [
    ...
    '~/plugins/cookie',
  ],

  ...

}

とてもシンプル

ログイン状態をわかるようにする

# components/AppToolbar.vue
<template>

...

  <img :src="user.image" :alt="user.name"/>

...

</template>

<script>
export default {

...

  computed: {
   user() {
     return this.$store.state.user || {}
   },
   ...
 },

...


}
</script>

ちょっと分かりづらいですが要はcomputedでvuexにあるuser情報持ってきて右上のアバターを動的にしてるだけです

未ログイン時のアクセス制御

# pulgins/redirect.js

export default function ({ store, route, redirect }) {

  if (store.state.user) return Promise.resolve()

  if (route.path === '/login') return Promise.resolve()

  if (route.path.match(/^\/oauth\/.+\/callback$/)) return Promise.resolve()

  window.location.href = '/login'
  return new Promise((resolve) => {})
}


# nuxt.config.js
module.exports = {

  ...

  plugins: [
   ...
   '~/plugins/redirect',
 ],

  ...

}

ログインしてないとログインページとコールバックページ以外はログインページへリダイレクトしてます。
やや特殊な書き方になっちゃった理由については以下参照ください(window.location.hrefとか...)

ログイン済みかどうかはVuexの値でチェックしてるのでセキュリティ的に微妙ですがこのサイト自体にIP制御やらBasic認証やらかけるのでいいでしょう・・・。(もちろんAPIは通信できません)

ログアウト

# components/AppToolbar.vue

...

<script>
export default {

...

  methods: {
   async handleLogout() {
     await $axios.$delete('auth/sign_out')
     this.$cookies.removeAll()
     this.$store.commit('user', {})
     this.$store.commit('auth', null)
     this.$router.push('/login')
   },
   ...
 },

...


}
</script>

APIにトークン消すリクエスト出してクッキーとVuexを初期化してるだけです。
ログアウトの共通処理作ったりログアウト用mutations追加とかするとよりよさそうです。

Rails側の改修​

前回の記事で作りましたが、ちょっとだけ改修する部分がありました。

change_headers_on_each_request​について

これ、当初の予定ではtrueにしてトークンを毎回変えてく予定でしたがなくしました。(falseへ)
trueでもログインしてAPI飛ばして認証できてるところまで確認できたのですが、ログアウトするとエラー出たり複数端末でログインすると片方切れたりと・・・、改善の調査に凄い時間かかりそうだったので無しにしました。
いい方法あれば教えてください🙏

返すユーザー情報のフィルタ

# app/models/user.rb

# frozen_string_literal: true

class User < ActiveRecord::Base
 extend Devise::Models
 # Include default devise modules. Others available are:
 # :confirmable, :lockable, :timeoutable and :omniauthable
 devise :database_authenticatable,
   :registerable,
   :recoverable,
   :rememberable,
   :trackable,
   :validatable,
   :omniauthable

 include DeviseTokenAuth::Concerns::User

 def token_validation_response
   as_json(only: [:id, :uid, :allow_password_change, :name, :nickname, :image])
 end

end

token_validation_responseをオーバーライドして、持ってくるデータをフィルターしました。
is_adminとかトークンとか持ってきちゃってたのが嫌だったので。

これにて完了です。
ログインの様子

画像2

次はこの管理テンプレートでいらないもの消してってモンスターのCRUDテーブル実装してこうかなと思います。それでは👋😉



この記事が気に入ったらサポートをしてみませんか?