简体   繁体   中英

Can't get authentication to work with devise-jwt

I am trying to get devise and devise-jwt gems to work so I can implement Authorization into my API only Rails app.

I have installed both devise and devise-jwt gems.

I followed the instructions on this blog post:

https://medium.com/@mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03

I implemented the requests specs the author included in his post, and I can't get them to pass. If I put a byebug into the session controller, I see that it's saying the "User needs to sign in or sign up before continuing."

Any thoughts on what I'm doing incorrectly?

Here are the relevant files:

routes.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

controllers/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

models/user.rb

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

      ...
  end

models/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

I would expect the tests to pass, but I get the following errors:

Test Failures

  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

I don't know if you found a solution; but I leave an approach I've made; It might helpfull.

Taking special attetion to the problem, The solution was to change:

decoded_token = decoded_jwt_token_from_response(response)

To:

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

Beacuse I din't find any in the documentation or other place and I chose to decode with method provided by JWT. Also if you see I handle the requests in a different way, but I think that is not a problem at all.

require 'rails_helper'
require "json"

RSpec.describe "POST /login", type: :request do
  
  let(:user) { User.create!(  username: 'usertest',
                              email: 'usertest@email.com',
                              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: 'usertest2@email.com',
        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: I leave the spec code for register, with a couple differences (requests, url, username params in User model (that's is why I use the login param y the login request), I made all in a sigle spec.rb file, ...) to https://medium.com/@mazik.wyry/rails-5-api-jwt-setup-in-minutes-using-devise-71670fd4ed03 . Kepp that in mind.

I believe you need to use the helper sign_in user before making the request for it to be authorized. Check https://github.com/heartcombo/devise , Controller tests

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM