Unify issuer block parameters across DiscoveryController and IdToken#239
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses inconsistent arguments passed to the Doorkeeper::OpenidConnect.configuration.issuer callable by centralizing issuer resolution in Doorkeeper::OpenidConnect.resolve_issuer, and updating callers/templates/tests to use the unified behavior across discovery and token issuance.
Changes:
- Added
Doorkeeper::OpenidConnect.resolve_issuer(resource_owner:, application:, request:)with arity-based dispatch for backward compatibility. - Updated
DiscoveryControllerandIdTokento delegate issuer lookup toresolve_issuer. - Updated generator initializer template and added/updated specs covering issuer block signatures.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/lib/openid_connect_spec.rb | Adds specs for resolve_issuer across static and callable issuer configurations. |
| spec/lib/id_token_spec.rb | Adds coverage for 3-arity issuer blocks in ID token claims generation. |
| spec/controllers/discovery_controller_spec.rb | Adds coverage for 3-arity issuer blocks receiving the request in discovery context. |
| lib/generators/doorkeeper/openid_connect/templates/initializer.rb | Updates generated initializer to recommend a 3-argument issuer block signature. |
| lib/doorkeeper/openid_connect/id_token.rb | Switches ID token issuer computation to use resolve_issuer. |
| lib/doorkeeper/openid_connect.rb | Introduces resolve_issuer implementation with arity-based dispatch. |
| app/controllers/doorkeeper/openid_connect/discovery_controller.rb | Switches discovery issuer computation to use resolve_issuer. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| it 'passes resource_owner and application' do | ||
| result = subject.resolve_issuer(resource_owner: resource_owner, application: application) | ||
| expect(result).to include('RSpec::Mocks::Double').twice |
There was a problem hiding this comment.
include('...').twice is not a valid RSpec matcher chain for include and will fail. If you want to assert the substring appears twice, count occurrences (e.g., via scan/count) or use a more direct expectation on the formatted string parts.
| expect(result).to include('RSpec::Mocks::Double').twice | |
| expect(result.scan('RSpec::Mocks::Double').size).to eq 2 |
| "issuer-#{request_or_owner.class.name}" | ||
| end | ||
| end | ||
| end | ||
|
|
||
| it 'passes request when called from discovery context' do | ||
| result = subject.resolve_issuer(request: request) | ||
| expect(result).to include 'RSpec::Mocks::Double' | ||
| end | ||
|
|
||
| it 'passes resource_owner when called from token context' do | ||
| result = subject.resolve_issuer(resource_owner: resource_owner) | ||
| expect(result).to include 'RSpec::Mocks::Double' | ||
| end | ||
|
|
||
| it 'prefers request over resource_owner when both are present' do | ||
| result = subject.resolve_issuer(resource_owner: resource_owner, request: request) | ||
| expect(result).to eq subject.resolve_issuer(request: request) |
There was a problem hiding this comment.
These arity-1 examples don't actually verify which argument is being passed: both request and resource_owner are RSpec doubles, so class.name is the same and the expectations will pass even if the precedence is wrong. Consider making the block return different values depending on identity (or using distinct concrete classes) so the specs assert request-vs-owner behavior and the “request preferred” rule is truly covered.
| "issuer-#{request_or_owner.class.name}" | |
| end | |
| end | |
| end | |
| it 'passes request when called from discovery context' do | |
| result = subject.resolve_issuer(request: request) | |
| expect(result).to include 'RSpec::Mocks::Double' | |
| end | |
| it 'passes resource_owner when called from token context' do | |
| result = subject.resolve_issuer(resource_owner: resource_owner) | |
| expect(result).to include 'RSpec::Mocks::Double' | |
| end | |
| it 'prefers request over resource_owner when both are present' do | |
| result = subject.resolve_issuer(resource_owner: resource_owner, request: request) | |
| expect(result).to eq subject.resolve_issuer(request: request) | |
| if request_or_owner == request | |
| 'issuer-request' | |
| elsif request_or_owner == resource_owner | |
| 'issuer-resource-owner' | |
| end | |
| end | |
| end | |
| end | |
| it 'passes request when called from discovery context' do | |
| result = subject.resolve_issuer(request: request) | |
| expect(result).to eq 'issuer-request' | |
| end | |
| it 'passes resource_owner when called from token context' do | |
| result = subject.resolve_issuer(resource_owner: resource_owner) | |
| expect(result).to eq 'issuer-resource-owner' | |
| end | |
| it 'prefers request over resource_owner when both are present' do | |
| result = subject.resolve_issuer(resource_owner: resource_owner, request: request) | |
| expect(result).to eq 'issuer-request' |
| def self.resolve_issuer(resource_owner: nil, application: nil, request: nil) | ||
| issuer = configuration.issuer | ||
| return issuer.to_s unless issuer.respond_to?(:call) | ||
|
|
There was a problem hiding this comment.
resolve_issuer currently does return issuer.to_s unless issuer.respond_to?(:call), which changes the behavior for a missing/nil issuer from nil to "". In discovery this can result in an issuer key with an empty string (since .compact won’t remove it), producing an invalid discovery document and potentially masking misconfiguration. Consider returning the raw value (no to_s) for non-callables, or raising when issuer is blank/nil.
| case issuer.arity | ||
| when 0 | ||
| issuer.call | ||
| when 1 | ||
| issuer.call(request || resource_owner) | ||
| when 2 | ||
| issuer.call(resource_owner, application) | ||
| else | ||
| issuer.call(resource_owner, application, request) | ||
| end.to_s |
There was a problem hiding this comment.
Proc#arity can be negative for callables with optional args (and some lambdas), and this case issuer.arity will route all negative arities to the else branch (calling with 3 args). That can raise ArgumentError for lambdas that accept at most 1–2 args, breaking backward compatibility. Consider handling negative arities explicitly (e.g., by using issuer.parameters to determine how many positional args to pass, or by mapping negative arity to its minimum required args).
|
CI failures seem related to recent changes on doorkeeper main (uninitialized constant Doorkeeper::AccessGrant, FrozenError in Rails Engine). This appears unrelated to my changes — would it be worth checking if master CI also fails with the latest doorkeeper main? |
|
Master branch looks fine 👀 |
|
The failing job uses |
|
Can we rebase with the latest master please? 🙏 Thanks! |
The `issuer` configuration block received inconsistent parameters depending on the caller:
- DiscoveryController passed `request` as the sole argument
- IdToken passed `resource_owner, application`
This made it impossible to write a single issuer block that works correctly in both contexts.
Introduce `Doorkeeper::OpenidConnect.resolve_issuer` to centralize issuer resolution with a consistent interface:
issuer do |resource_owner, application, request|
request&.base_url || "https://default.example.com"
end
Backward compatibility is maintained via arity checks:
- arity 0: called with no arguments
- arity 1: called with `request` or `resource_owner` (legacy)
- arity 2: called with `resource_owner, application` (generator default)
- arity 3+: called with `resource_owner, application, request` (new)
Closes #238
Address Copilot review feedback: - Use object identity (equal?) to distinguish request from resource_owner in arity-1 tests, ensuring the specs actually verify which argument is passed in each context - Replace include(...).twice with scan-based count for arity-2 test, as .twice is not a valid modifier for the include matcher
d7b2c86 to
ef783d0
Compare
|
Rebased onto latest master 😊 Thanks! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The first describe block was a leftover from a rebase and contained the pre-review version (invalid `.twice` matcher and weak arity-1 assertions that didn't actually verify argument identity). The second block is the improved version addressing previous review feedback.
Closes #238
Problem
The
issuerconfiguration block receives inconsistent parameters depending on the caller:DiscoveryControllerissuer.call(request)IdTokenissuer.call(resource_owner, application)This means there is no single block signature that works correctly in both contexts. For example, the generator template uses
|resource_owner, application|, but when called fromDiscoveryController, therequestobject is passed asresource_owner.Solution
Introduce
Doorkeeper::OpenidConnect.resolve_issueras a centralized method (following the same pattern as.signing_algorithmand.signing_key) that accepts keyword arguments:The new recommended block signature is:
In discovery context,
resource_ownerandapplicationarenil.In token context,
requestisnil.Backward compatibility
Existing configurations continue to work via arity-based dispatch:
issuer { "..." }issuer { |req| ... }requestorresource_owner(legacy)issuer { |owner, app| ... }resource_owner, applicationissuer { |owner, app, req| ... }Changes
Doorkeeper::OpenidConnect.resolve_issuerwith arity-based backward compatibilityDiscoveryController#issuerandIdToken#issuerto delegate toresolve_issuer