Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(Rails編)

# はじめに

本記事はAPIをRailsの[APIモード](https://railsguides.jp/api_app.html)で開発し、フロント側をVue.js 3で開発して、認証基盤に[devise_token_auth](https://github.com/lynndylanhurley/devise_token_auth)を用いてトークンベースの認証機能付きのSPAを作るチュートリアルになります。

RailsやVue.jsの環境構築には触れませんのであしからず。

Vue.js編(その1)はこちら
https://qiita.com/kk-icare/items/401114a1284f8e9ca135

# Rails newでAPI作成

今回はsample-apiというプロジェクト名とします。

```terminal
$ rails new sample-api -d postgresql -T --api

$ cd sample-api

$ rails db:create
```

DBはPostgreSQLを使用し、testディレクトリの生成をスキップし、APIモードでRailsアプリの雛形を作成するように実行します。

--api を指定することで、以下の設定がなされます。

>
・利用するミドルウェアを通常よりも絞り込んでアプリケーションを起動するよう設定します。特に、ブラウザ向けアプリケーションで有用なミドルウェア（cookiesのサポートなど）を一切利用しなくなります。
>
・ApplicationControllerを、通常のActionController::Baseの代わりにActionController::APIから継承します。ミドルウェアと同様、Action Controllerモジュールのうち、ブラウザ向けアプリケーションでしか使われないモジュールをすべて除外します。
>
・ビュー、ヘルパー、アセットを生成しないようジェネレーターを設定します。

出典: [Rails による API 専用アプリケーション](https://railsguides.jp/api_app.html)

# deviseおよびdevise_token_authのインストール

Gemfileを編集します。

```ruby
gem 'jbuilder'
gem 'rack-cors'
gem 'devise'
gem 'devise_token_auth'

$ bundle
```

jbuilderおよびrack-corsはデフォルトでコメントアウトされているので、それを外せばOKです。

簡単に使用意図を説明すると、[jbuilder](https://github.com/rails/jbuilder)はRailsでJSONをシンプルに扱うため、[rack-cors](https://github.com/cyu/rack-cors)はCORS(Cross Origin Resource Sharing)の設定を簡単に行うために導入しています。

CORSとはなんぞや？については徳丸浩さんのYou Tubeが詳しかったです。
[CORSの原理を知って正しく使おう](https://www.youtube.com/watch?v=ryztmcFf01Y)

bundle installが終了したら、以下のコマンドを実行します。

```terminal
sample-api $ rails g devise:install

sample-api $ rails g devise_token_auth:install User auth
```

上記のコマンドを実行すると、deviseのinitializerファイルやlocaleファイルが作成されます。
deviseの詳しい使い方については以下の記事が非常に詳しいです。

[Devise入門64のレシピ その1](https://qiita.com/shita1112/items/8b1b66705534d508cb8b)

次に作成されたmigrationファイルを修正します。

```ruby
## Rememberable
t.datetime :remember_created_at

# 追加
## Trackable
t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string   :current_sign_in_ip
t.string   :last_sign_in_ip

## Confirmable
t.string   :confirmation_token
t.datetime :confirmed_at
t.datetime :confirmation_sent_at
t.string   :unconfirmed_email # Only if using reconfirmable
```

修正したら以下のコマンドを実行します。

```
$ rails db:migrate
```

# rack-corsの設定

config/initializers/cors.rbを以下のように編集します。

```ruby
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:8080'
    resource '*',
    headers: :any,
    expose: ['access-token', 'expiry', 'token-type', 'uid', 'client'],
    methods: [:get, :post, :options, :delete, :put, :patch, :head]
  end
end
```

上記の設定を行うことで、異なるオリジン(localhost:8080 から localhost:3000への通信等)間での通信を行うことができるようになります。

ワイルドカード(*)で指定すると、どのオリジンからの通信も許可してしまうようになり、[SameOriginPolicy](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)が意味を為さなくなってしまうため、ワイルドカードでの指定は避けましょう。

# APIの疎通確認を行う

デフォルトの設定だと、リクエスト毎にtokenが更新されてしまうので、configをいじります。

```ruby
# config/initializers/devise_token_auth.rb

  # By default the authorization headers will change after each request. The
  # client is responsible for keeping track of the changing tokens. Change
  # this to false to prevent the Authorization header from changing after
  # each request.
  config.change_headers_on_each_request = false # <= コメントアウトを外す

  # By default, users will need to re-authenticate after 2 weeks. This setting
  # determines how long tokens will remain valid after they are issued.
  config.token_lifespan = 2.weeks # <= コメントアウトを外す

# コメントアウトを外す
config.headers_names = {:'access-token' => 'access-token',
                        :'client' => 'client',
                        :'expiry' => 'expiry',
                        :'uid' => 'uid',
                        :'token-type' => 'token-type' }
```

ここまでできたら、認証機能を試してみましょう。

まずは以下のコマンドを実行してください。

```ruby
$ rails c

> User.create!(name: 'テストユーザー', email: 'test-user+1@example.com', password: 'password')
```

次に、rails s コマンドでローカルサーバーを起動し以下のcurlコマンドを実行します。

```terminal
$ curl -D - localhost:3000/auth/sign_in -X POST -d '{"email":"test-user+1@example.com", "password":"password"}' -H "content-type:application/json"
```

ログインに成功すると、以下のような値が返ると思います。

```
HTTP/1.1 200 OKn"
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
access-token: q1wL0eAS3IwKGcs5-8vEyA
token-type: Bearer
client: sd74van0pd3Sxs4O-fowvQ
expiry: 1641540499
uid: test-user+1@example.com
ETag: W/"12ac3053b26f91ca234280ac13a0790c"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 707fe01b-d25a-4167-b0f2-95e009c9271a
X-Runtime: 0.403161
Vary: Origin
Transfer-Encoding: chunked

{"data":{"id":1,"email":"test-user+1@example.com","provider":"email","uid":"test-user+1@example.com","allow_password_change":false,"name":"テストユーザー"}}
```

access-tokenやclientの値はログインしているかどうか、の判定に用いることになります。

curlで指定しているオプションについて説明しておくと、

```ruby
-D - # レスポンスヘッダーを表示する。
-X   # HTTPメソッドの指定に用いる
-d   # POSTリクエストでRequest bodyにdataを入れて送信
-H   # ヘッダ情報の付与
```

を行っています。

# tokenの検証を行う

devise_token_authは、token検証のための機能も提供しています。

先ほど取得したtokenを活用して、以下のcurlリクエストを送ってみます。

```terminal
$ curl -D - -H "access-token:取得したtoken" -H "client:取得したclient" -H "expiry:取得したexpiry" -H "uid:取得したuid" -H "content-type:application/json" localhost:3000/auth/validate_token
```

成功すると以下のレスポンスが返る

```
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
access-token: q1wL0eAS3IwKGcs5-8vEyA
token-type: Bearer
client: sd74van0pd3Sxs4O-fowvQ
expiry: 1641540499
uid: test-user+1@gmail.com
ETag: W/"f3e45c8f2942619bd67981aead0bc740"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0b9e57df-1f3b-4597-9c0f-01a6b3f904be
X-Runtime: 0.086486
Vary: Origin
Transfer-Encoding: chunked

{"success":true,"data":{"id":1,"provider":"email","uid":"test-user+1@example.com","allow_password_change":false,"name":"テストユーザー"}}
```

# scaffoldで簡単なCRUDを行う

deviseを用いているので、authenticate_user!メソッドが使えるようになります。
なので、scaffoldを用いて作成したcontrollerにauthenticate_user!メソッドを記述して、
きちんと認証判定が行われるかをテストしてみましょう。

```ruby
$ rails g scaffold Post title:string body:text user:references
Running via Spring preloader in process 21
      invoke  active_record
      create    db/migrate/20210112131218_create_posts.rb
      create    app/models/post.rb
       error    rspec [not found] # Rspecを導入していないため
      invoke  resource_route
       route    resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
       error    rspec [not found] # Rspecを導入していないため
      invoke    jbuilder
      create      app/views/posts
      create      app/views/posts/index.json.jbuilder
      create      app/views/posts/show.json.jbuilder
      create      app/views/posts/_post.json.jbuilder
```

いくつか編集を加えます。

```ruby
# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :update]
  before_action :authenticate_user! # authenticate_user!を各アクション実行前に呼ばれるようにする

  def index
    @posts = Post.all
  end

  def show
  end

  def create
    @post = Post.new(post_params)

    if @post.save
      render :show, status: :created # locationを削除
    else
      render json: @post.errors, status: :unprocessable_entity
    end
  end

  def update
    if @post.update(post_params)
      render :show, status: :ok # locationを削除
    else
      render json: @post.errors, status: :unprocessable_entity
    end
  end

  private
    def set_post
      @post = Post.find(params[:id])
    end

    def post_params
      params.permit(:title, :body).merge(user: current_user) # require を削除し、current_userをmerge
    end
end
```

```ruby
# config/routes.rb

  # 自動で追加されたresources :posts は消す

  scope format: 'json' do # json形式のリクエストに対応
    resources :posts
  end
```

```ruby
# app/models/user.rb

has_many :posts, dependent: :destroy
```

```ruby
# app/views/_post.json.jbuilder

json.extract! post, :id, :title, :body, :created_at
json.user_name post.user.name
# json.url post_url(post, format: :json)を削除する
```

この状態でマイグレーションを実行してください。

```
$ rails db:migrate
```

先ほどと同様、curlコマンドで動作確認をしてみます。

```ruby
# createアクション
$ curl localhost:3000/posts -X POST -d '{"title":"レビュー", "body":"面白い"}' \
-H "content-type:application/json" \
-H "access-token:取得したaccess-token" \
-H "client:取得したclient" \
-H "expiry:取得したexpiry" \
-H "uid:取得したuid"

{"id":1,"title":"レビュー","body":"面白い","user_name":"テストユーザー","created_at":"2021-01-12T22:27:28.290+09:00"}

# indexアクション

$ curl localhost:3000/posts -H "content-type:application/json" \
-H "content-type:application/json" \
-H "access-token:取得したaccess-token" \
-H "client:取得したclient" \
-H "expiry:取得したexpiry" \
-H "uid:取得したuid"

[{"id":1,"title":"レビュー","body":"面白い","user_name":"テストユーザー","created_at":"2021-01-12T22:27:28.290+09:00"}
]
```

きちんと認証をパスしていて、正常にjsonを取得することができました。

次回はVue.js側を実装していきます。