waiting-for-dev/devise-jwt

Error: SQLite3::SQLException: no such column: allowlisted_jwts.allowlisted_jwt_id

abratashov opened this issue · 3 comments

I'm implementing the JWT Allowlist strategy, and when I try to revoke the token the error arises:

AllowlistedJwt.revoke_jwt(payload, user)

=>
SQLite3::SQLException: no such column: allowlisted_jwts.allowlisted_jwt_id
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/sqlite3-1.7.3-x86_64-linux/lib/sqlite3/database.rb:177:in `initialize'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/sqlite3-1.7.3-x86_64-linux/lib/sqlite3/database.rb:177:in `new'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/sqlite3-1.7.3-x86_64-linux/lib/sqlite3/database.rb:177:in `prepare'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/sqlite3/database_statements.rb:47:in `block (2 levels) in internal_exec_query'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract_adapter.rb:1028:in `block in with_raw_connection'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract_adapter.rb:1000:in `with_raw_connection'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/sqlite3/database_statements.rb:33:in `block in internal_exec_query'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/notifications/instrumenter.rb:58:in `instrument'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract_adapter.rb:1143:in `log'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/sqlite3/database_statements.rb:32:in `internal_exec_query'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/database_statements.rb:630:in `select'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/database_statements.rb:71:in `select_all'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/query_cache.rb:112:in `block in select_all'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/query_cache.rb:152:in `block in cache_sql'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/query_cache.rb:147:in `cache_sql'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/query_cache.rb:112:in `select_all'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/querying.rb:62:in `_query_by_sql'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/querying.rb:51:in `find_by_sql'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/statement_cache.rb:150:in `execute'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/associations/association.rb:235:in `find_target'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/associations/collection_association.rb:270:in `load_target'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/associations/has_many_association.rb:28:in `handle_dependency'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/associations/builder/association.rb:141:in `block in add_destroy_callbacks'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:470:in `instance_exec'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:470:in `block in make_lambda'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:202:in `block (2 levels) in halting'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:707:in `block (2 levels) in default_terminator'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:706:in `catch'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:706:in `block in default_terminator'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:203:in `block in halting'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:598:in `block in invoke_before'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:598:in `each'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:598:in `invoke_before'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:109:in `run_callbacks'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/callbacks.rb:952:in `_run_destroy_callbacks'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/callbacks.rb:423:in `destroy'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/transactions.rb:305:in `block in destroy'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/transactions.rb:365:in `block in with_transaction_returning_status'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/transaction.rb:535:in `block in within_new_transaction'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3.4/lib/active_support/concurrency/null_lock.rb:9:in `synchronize'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/transaction.rb:532:in `within_new_transaction'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/connection_adapters/abstract/database_statements.rb:344:in `transaction'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/transactions.rb:361:in `with_transaction_returning_status'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/transactions.rb:305:in `destroy'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3.4/lib/active_record/persistence.rb:797:in `destroy!'
/home/alex/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/devise-jwt-0.12.1/lib/devise/jwt/revocation_strategies/allowlist.rb:36:in `revoke_jwt'
/home/alex/projects/abratashov/DailyTracker/app/interactors/api/auth/jwt_sign_out.rb:22:in `call'

Here JWT setup:

# Migration
class CreateAllowlistedJwts < ActiveRecord::Migration[7.1]
  def change
    create_table :allowlisted_jwts do |t|
      t.string :jti, null: false
      t.string :aud
      t.datetime :exp, null: false
      t.references :user, foreign_key: { on_delete: :cascade }, null: false
    end

    add_index :allowlisted_jwts, :jti, unique: true
  end
end

# Models

class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :lockable, :timeoutable, :trackable, # :omniauthable
         :jwt_authenticatable, jwt_revocation_strategy: AllowlistedJwt

  has_many :allowlisted_jwts

  attribute :jwt_token, :string
end

class AllowlistedJwt < ApplicationRecord
  include Devise::JWT::RevocationStrategies::Allowlist

  class << self
    def decode_jwt_payload(token, secret_key = Rails.application.credentials.jwt_secret_key!, alg = 'HS256')
      body = JWT.decode(token, secret_key, true, algorithm: alg)[0]
      HashWithIndifferentAccess.new body
    rescue JWT::ExpiredSignature, JWT::VerificationError, JWT::DecodeError => e
      raise StandardError.new(e.message)
    end
  end
end

# Service

module Api
  module Auth
    class JwtSignOut
      include Interactor

      delegate :user, :auth_header, to: :context
      attr_reader :token

      before do
        context.fail!(error: 'User not found', status: :not_found) if user.blank?
        @token = auth_header.to_s.split(' ').last
        context.fail!(error: 'Authorization header should contains JWT token', status: :not_found) if token.blank?
      end

      def call
        payload = AllowlistedJwt.decode_jwt_payload(token)

        # The error arises here:
        # AllowlistedJwt.revoke_jwt(payload, user)

        # Manual revocation:
        allowlisted_jwt = user.allowlisted_jwts.find_by(jti: payload[:jti])

        context.fail!(error: 'Token not found', status: :not_found) if allowlisted_jwt.blank?
        
        # The same error in case manual destroying:
        # allowlisted_jwt.destroy!

        # Only this works fine:
        allowlisted_jwt.delete
      end
    end
  end
end

Versions:

rails (7.1.3.4)
devise (4.9.4)
devise-jwt (0.12.1)
sqlite3 (1.7.3)

Hey there 👋 Sorry, you have custom code that is not part of the recommended setup. Please, tyr to reproduce your bug in a pristine repository where the canonical installation is used. There's no previous notice of a problem with the allow list strategy so you should no be being experimenting this issue. I'll happily reopen this one if proven to be a bug here. Cheers.

Yeah, as mentioned in the docs of the Allowlist strategy, if I move Devise::JWT::RevocationStrategies::Allowlist to the User:

class AllowlistedJwt < ApplicationRecord
end

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

  devise :database_authenticatable,
         :jwt_authenticatable, jwt_revocation_strategy: self
end

It will work fine, thanks!

P.S.
I missed this docs part because I followed another guide to adding JWT to Rails.

Happy you figured it out!! 🙂