概要

devise とその JWT 用のプラグインである devise-jwt を使って Rails6 に JWT 認証を実装します。
device を使わない JWT 認証の API の実装はこちらに別の記事があります。

Ruby 2.6.5
Rails 6.0.2.1
Devise 4.7.1
Device::JWT 0.6.0

Rails プロジェクトの作成

Rails の新しいプロジェクトを作成して、必要な gem をインストールします。

rails new devise-jwt-sample --api
cd devise-jwt-sample

Gemfile に以下を追記します。

# ...
gem 'devise'
gem 'devise-jwt'
gem 'dry-configurable', '~> 0.9.0'

そして、bundle installします。

dry-configurableはバージョン0.9.0でないと現時点でエラーが出てしまうので入れています。

Devise のセットアップ

ここから Device の設定を行なっていきます。
まずはモデルの作成から。

bin/rails g devise:install
bin/rails g devise user

マイグレーションファイルが作成されるので、JWT で使うカラムを追加します。

def change
  create_table :users do |t|
  # ...
    t.string :jti, null: false
  end
  # ...
  add_index :users, :jti, unique: true
end

全て終わったらbin/rails db:migrateします。

Device::JWT のセットアップ

次に JWT の設定を行います。
config/initializers/devise.rbから JWT の署名に使う秘密鍵を設定します。

Devise.setup do |config|
  # ...
  config.jwt do |jwt|
    jwt.secret = Rails.application.credentials.secret_key_base
  end
end

User モデルを編集します。

class User < ApplicationRecord
  include Devise::JWT::RevocationStrategies::JTIMatcher

  devise :database_authenticatable,
         :jwt_authenticatable, jwt_revocation_strategy: self
end

動作確認

すべての準備ができたので動作確認します。
適当なユーザーを作ってログインしてみます。

bin/rails r 'User.create(email: "test@example.com", password: "password")'
curl -D - -o /dev/null http://localhost:3000/users/sign_in -H 'content-type: application/json' -d '{"user": {"email": "test@example.com", "password": "password"}}'

以下のような返答があれば成功です。
閲覧に認証が必要なリソースへはAuthorization: Bearer eyJhbGc...の箇所をヘッダーへ入れてリクエストすれば OK です。

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0HTTP/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
Location: /
Content-Type: text/plain; charset=utf-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxNzk0MmY0ZC1hYTMxLTRjNzEtYmFhOC0xMzUxMzgyY2E3OWYiLCJzdWIiOiIxIiwic2NwIjoidXNlciIsImF1ZCI6bnVsbCwiaWF0IjoxNTgyMjc0OTc4LCJleHAiOjE1ODIyNzg1Nzh9.egm1mzjd6pqc9ZM04aZEqTdM0E-d_5OUkL5WjVoAZro
ETag: W/"36a9e7f1c95b82ffb99743e0c5c4ce95"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 125b3788-bfed-4bad-a699-03974e70ea6d
X-Runtime: 0.175217
Transfer-Encoding: chunked

おしまい