waiting-for-dev / devise-jwt

JWT token authentication with devise and rails

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Logout with JTIMatcher as revocation strategy not revoking token

Lary15 opened this issue · comments

Expected behavior

While using JTIMatcher the expected outcome of the logout action is to update the jti column. As I read in this issue #167,
this is the only time the jti column will update.

Actual behavior

When I logout, the jti doesn't change, so, while repeatedly logging in, the jti is always the same.

Steps to Reproduce the Problem

  1. Calling login method with credentials of a user
  2. Calling logout method
  3. Calling login method with the same credentials
  4. The jti field remains the same

Debugging information

  • Version of devise-jwt in use: 0.7.0
  • Version of rails in use: 6.1.1
  • Version of ruby in use: 2.7.1
  • Output of Devise::JWT.config
#<Dry::Configurable::Config values={:secret=>#<Dry::Configurable::Settings elements=#<Concurrent::Map:0x00000000048df910 entries=8 default_proc=nil>>,
:expiration_time=>#<Dry::Configurable::Settings elements=#<Concurrent::Map:0x00000000048df910 entries=8 default_proc=nil>>, 
:dispatch_requests=>#<Dry::Configurable::Settings elements=#<Concurrent::Map:0x00000000048df910 entries=8 default_proc=nil>>, 
:revocation_requests=>#<Dry::Configurable::Settings elements=#<Concurrent::Map:0x00000000048df910 entries=8 default_proc=nil>>, 
:aud_header=>#<Dry::Configurable::Settings elements=#<Concurrent::Map:0x00000000048df910 entries=8 default_proc=nil>>, :request_formats=>{:user=>[:json]}}> 
  • Output of Warden::JWTAuth.config
#<Dry::Configurable::Config values={:secret=>secret, :algorithm=>"HS256", 
:expiration_time=>86400, 
:aud_header=>nil, 
:mappings=>{:user=>User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, sign_in_count: integer, current_sign_in_at: datetime, last_sign_in_at: datetime, current_sign_in_ip: string, last_sign_in_ip: string, confirmation_token: string, confirmed_at: datetime, confirmation_sent_at: datetime, unconfirmed_email: string, failed_attempts: integer, unlock_token: string, locked_at: datetime, created_at: datetime, updated_at: datetime, jti: string)},
:dispatch_requests=>[["POST", /api\/v1\/login/], 
["POST", /api\/v1\/login.json/], ["POST", /^\/api\/v1\/login.json$/], ["POST", /^\/api\/v1\/signup.json$/]], 
:revocation_requests=>[["DELETE", /api\/v1\/logout/], ["DELETE", /api\/v1\/logout.json/], ["DELETE", /^\/api\/v1\/logout.json$/]], 
:revocation_strategies=>{:user=>User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, sign_in_count: integer, current_sign_in_at: datetime, last_sign_in_at: datetime, current_sign_in_ip: string, last_sign_in_ip: string, confirmation_token: string, confirmed_at: datetime, confirmation_sent_at: datetime, unconfirmed_email: string, failed_attempts: integer, unlock_token: string, locked_at: datetime, created_at: datetime, updated_at: datetime, jti: string)}}> 
  • Output of Devise.mappings
{:user=>#<Devise::Mapping:0x0000000005a57b70 @scoped_path="users", 
@singular=:user, 
@class_name="User",
@klass=#<Devise::Getter:0x0000000005a57738 @name="User">, 
@path="", 
@path_prefix=nil, 
@sign_out_via=:delete, 
@format=nil, 
@router_name=nil, 
@failure_app=Devise::FailureApp, 
@controllers={:sessions=>"api/v1/users/sessions", :registrations=>"api/v1/users/registrations", :passwords=>"api/v1/users/passwords"}, 
@path_names={:registration=>"api/v1/signup", :new=>"new", :edit=>"edit", :sign_in=>"api/v1/login", :sign_out=>"api/v1/logout", :password=>"api/v1/reset_password", :sign_up=>"sign_up", :cancel=>"cancel"}, 
@modules=[:database_authenticatable, :rememberable, :recoverable, :registerable, :validatable, :jwt_authenticatable], @routes=[:session, :password, :registration], 
@used_routes=[:session, :password, :registration], 
@used_helpers=[:session, :password, :registration]>} 

here is the devise configuration on my routes.rb

  devise_for(
    :users,
    path: "",
    path_names: {
      sign_in: "api/v1/login",
      sign_out: "api/v1/logout",
      registration: "api/v1/signup",
      password: "api/v1/reset_password"
    },
    controllers: {
      sessions: "api/v1/users/sessions",
      registrations: "api/v1/users/registrations",
      passwords: "api/v1/users/passwords"
    },
    defaults: { format: :json }
  )

user.rb

class User < ApplicationRecord
  include Devise::JWT::RevocationStrategies::JTIMatcher
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable, :recoverable, :rememberable,
         :jwt_authenticatable, :validatable, jwt_revocation_strategy: self

  validates :email, presence: true, uniqueness: true

  def token
    token, _payload = Warden::JWTAuth::UserEncoder.new.call(self, :user, nil)
    token
  end
end

jwt config in devise.rb

config.jwt do |jwt|
    # Setting secret key that will be used to sign generated tokens
    jwt.secret = secret
    jwt.dispatch_requests = [
      ["POST", %r{api/v1/login}],
      ["POST", %r{api/v1/login.json}]
    ]
    jwt.revocation_requests = [
      ["DELETE", %r{api/v1/logout}],
      ["DELETE", %r{api/v1/logout.json}]
    ]
    jwt.expiration_time = 1.day.to_i
    jwt.request_formats = { user: [:json] }
  end

and sessions_controller.rb

module Api
  module V1
    module Users
      class SessionsController < Devise::SessionsController
        # before_action :configure_sign_in_params, only: [:create]

        # GET /resource/sign_in
        # def new
        #   super
        # end

        # POST /resource/sign_in

        # DELETE /resource/sign_out
        # def destroy
        #   super
        # end

        # protected

        # If you have extra params to permit, append them to the sanitizer.
        # def configure_sign_in_params
        #   devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
        # end

        private

        def respond_with(resource, _opts = {})
          if resource.errors.empty?
            render json: resource.to_json(methods: :token), status: :ok
          else
            render json: resource.errors.full_messages, status: :unauthorized
          end
        end

        def respond_to_on_destroy
          head :no_content
        end
      end
    end
  end
end

I look up various issues to see if could find the cause of the problem, but to no avail. I guess the login is working as expected, it is generating the token and putting on the header, but the logout is not doing much.

Ok, now so this was really dumb. I forgot to put the Authorization header with the token in the logout request 🤦‍♀️