Rails 6+Grapeで作るAPIサーバーにDeviseトークン認証を付ける

# はじめに

iOS・AndroidアプリやWebアプリをクライアントとしたAPIサーバーをRuby on Railsで実装するケースがあります。この記事では、[Grape](http://www.ruby-grape.org/)（REST APIフレームワーク）を利用して作るAPIサーバーに[Devise](https://github.com/heartcombo/devise)（認証機能を提供するgem）を組み合わせ、アクセストークンを介した認証方式を実装する手順を紹介します。

# 同様の紹介事例について

[Devise + Grape で認証付きAPIの実装 - Qiita](https://qiita.com/travelist/items/ec0b08a9a19cbe9ec78b)

上記の記事では本記事と同様に、GrapeとDeviseに加えてトークン認証機能を付加するgemである[devise_token_auth](https://github.com/lynndylanhurley/devise_token_auth)を用いて実装する手法が紹介されています。ただし、

> Token の更新に関してですが、今回の例では一度発行した Token は再度ログインするまで更新されません。

とあるようにdevise_token_authの本来の機能であるリクエストごとにアクセストークンを更新する仕様は有効になっていません。ここでは、上記記事のコードをベースに、Grapeとdevise_token_authの仲立ちをする[grape_devise_token_auth](https://github.com/mcordell/grape_devise_token_auth)を加えることでその点をクリアし、現行のRails 6で動作するサンプルを作ります。

# 前提条件

以降の手順解説では、Ruby 2.6.5とRails 6.0.2.1を使用することを想定しています。

# Railsアプリを作成する

```
# rails new token-auth-sample
```

Railsアプリを新規作成して、以下のGemを追加します。

```ruby:Gemfile.rb
gem 'devise'
gem 'devise_token_auth'
gem 'grape'
gem 'grape_devise_token_auth'
```
```
$ bundle install
```

# Devise関連の環境設定を行う

```
# rails g devise:install
# rails g devise_token_auth:install User auth
```

新たに作成されたUserモデルのマイグレーションを行います。

```
# rails db:migrate
```

# 基本部分のコード修正

```ruby:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken

  protect_from_forgery with: :null_session
end
```

protect_from_forgeryは一般的なRailsアプリにおけるCSRF対策の仕組みに関する設定で、指定しない場合はPOSTリクエストを受け付けた際にActionController::InvalidAuthenticityTokenエラーとなります。これを無効化するには ```protect_from_forgery with: :null_session``` とします。

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  include DeviseTokenAuth::Concerns::User
end
```

Userモデルでは、```devise``` メソッドで有効化する機能が列挙されていますが、 ```:trackable``` が含まれていたら外しておきましょう。前述の ```rails g devise_token_auth:install User auth``` コマンドで作成されたマイグレーションでtrackable用のカラムが作成されておらず、サインインリクエストの際に ```NoMethodError (undefined method `current_sign_in_at' 〜``` が発生するためです。（末尾の参考情報を参照）


```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users

  namespace :api do
    mount_devise_token_auth_for 'User', at: '/v1/auth'

    mount Api::Root => '/'
  end
end
```

# APIエンドポイント部分のコード追加

```ruby:app/api/api.rb
module Api
  class Root < Grape::API
    GrapeDeviseTokenAuth.setup! do |config|
      config.authenticate_all = true
    end

    mount V1::Root
  end
end
```

```ruby:api/v1/root.rb
module V1
  class Root < Grape::API
    version 'v1', using: :path
    format :json

    auth :grape_devise_token_auth, resource_class: :user

    helpers GrapeDeviseTokenAuth::AuthHelpers

    desc 'GET /api/v1/test'
    get 'test' do
      authenticate_user!

      {
        message: 'test',
        current_user_uid: current_user.uid,
        authenticated?: authenticated?,
      }
    end
  end
end
```

ここまでで、以下のエンドポイントが利用可能になっています。

* ユーザーアカウント作成
    * ```POST http://localhost:3000/api/v1/auth```
* サインイン
    * ```POST http://localhost:3000/api/v1/auth/sign_in```
* サンプルGETリクエスト
    * ```GET http://localhost:3000/api/v1/test```

# 動作確認

ローカルサーバーを起動しておきます。

```
# rails s
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.1 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://127.0.0.1:3000
* Listening on tcp://[::1]:3000
Use Ctrl-C to stop
```

## サインアップ（ユーザーアカウント作成）

まず、新しいユーザーを作成します。

<img width="807" alt="0584E428-FCF5-4FE1-BBE2-AE3C2770069C.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528506/dd6e7e23-1d92-6073-462c-a01a6a9cfeef.png">

```bash:cURLでのリクエスト例
# curl --request POST \
  --url http://localhost:3000/api/v1/auth \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data email=user01@example.com \
  --data password=user01 \
  --data password_confirmation=user01
```

サインアップリクエストのレスポンスヘッダーには、このユーザーの認証情報が含まれています。次のリクエストの際には認証情報として ```access-token, client, uid``` の値を指定します。

## GETリクエスト

次に、作成されたユーザーでGETリクエストを行います。認証情報を収めた3つのリクエストヘッダーを付加します。

<img width="819" alt="5B2BA75C-6C45-4C61-B669-364146A169CC.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528506/14168053-b0d7-df27-657e-9b0bb810e875.png">

```bash:cURLでのリクエスト例
# curl --request GET \
  --url http://localhost:3000/api/v1/test \
  --header 'access-token: b638qOBLJ_sIEEJEiMC1ug' \
  --header 'client: 1XcTtaq2uA3DK0qvY6qM9Q' \
  --header 'uid: user01@example.com'
```

レスポンスヘッダーには ```access-token, client, uid``` が含まれており、 ```access-token``` の値が更新されています。 ```access-token``` はリクエストを行うたびに新しい値と置き換わることから、毎回異なるアクセストークンを用いてアクセスする仕様で動作していることが分かります。

# おわりに

RailsとGrapeを使ったAPIプロジェクトで、Deviseの認証機能を用いてトークン認証を行う手順を紹介しました。devise_token_authのトークン更新機能が有効になっており、トークンの有効期間を絞れるという点で安全上望ましい仕様になっています。一方APIクライアントの方では、トークンの値が変更されるのに応じて書き換わったトークンを適切に更新管理することが大切になります。

# 参考情報

* Devise + Grape で認証付きAPIの実装 - Qiita 
  https://qiita.com/travelist/items/ec0b08a9a19cbe9ec78b

* Grapeとdevise_token_authを併用する方法について
  * Usage with Grape · Issue #73 · lynndylanhurley/devise_token_auth 
    https://github.com/lynndylanhurley/devise_token_auth/issues/73

  * How can I use this gem with Grape?・FAQ - devise-token-auth 
    https://devise-token-auth.gitbook.io/devise-token-auth/faq#how-can-i-use-this-gem-with-grape

* モデルでのTrackableオプションについて
  * Remove Trackable option from generator by SugiKent · Pull Request #1362 · lynndylanhurley/devise_token_auth  
    https://github.com/lynndylanhurley/devise_token_auth/pull/1362
  * Why is Trackable option still active? · Issue #1356 · lynndylanhurley/devise_token_auth  
    https://github.com/lynndylanhurley/devise_token_auth/issues/1356