navi
navi

Reputation: 193

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

Upvotes: 1

Views: 4582

Answers (3)

Ghayor Ul Baqir
Ghayor Ul Baqir

Reputation: 1

  1. Add this line in rails_helper.rb file:
config.include Devise::Test::IntegrationHelpers, type: :request

The above line includes Devise's integration test helpers for RSpec request specs, allowing you to simulate user authentication in your tests.

  1. Use Devise helper method sign
and add this method in your spec or rails_helper file
 def auth_headers(user)
  sign_in user
  { 'Authorization' => "Bearer #{user.auth_token}" }
end

The auth_headers method signs in a user using Devise's sign_in helper method and returns a hash containing the user's authorization token in the 'Authorization' header, prefixed with the string 'Bearer '. This method can be used in API request specs to authenticate as a specific user.

Now you can add auth_headers(user) in your spec headers to authenticate API requests. This will pass the user's authorization token in the 'Authorization' header of the request, allowing your application to authenticate the user and authorize access to protected resources.

Upvotes: 0

Adrian Rama
Adrian Rama

Reputation: 53

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: '[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: 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.

Upvotes: 1

cesartalves
cesartalves

Reputation: 1585

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

Upvotes: 0

Related Questions