Device-jwt で認証を機能させることができません

概要

API のみの Rails アプリに認可を実装できるように、device および Device-jwt gem を機能させようとしています。

Device と Device-jwt gem の両方をインストールしました。

このブログ投稿の指示に従いました。

https://translate.google.com/translate?hl=ja&sl=en&tl=ja&u=https://medium.com/@mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03

著者が投稿に含めたリクエストの仕様を実装しましたが、それらを通過させることができません。セッション コントローラーに byebug を挿入すると、「ユーザーは続行する前にサインインまたはサインアップする必要があります。」というメッセージが表示されます。

私が間違っていることについて何か考えはありますか?

関連するファイルは次のとおりです。

ルート.rb

Rails.application.routes.draw do

  namespace :api, path: '', defaults: {format: :json} do
      namespace :v1 do
        devise_for :users,
                   path: '',
                   path_names: {
                     sign_in: 'signin',
                     sign_out: 'signout',
                     registration: 'signup'
                   }
        ...
      end
  end

コントローラー/api/v1/sessions_controller.rb

  class API::V1::SessionsController < Devise::SessionsController
    respond_to :json

    private

    def respond_with(resource, _opts = {})
      render json: resource
    end

    def respond_to_on_destroy
      head :no_content
    end
  end

モデル/user.rb

class User < ApplicationRecord
      devise  :confirmable, :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :jwt_authenticatable, jwt_revocation_strategy: JwtBlacklist

      ...
  end

モデル/jwt_blacklist.rb

  class JwtBlacklist < ApplicationRecord
    include Devise::JWT::RevocationStrategies::Blacklist

    self.table_name = 'jwt_blacklist'
  end

config/initializers/devise.rb

  Devise.setup do |config|

    # Setup for devise JWT token authentication
    config.jwt do |jwt|
      jwt.secret = Rails.application.secret_key_base
      jwt.dispatch_requests = [
        ['POST', %r{^*/signin$}]
      ]
      jwt.revocation_requests = [
        ['DELETE', %r{^*/signout$}]
      ]
      jwt.expiration_time = 1.day.to_i
    end

    config.navigational_formats = []

    ...

  end

spec/request/authentication_spec.rb

  require 'rails_helper'

  describe 'POST /v1/signin', type: :request do
    let(:user) { create(:user) }
    let(:url) { '/v1/signin' }
    let(:params) do
      {
        user: {
          email: user.email,
          password: user.password
        }
      }
    end

    context 'when params are correct' do
      before do
        post url, params: params
      end

      it 'returns 200' do
        expect(response).to have_http_status(200)
      end

      it 'returns JTW token in authorization header' do
        expect(response.headers['Authorization']).to be_present
      end

      it 'returns valid JWT token' do
        decoded_token = decoded_jwt_token_from_response(response)
        expect(decoded_token.first['sub']).to be_present
      end
    end

    context 'when login params are incorrect' do
      before { post url }

      it 'returns unathorized status' do
        expect(response.status).to eq 401
      end
    end
  end

  describe 'DELETE /v1/signout', type: :request do
    let(:url) { '/v1/signout' }

    it 'returns 204, no content' do
      delete url
      expect(response).to have_http_status(204)
    end
  end

テストが成功すると期待していましたが、次のエラーが発生しました。

テストの失敗

  Failures:

    1) POST /v1/signin when params are correct returns 200
       Failure/Error: expect(response).to have_http_status(200)
         expected the response to have status code 200 but it was 401
       # ./spec/request/authentication_spec.rb:21:in `block (3 levels) in <top (required)>'

    2) POST /v1/signin when params are correct returns JTW token in authorization header
       Failure/Error: expect(response.headers['Authorization']).to be_present
         expected `nil.present?` to return true, got false
       # ./spec/request/authentication_spec.rb:25:in `block (3 levels) in <top (required)>'

    3) POST /v1/signin when params are correct returns valid JWT token
       Failure/Error: decoded_token = decoded_jwt_token_from_response(response)

       NoMethodError:
         undefined method `decoded_jwt_token_from_response' for #<RSpec::ExampleGroups::POSTV1Signin::WhenParamsAreCorrect:0x00007fec3d3ae158>
       # ./spec/request/authentication_spec.rb:29:in `block (3 levels) in <top (required)>'

  Finished in 0.76386 seconds (files took 3.31 seconds to load)
  5 examples, 3 failures

  Failed examples:

  rspec ./spec/request/authentication_spec.rb:20 # POST /v1/signin when params are correct returns 200
  rspec ./spec/request/authentication_spec.rb:24 # POST /v1/signin when params are correct returns JTW token in authorization header
  rspec ./spec/request/authentication_spec.rb:28 # POST /v1/signin when params are correct returns valid JWT token

解決策

解決策が見つかったかどうかはわかりません。しかし、私は自分が行ったアプローチを残します。役立つかもしれません。

この問題に特に注意を払い、解決策は次のように変更することでした。

decoded_token = decoded_jwt_token_from_response(response)

に:

decoded_token = JWT.decode(response.headers['authorization'].split(' ').last, Rails.application.credentials.jwt_secret, true)

ドキュメントや他の場所で何も見つからなかったため、JWTが提供するメソッドを使用してデコードすることを選択したためです。 また、ご覧のとおり、リクエストを別の方法で処理していますが、それはまったく問題ないと思います。

require 'rails_helper'
require "json"

RSpec.describe "POST /login", type: :request do
  
  let(:user) { User.create!(  username: 'usertest',
                              email: '[email protected]',
                              password: 'passwordtest123',
                              password_confirmation: 'passwordtest123') }

  let(:url) { '/users/login' }
  let(:params) do
    {
      user: {
        login: user.email,
        password: user.password
      }       
    }
  end

  context 'when params are correct' do
    before do
      post url, params: params.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
    end

    it 'returns 200' do
      expect(response).to have_http_status(200)
    end

    it 'returns JTW token in authorization header' do
      expect(response.headers['authorization']).to be_present
    end

    it 'returns valid JWT token' do
      token_from_request = response.headers['Authorization'].split(' ').last
      decoded_token = JWT.decode(token_from_request, Rails.application.credentials.jwt_secret, true)
      expect(decoded_token.first['sub']).to be_present
    end
  end

  context 'when login params are incorrect' do
    before { post url }
    
    it 'returns unathorized status' do
      expect(response.status).to eq 401
    end
  end
end

RSpec.describe 'DELETE /logout', type: :request do
  let(:url) { '/users/logout' }

  it 'returns 204, no content' do
    delete url
    expect(response).to have_http_status(204)
  end
end

RSpec.describe 'POST /signup', type: :request do
  let(:url) { '/users/signup' }
  let(:params) do
    {
      user: {
        username: 'usertest2',
        email: '[email protected]',
        password: 'passwordtest123',
        password_confirmation: 'passwordtest123'
      }
    }
  end

  context 'when user is unauthenticated' do
    before  {
              post url,
              params: params.to_json,
              headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
            }

    it 'returns 201' do
      expect(response.status).to eq 201
    end

    it 'returns a new user' do
      expect(response).to have_http_status :created
    end
  end

  context 'when user already exists' do
    before do
      post url,
      params: params.to_json,
      headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }

      post url,
      params: params.to_json,
      headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
    end

    it 'returns bad request status' do
      expect(response.status).to eq 400
    end

    it 'returns validation errors' do
      expect(response_body['errors'].first['title']).to eq('Bad Request')
    end
  end
end

PD: レジスタの仕様コードはそのままにしますが、いくつかの違い (リクエスト、URL、ユーザー モデルのユーザー名パラメーター (これが、ログイン パラメーターとログイン リクエストを使用する理由です)) を除き、すべてを 1 つの spec.rb ファイルに作成しました。 …) https://medium.com/@mazik.wyry/rails-5-api-jwt-setup-in- minutes-using-devise-71670fd4ed03 に送信します。ケップはそれを念頭に置いてください。