doorkeeper: Unable to use a custom STI OAuthApplication class

Steps to reproduce

Here is the basic setup:

I added a type column to the oauth_applications database table.

I’m using something similar to the following class setup:

class MyCustomOauthApplicationClass  < ApplicationRecord
  include ::Doorkeeper::Orm::ActiveRecord::Mixins::Application
end

class MySpecialCustomOauthApplicationClass < MyCustomOauthApplicationClass
end

I have a bit of custom logic for the AuthorizationsController to integrate with our SPA. This shouldn’t be affecting things, since I was able to reproduce the issue in the Rails console. Just adding for some completeness.

class OauthIdpAuthorizationsController < Doorkeeper::AuthorizationsController
  def new
    if pre_auth.authorizable?
      redirect_to spa_app_path(spa_route: 'oauth/spa_authorize', **pre_auth_params)
    else
      render_error
    end
  end
end

Expected behavior

pre_auth in Doorkeeper::AuthorizationsController should be able to find and initialize the correct model instance.

Actual behavior

I get the following error: ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: MySpecialCustomOauthApplicationClass is not a subclass of MyCustomOauthApplicationClass

What I have been able to figure out

I went on a deep dive into ActiveRecord’s internals to hunt down how STI was handled. That lead me to ActiveSupport::DescendantsTracker which, as the name implies, tracks all of the subclasses, recursively, of a class.

It looks like the object that is held in Doorkeeper.config.application_model is different than calling MyCustomOauthApplicationClass.

They actually return different object_ids which is odd. This is a scrubbed version of a console session:

[1] > MyCustomOauthApplicationClass.object_id
=> 97020
[2] > Doorkeeper.config.application_model.name
=> "MyCustomOauthApplicationClass"
[3] > Doorkeeper.config.application_model.object_id
=> 90500
[4] > uid = 'asfsafsdf' # uid of an STI class instance
=> "asfsafsdf"
[5] > MyCustomOauthApplicationClass.find_by(uid: uid)
=> #<MySpecialCustomOauthApplicationClass id: 2, ...>
[6] > Doorkeeper.config.application_model.find_by(uid: uid)
=> ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: MySpecialCustomOauthApplicationClass is not a subclass of MyCustomOauthApplicationClass
[7] > ActiveSupport::DescendantsTracker.descendants(MyCustomOauthApplicationClass)
=> [MySpecialCustomOauthApplicationClass(id: integer, type: string, ...)]
[8] > ActiveSupport::DescendantsTracker.descendants(Doorkeeper.config.application_model)
=> []

ActiveSupport::DescendantsTracker is a critical part of determining the correctness of STI classes. It looks like two different versions of MyCustomOauthApplicationClass are getting loaded and the one associated with Doorkeeper.config.application_model isn’t getting setup correctly.

I’m not an expert on class, constant, or object loading, but I wonder if the fact that Doorkeeper.config.application_model object_id is lower means that it was loaded first but not in a way that ActiveRecord can find it and use it.

Possibly related issues:

System configuration

Doorkeeper initializer:

# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  orm :active_record
  application_class 'MyCustomOauthApplicationClass'

  api_only
end

# routes.rb
use_doorkeeper do
  controllers authorizations: 'oauth_idp_authorizations'
  skip_controllers :applications, :token_info, :authorized_applications
end

Ruby version: 2.7.3

Gemfile.lock:

Gemfile.lock content
activerecord (6.1.3.2)
doorkeeper (5.5.1)

Let me know if there are any gem versions you need beyond these.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 22 (7 by maintainers)

Most upvoted comments

@nbulaj nowadays constantize is basically const_get (source code). However, if the value is cached in a place that gets reloaded, I believe I’d personally prefer to cache it. Even if the method is cheap, I believe I’d have a sense of doing unnecessary method calls nonetheless.