35
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

Nuxt.js + GraphQL + Ruby on Railsで作ったアプリにJWT認証を追加する方法

本記事ではフロントエンドに Nuxt.js(Vue.js)、バックエンドに Ruby on Rails 、APIに GraphQL を採用したアプリケーションに、JWTトークンによる認証 を追加する方法についてまとめます。

login.gif

題材

サンプルとして以下チュートリアルで作成したToDoリストを使用します。

Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(前編)
Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(後編)

JWTでの認証フロー

今回実装するJWTでの認証フローを図にまとめました。

Untitled_graphql-jwt_-_Cacoo.png

トークンの発行・保存・付与・検証がめんどくさそうに見えるかもしれませんが、フロントエンド側はAuth Moduleが、バックエンド側はknockがトークンをいい感じに処理してくれるので、安心してください。

実装(バックエンド)

ユーザモデルを定義する

認証単位となるモデル(User)を生成します。

$ bundle exec rails g model user email:string password_digest:string
$ bundle exec rails db:migrate

Userはパスワードをハッシュ化して管理するので、モデルに has_secure_password を宣言します。

app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end

また、Gemfileの bcrypt のコメントを外します。

Gemfile
# ・・・中略・・・

# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7

# ・・・中略・・・

テスト用のUserを seed.rb に追記します。

db/seeds.rb
# ・・・中略・・・

User.create(
  email: 'test@example.com',
  password: 'xxxxxxxx',
  password_confirmation: 'xxxxxxxx'
)
$ bundle exec rails db:seed

knockをインストールする

Gemfileにknockを追加し、インストールします。

Gemfile
gem 'rack-cors'
gem 'graphql'
gem 'knock' # ★追加
$ bundle install

Rails6だとknockのautoloadに失敗するのでinitializerで明示的にrequireします。

config/initializers/eager_load_knock.rb
require 'knock/version'
require 'knock/authenticable'

ジェネレータを実行します。

$ rails generate knock:install

ログイン/ログアウトのエンドポイントを準備

ジェネレータでControllerを生成します。

$ rails generate knock:token_controller user

各Controllerにて認証処理を行えるようにするため、 ApplicationController にてmoduleをincludeします。

app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  include Knock::Authenticable
end

GraphqlController のbefore_actionとして認証処理を追加します。

app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  before_action :authenticate_user

  # ・・・中略・・・
end

今回、セッションは使わないのとCORS設定済みであることを考慮して、CSRF対策を解除します。

config/application.rb
# ・・・中略・・・

module RailsNuxtGrapshqlTodoapp
  class Application < Rails::Application
    config.load_defaults 6.0
    config.api_only = true
    config.action_controller.default_protect_from_forgery = false 
  end
end

動作確認(バックエンド)

サーバ起動してInsomniaを使ってリクエストを送信してみます。

$ bundle exec rails s

未認証の状態でGraphQL Queryを送信しても、401 が返ってきます。

Insomnia__onescene__–_tasks.png

/user_token へリクエストすると、JWTトークンが返ってきます。

Insomnia__onescene__–_user_token.png

Bearerの設定でTOKEN欄に上記JWTトークンを記載します。

Insomnia__onescene__–_tasks.png

この状態で再度GraphQL Queryを送信すると、 200 が返ってきました。
JWT認証が機能していますね。

Insomnia__onescene__–_tasks.png

実装(フロントエンド)

Auth Moduleをインストールする

npmでインストールします。
Auth Moduleは store/index.js が存在していないとエラーを出すので、空のファイルを作成しておきます。

$ npm install @nuxtjs/auth @nuxtjs/axios
$ touch store/index.js

nuxt.config.jsmodules, axios, auth, apollo を追記します。

  // ・・・中略・・・
  modules: [
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module',
    '@nuxtjs/apollo',
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],
  axios: {
    baseURL: 'http://localhost:3000/'
  },
  auth: {
    strategies: {
      local: {
        endpoints: {
          login: { url: 'user_token', method: 'post', propertyName: 'jwt' },
          user: false,
          logout: false
        }
      }
    }
  },
  // ・・・中略・・・
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'http://localhost:3000/graphql',
        getAuth: () => ''
      }
    }
  }
  // ・・・中略・・・

ログイン画面を準備

rails_nuxt_grapshql_todoapp_front_-_rails_nuxt_grapshql_todoapp_front.png

/login に相当する画面およびログイン処理を実装します。

pages/login.vue
<template>
  <v-container>
    <v-row>
      <v-col cols="6" offset="3">
        <v-card>
          <v-card-title>Login</v-card-title>
          <v-card-text>
            <v-form>
              <v-container>
                <v-row>
                  <v-col cols="6">
                    <v-text-field v-model="email" label="Email" required />
                  </v-col>
                </v-row>
                <v-row>
                  <v-col cols="6">
                    <v-text-field v-model="password" type="password" label="Password" required />
                  </v-col>
                </v-row>
                <v-row>
                  <v-col>
                    <v-btn @click="login()">
                      Login
                    </v-btn>
                  </v-col>
                </v-row>
              </v-container>
            </v-form>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
export default {
  middleware({ store, redirect }) {
    window.console.log(store.state.auth.loggedIn)
    if (store.state.auth.loggedIn) {
      return redirect('/')
    }
  },
  data() {
    return {
      email: '',
      password: ''
    }
  },
  methods: {
    async login() {
      try {
        await this.$auth.loginWith('local', {
          data: {
            auth: {
              email: this.email,
              password: this.password
            }
          }
        })
        await this.$apolloHelpers.onLogin(this.$auth.getToken('local').match(/^Bearer[ ]+([^ ]+)[ ]*$/i)[1])
        this.$router.push('/')
      } catch (e) {
        window.console.log(e)
      }
    }
  }
}
</script>

無名middlewareを用いることで、既にlogin済みの状態でこのページを開くと、'/' でリダイレクトするようにしています。

methodsの login() がLOGINボタンを押したときの処理です。
Auth Moduleでログインをした後で、Apollo ModuleへJWTトークンをセットしています。

ログアウトボタンを追加

rails_nuxt_grapshql_todoapp_front_-_rails_nuxt_grapshql_todoapp_front.png

ログイン中の場合のみ、メニューバーにログアウトボタンを表示します。

layouts/default.vue
<template>
  <v-app>
    <v-app-bar app>
      <v-toolbar-title v-text="title" />
      <div class="flex-grow-1" />
      <span v-if="loggedIn" @click="logout()">Logout</span>
    </v-app-bar>
    <v-content>
      <nuxt />
    </v-content>
    <v-footer center>
      <v-layout justify-center>
        <span>&copy; 2019 Yuhei Okazaki. All Rights Reserved.</span>
      </v-layout>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      title: 'Tasks'
    }
  },
  computed: {
    loggedIn() {
      return this.$auth.loggedIn
    }
  },
  methods: {
    async logout() {
      try {
        await this.$auth.logout()
        await this.$apolloHelpers.onLogout()
        this.$router.push('/login')
      } catch (e) {
        window.console.log(e)
      }
    }
  }
}
</script>

methodsの logout() がLOGOUTを押したときの処理です。
Auth Moduleでログアウトをした後で、Apollo Moduleもログアウトしています。

未認証時のリダイレクトを追加

このままだと、ログインしていない状態でもタスク一覧画面を開けてしまうので、ログインしていないときには /login へ飛ばすようmiddlewareを設定します。

pages/index.vue
// 中略
export default {
  middleware: 'auth'
  // 中略
}

動作確認(全体)

冒頭の画像のように、ログインしたときのみタスク一覧が表示されます。

login.gif

まとめ

本記事ではバックエンドにRuby on Rails、フロントエンドにNuxt.js、APIにGraphQLを採用したアプリケーションにJWT認証を追加しました。
新規登録画面やタスクとユーザの紐付け等、未実装の処理は多数あるものの、認証というアプリケーションを実装するときの最初の壁は越えられたかと思います。

knockやAuth Module、Apollo Moduleを用いたことで、トークン操作を意識せず簡単に認証追加できたので、ぜひお試しください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
35
Help us understand the problem. What are the problem?