Skip to content

Commit fc60a3c

Browse files
committed
Wire openid_request from AccessGrant mixin instead of on_load (#306)
Namespaced custom access grant models (e.g. `Auth::OAuthAccessGrant < ApplicationRecord`) crashed on Rails boot since v1.10.0 with `NameError: uninitialized constant Auth::ApplicationRecord`. v1.10.0 (#241) wrapped the access-grant prepend in `ActiveSupport.on_load(:active_record)`, following doorkeeper #1804. doorkeeper reverted that approach in #1830 (v5.9.2): the load hook fires while `ActiveRecord::Base` is first loaded — mid-evaluation of `class ApplicationRecord < ActiveRecord::Base` — so constantizing a namespaced grant model from the hook resolves `ApplicationRecord` before `::ApplicationRecord` is registered, raising the NameError. Follow doorkeeper #1830: drop the `run_hooks`/`initialize_models!` overrides and the on_load block, and instead prepend an extension onto the singleton class of `Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant`. Its `included` callback adds the `openid_request` association when the host model includes the mixin — at the model's own load time, with `base` handed in by Ruby. Nothing constantizes the configured grant class, so the re-entrant load window is gone. The string `class_name:` keeps the association target resolved lazily by ActiveRecord. The prepend is guarded with `base.is_a?(Class)`: when the mixin is included into an intermediate ActiveSupport::Concern, the hook first fires with that module as `base`, and the deferred include fires it again with the actual model class. The legacy `active_record_options[:establish_connection]` handling moves into the OpenidRequest mixin's `included` block (guarded; a no-op on doorkeeper 5.9.x, which no longer exposes `active_record_options`). Adds regression coverage: a namespaced custom model that includes the doorkeeper mixin is wired with the `openid_request` association.
1 parent 75d1613 commit fc60a3c

4 files changed

Lines changed: 90 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- [#304] allow handle auth_time per grant
55
- [#305] Document the `auth_time_from_access_token` config option in the README (per-grant `auth_time`), clarifying that it only affects the ID Token `auth_time` claim and not `max_age` enforcement
66
- [#307] Fix `bundle exec rake server` for the test application
7+
- [#308] Fix `NameError: uninitialized constant Auth::ApplicationRecord` on boot when using a namespaced custom access grant model (e.g. `Auth::OAuthAccessGrant < ApplicationRecord`). Since v1.10.0 ([#241]) the `openid_request` association was wired inside an `ActiveSupport.on_load(:active_record)` block, which fires while `ActiveRecord::Base` is first loaded and constantizes the grant model too early. The association is now added from Doorkeeper's `AccessGrant` mixin `included` callback — at the model's own load time, without constantizing — mirroring the fix doorkeeper made in [#1830](https://github.com/doorkeeper-gem/doorkeeper/pull/1830) ([#306](https://github.com/doorkeeper-gem/doorkeeper-openid_connect/issues/306))
78

89
## v1.10.1 (2026-06-03)
910

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require "active_support/lazy_load_hooks"
4-
53
module Doorkeeper
64
module OpenidConnect
75
autoload :AccessGrant, "doorkeeper/openid_connect/orm/active_record/access_grant"
@@ -14,51 +12,43 @@ module Mixins
1412
"doorkeeper/openid_connect/orm/active_record/mixins/openid_request"
1513
end
1614

17-
def run_hooks
18-
super
19-
20-
ActiveSupport.on_load(:active_record) do
21-
require "doorkeeper/openid_connect/orm/active_record/access_grant"
22-
require "doorkeeper/openid_connect/orm/active_record/request"
23-
24-
if Gem.loaded_specs["doorkeeper"].version >= Gem::Version.create("5.5.0")
25-
Doorkeeper.config.access_grant_model.prepend Doorkeeper::OpenidConnect::AccessGrant
26-
else
27-
Doorkeeper::AccessGrant.prepend Doorkeeper::OpenidConnect::AccessGrant
28-
end
29-
30-
if Doorkeeper.configuration.respond_to?(:active_record_options) && Doorkeeper.configuration.active_record_options[:establish_connection]
31-
[Doorkeeper::OpenidConnect.configuration.open_id_request_model].each do |c|
32-
c.send :establish_connection,
33-
Doorkeeper.configuration.active_record_options[:establish_connection]
34-
end
35-
end
36-
end
37-
end
38-
39-
def initialize_models!
40-
super
41-
ActiveSupport.on_load(:active_record) do
42-
require "doorkeeper/openid_connect/orm/active_record/access_grant"
43-
require "doorkeeper/openid_connect/orm/active_record/request"
44-
45-
if Gem.loaded_specs["doorkeeper"].version >= Gem::Version.create("5.5.0")
46-
Doorkeeper.config.access_grant_model.prepend Doorkeeper::OpenidConnect::AccessGrant
47-
else
48-
Doorkeeper::AccessGrant.prepend Doorkeeper::OpenidConnect::AccessGrant
49-
end
50-
51-
if Doorkeeper.configuration.active_record_options[:establish_connection]
52-
[Doorkeeper::OpenidConnect.configuration.open_id_request_model].each do |c|
53-
c.send :establish_connection,
54-
Doorkeeper.configuration.active_record_options[:establish_connection]
55-
end
56-
end
15+
# Prepended onto the singleton class of Doorkeeper's AccessGrant
16+
# mixin so that every model which includes
17+
# `Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant` — the default
18+
# `Doorkeeper::AccessGrant` as well as any (possibly namespaced)
19+
# custom access grant model — also gains the OpenID Connect
20+
# `openid_request` association.
21+
#
22+
# The association is wired from the mixin's `included` callback, at
23+
# the moment the host model is loaded: `base` is the model class
24+
# itself, handed to us by Ruby. Nothing reaches out to constantize
25+
# the configured grant class, so the re-entrant
26+
# `ActiveSupport.on_load(:active_record)` window that broke
27+
# namespaced custom models is gone.
28+
#
29+
# Background: doorkeeper-openid_connect v1.10.0 (#241) wrapped the
30+
# grant-model prepend in `ActiveSupport.on_load(:active_record)`,
31+
# following doorkeeper #1804. doorkeeper later reverted that in
32+
# #1830 (v5.9.2) because the hook fires while `ActiveRecord::Base`
33+
# is first loaded — e.g. mid-evaluation of
34+
# `class ApplicationRecord < ActiveRecord::Base` — at which point
35+
# constantizing `Auth::OAuthAccessGrant < ApplicationRecord` raises
36+
# `NameError: uninitialized constant Auth::ApplicationRecord` (#306).
37+
# We follow the same fix: wire from the mixin instead of on_load.
38+
module AccessGrantExtension
39+
def included(base)
40+
super
41+
# `base` is a Module (not the model) when the mixin is included
42+
# into an intermediate ActiveSupport::Concern; the concern defers
43+
# the include, so this hook fires again with the model class.
44+
base.prepend(OpenidConnect::AccessGrant) if base.is_a?(Class)
5745
end
5846
end
5947
end
6048
end
6149
end
6250

63-
Orm::ActiveRecord.singleton_class.send :prepend, OpenidConnect::Orm::ActiveRecord
51+
Orm::ActiveRecord::Mixins::AccessGrant.singleton_class.prepend(
52+
OpenidConnect::Orm::ActiveRecord::AccessGrantExtension,
53+
)
6454
end

lib/doorkeeper/openid_connect/orm/active_record/mixins/openid_request.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ module OpenidRequest
1111
included do
1212
self.table_name = "#{table_name_prefix}oauth_openid_requests#{table_name_suffix}".to_sym
1313

14+
# Legacy multi-database support: older Doorkeeper releases let
15+
# users route the ORM models to a separate connection via
16+
# `active_record_options[:establish_connection]`. Doorkeeper
17+
# 5.9.x no longer exposes `active_record_options`, so the guard
18+
# makes this a no-op there. It used to live in the ORM adapter's
19+
# `run_hooks`; wiring it from the model's own `included` block
20+
# keeps it off the re-entrant `on_load(:active_record)` path.
21+
if Doorkeeper.configuration.respond_to?(:active_record_options) &&
22+
(connection_options = Doorkeeper.configuration.active_record_options[:establish_connection])
23+
establish_connection(connection_options)
24+
end
25+
1426
validates :access_grant_id, :nonce, presence: true
1527

1628
if Gem.loaded_specs["doorkeeper"].version >= Gem::Version.create("5.5.0")

spec/lib/orm/active_record_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
# Regression coverage for #306.
6+
#
7+
# doorkeeper-openid_connect v1.10.0 (#241) wired the `openid_request`
8+
# association onto the access grant model from inside an
9+
# `ActiveSupport.on_load(:active_record)` block. That hook fires while
10+
# `ActiveRecord::Base` is first loaded — e.g. mid-evaluation of
11+
# `class ApplicationRecord < ActiveRecord::Base` — so constantizing a
12+
# namespaced custom grant model (`Auth::OAuthAccessGrant < ApplicationRecord`)
13+
# from the hook raised `NameError: uninitialized constant Auth::ApplicationRecord`.
14+
#
15+
# The fix wires the association from Doorkeeper's AccessGrant mixin
16+
# `included` callback instead, at the host model's own load time, without
17+
# constantizing anything. These specs pin that behavior.
18+
describe "Doorkeeper::OpenidConnect ActiveRecord ORM integration" do
19+
describe "the openid_request association" do
20+
it "is wired onto the default access grant model" do
21+
association = Doorkeeper.config.access_grant_model.reflect_on_association(:openid_request)
22+
expect(association).not_to be_nil
23+
end
24+
25+
it "is wired onto a namespaced custom model that includes Doorkeeper's mixin" do
26+
# Mirrors the #306 reporter's setup: a namespaced model subclassing
27+
# the host app's ApplicationRecord and including the doorkeeper mixin.
28+
# Because the association is added from the mixin's `included` callback
29+
# (not a deferred load hook), the model is wired at its own load time
30+
# without anything constantizing the configured grant class.
31+
stub_const("Auth306", Module.new)
32+
custom_model = Class.new(ApplicationRecord) do
33+
self.table_name = "oauth_access_grants"
34+
include Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant
35+
end
36+
Auth306.const_set(:OAuthAccessGrant, custom_model)
37+
38+
association = custom_model.reflect_on_association(:openid_request)
39+
expect(association).not_to be_nil
40+
expect(association.options[:class_name])
41+
.to eq(Doorkeeper::OpenidConnect.configuration.open_id_request_class)
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)