VYPR
Medium severity6.3NVD Advisory· Published Jun 4, 2026

Doorkeeper Openid Connect: Dynamic Client Registration feature creates public clients with client_secret

CVE-2026-44476

Description

Impact

The DynamicClientRegistrationController#register action hard-codes confidential: false when creating applications (dynamic_client_registration_controller.rb:18-25), yet the response includes a client_secret and advertises token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"].

Because Doorkeeper's Application.by_uid_and_secret treats a blank/missing secret as valid for non-confidential (public) clients, an attacker who knows only the client_id (which is public information) can authenticate as the dynamically-registered client at the token endpoint.

Note that Dynamic Client Registration is opt-in feature which is disabled by default so only projects that explicitly enabled it are affected.

Steps to Reproduce

1. Enable dynamic client registration in the initializer 2. POST /oauth/registration with client_name, redirect_uris, and scope 3. Observe: response returns client_secret, but the created Doorkeeper::Application has confidential: false 4. Call Doorkeeper::Application.by_uid_and_secret(client_id, nil) — it returns the application (credentials bypass) 5. POST /oauth/token with grant_type=client_credentials and only client_id (no client_secret) — the token endpoint issues an access token without any secret verification

Patches

Patched in 1.10.0

Workarounds

Upgrade existing applications created with a Dynamic Client registration to have confidential: true

Affected products

2

Patches

7
561af83dcf71

Determine confidentiality from token_endpoint_auth_method in dynamic client registration

https://github.com/doorkeeper-gem/doorkeeper-openid_connectKenta IshizakiApr 29, 2026Fixed in 1.10.0via ghsa-release-walk
5 files changed · +254 54
  • app/controllers/concerns/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb+16 0 added
    @@ -0,0 +1,16 @@
    +# frozen_string_literal: true
    +
    +module Doorkeeper
    +  module OpenidConnect
    +    module TokenEndpointAuthMethodsSupportedMixin
    +      CLIENT_CREDENTIALS_METHOD_MAPPING = {
    +        from_basic: 'client_secret_basic',
    +        from_params: 'client_secret_post',
    +      }.freeze
    +
    +      def token_endpoint_auth_methods_supported(doorkeeper)
    +        doorkeeper.client_credentials_methods.filter_map { |method| CLIENT_CREDENTIALS_METHOD_MAPPING[method] }
    +      end
    +    end
    +  end
    +end
    
  • app/controllers/doorkeeper/openid_connect/discovery_controller.rb+1 5 modified
    @@ -5,6 +5,7 @@ module OpenidConnect
         class DiscoveryController < ::Doorkeeper::ApplicationMetalController
           include Doorkeeper::Helpers::Controller
           include GrantTypesSupportedMixin
    +      include TokenEndpointAuthMethodsSupportedMixin
     
           WEBFINGER_RELATION = 'http://openid.net/specs/connect/1.0/issuer'
     
    @@ -79,11 +80,6 @@ def response_modes_supported(doorkeeper)
             doorkeeper.authorization_response_flows.flat_map(&:response_mode_matches).uniq
           end
     
    -      def token_endpoint_auth_methods_supported(doorkeeper)
    -        mapping = { from_basic: 'client_secret_basic', from_params: 'client_secret_post' }
    -        doorkeeper.client_credentials_methods.filter_map { |method| mapping[method] }
    -      end
    -
           def code_challenge_methods_supported(doorkeeper)
             return unless doorkeeper.access_grant_model.pkce_supported?
     
    
  • app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb+36 5 modified
    @@ -4,8 +4,21 @@ module Doorkeeper
       module OpenidConnect
         class DynamicClientRegistrationController < ::Doorkeeper::ApplicationMetalController
           include GrantTypesSupportedMixin
    +      include TokenEndpointAuthMethodsSupportedMixin
    +
    +      DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD = 'client_secret_basic'
    +      PUBLIC_CLIENT_AUTH_METHOD = 'none'
     
           def register
    +        unless supported_auth_methods.include?(requested_auth_method)
    +          render json: {
    +            error: 'invalid_client_metadata',
    +            error_description: "token_endpoint_auth_method '#{requested_auth_method}' is not supported. " \
    +                               "Supported methods: #{supported_auth_methods.join(', ')}",
    +          }, status: :bad_request
    +          return
    +        end
    +
             client = Doorkeeper::Application.create!(application_params)
             render json: registration_response(client), status: :created
           rescue ActiveRecord::RecordInvalid => e
    @@ -20,24 +33,42 @@ def application_params
               name: params.dig(:client_name),
               redirect_uri: params.dig(:redirect_uris) || [],
               scopes: params.dig(:scope),
    -          confidential: false,
    +          confidential: confidential_client?,
             }
           end
     
    +      def requested_auth_method
    +        params[:token_endpoint_auth_method].presence || DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD
    +      end
    +
    +      def confidential_client?
    +        requested_auth_method != PUBLIC_CLIENT_AUTH_METHOD
    +      end
    +
    +      def supported_auth_methods
    +        token_endpoint_auth_methods_supported(::Doorkeeper.configuration) + [PUBLIC_CLIENT_AUTH_METHOD]
    +      end
    +
           def registration_response(doorkeeper_application)
             doorkeeper_config = ::Doorkeeper.configuration
     
    -        {
    -          client_secret: doorkeeper_application.plaintext_secret || doorkeeper_application.secret,
    +        response = {
               client_id: doorkeeper_application.uid,
               client_id_issued_at: doorkeeper_application.created_at.to_i,
               redirect_uris: doorkeeper_application.redirect_uri.split,
    -          token_endpoint_auth_methods_supported: %w[client_secret_basic client_secret_post],
    +          token_endpoint_auth_method: requested_auth_method,
    +          token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported(doorkeeper_config),
               response_types: doorkeeper_config.authorization_response_types,
               grant_types: grant_types_supported(doorkeeper_config),
               scope: doorkeeper_application.scopes.to_s,
    -          application_type: "web"
    +          application_type: 'web',
             }
    +
    +        if confidential_client?
    +          response[:client_secret] = doorkeeper_application.plaintext_secret || doorkeeper_application.secret
    +        end
    +
    +        response
           end
         end
       end
    
  • CHANGELOG.md+1 0 modified
    @@ -11,6 +11,7 @@
     - [#258] Skip `IdToken` construction on password grants without the `openid` scope
     - [#259] Skip `IdToken` construction on authorization code grants without the `openid` scope
     - [#261] Fix obsolete RuboCop configuration (`require:` → `plugins:`, `RSpec/FilePath` split, remove `Capybara/FeatureMethods`)
    +- [#263] **Security/Breaking:** Determine dynamically registered client's `confidential` flag from `token_endpoint_auth_method` per RFC 7591 — previously every dynamically registered client was created as public (`confidential: false`), which let callers authenticate with only `client_id` (`by_uid_and_secret(uid, nil)` bypass). Default is now `client_secret_basic` (confidential); `none` produces a public client; unsupported values (e.g. `private_key_jwt`) are rejected with `invalid_client_metadata`. Also derive `token_endpoint_auth_methods_supported` in the response from `Doorkeeper.configuration.client_credentials_methods` instead of a hardcoded list, matching #236
     
     ## v1.9.0 (2026-03-16)
     
    
  • spec/controllers/dynamic_client_registration_controller_spec.rb+200 44 modified
    @@ -3,6 +3,13 @@
     require 'rails_helper'
     
     describe Doorkeeper::OpenidConnect::DynamicClientRegistrationController, type: :controller do
    +  let(:redirect_uris) do
    +    [
    +      'https://test.host/registration_success',
    +      'https://test.host/registration_success_second_location',
    +    ]
    +  end
    +
       before do
         Doorkeeper::OpenidConnect.configure do
           issuer "dummy"
    @@ -13,50 +20,199 @@
       end
     
       describe "#register" do
    -    it "creates a Doorkeeper::Application" do
    -      redirect_uris = [
    -        'https://test.host/registration_success',
    -        'https://test.host/registration_success_second_location',
    -      ]
    -
    -      post :register, params: {
    -        client_name: "dummy_client",
    -        redirect_uris: redirect_uris,
    -        scope: "public"
    -      }
    -
    -      expect(response.status).to eq 201
    -      expect(Doorkeeper::Application.count).to eq(1)
    -
    -      doorkeeper_application = Doorkeeper::Application.first
    -      expect(JSON.parse(response.body)).to eq({
    -        'client_secret' => doorkeeper_application.plaintext_secret || doorkeeper_application.secret,
    -        'client_id' => doorkeeper_application.uid,
    -        'client_id_issued_at' => doorkeeper_application.created_at.to_i,
    -        'redirect_uris' => redirect_uris,
    -        'token_endpoint_auth_methods_supported' => %w[client_secret_basic client_secret_post],
    -        'response_types' => ['code', 'token', 'id_token', 'id_token token'],
    -        'grant_types' => %w[authorization_code client_credentials implicit_oidc],
    -        'scope' => "public",
    -        'application_type' => "web"
    -      })
    -    end
    -
    -    it "errors and returns errors" do
    -      post :register, params: {
    -        client_name: "dummy_client",
    -        redirect_uris: [
    -          'http://test.host/registration_success',
    -        ],
    -        scopes: "openid"
    -      }
    -
    -      expect(response.status).to eq 400
    -      expect(Doorkeeper::Application.count).to eq(0)
    -      expect(JSON.parse(response.body)).to eq({
    -        "error" => "invalid_client_params",
    -        "error_description" => "Redirect URI must be an HTTPS/SSL URI."
    -      })
    +    context "when token_endpoint_auth_method is omitted" do
    +      it "defaults to client_secret_basic and creates a confidential client with a secret" do
    +        post :register, params: {
    +          client_name: "dummy_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public"
    +        }
    +
    +        expect(response.status).to eq 201
    +        expect(Doorkeeper::Application.count).to eq(1)
    +
    +        doorkeeper_application = Doorkeeper::Application.first
    +        expect(doorkeeper_application.confidential).to be true
    +
    +        body = JSON.parse(response.body)
    +        expect(body).to eq({
    +          'client_secret' => doorkeeper_application.plaintext_secret || doorkeeper_application.secret,
    +          'client_id' => doorkeeper_application.uid,
    +          'client_id_issued_at' => doorkeeper_application.created_at.to_i,
    +          'redirect_uris' => redirect_uris,
    +          'token_endpoint_auth_method' => 'client_secret_basic',
    +          'token_endpoint_auth_methods_supported' => %w[client_secret_basic client_secret_post],
    +          'response_types' => ['code', 'token', 'id_token', 'id_token token'],
    +          'grant_types' => %w[authorization_code client_credentials implicit_oidc],
    +          'scope' => "public",
    +          'application_type' => "web",
    +        })
    +      end
    +    end
    +
    +    context "when token_endpoint_auth_method is client_secret_basic" do
    +      it "creates a confidential client with a secret" do
    +        post :register, params: {
    +          client_name: "basic_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          token_endpoint_auth_method: "client_secret_basic",
    +        }
    +
    +        expect(response.status).to eq 201
    +
    +        doorkeeper_application = Doorkeeper::Application.first
    +        expect(doorkeeper_application.confidential).to be true
    +
    +        body = JSON.parse(response.body)
    +        expect(body['token_endpoint_auth_method']).to eq('client_secret_basic')
    +        expect(body['client_secret']).to be_present
    +      end
    +    end
    +
    +    context "when token_endpoint_auth_method is client_secret_post" do
    +      it "creates a confidential client with a secret" do
    +        post :register, params: {
    +          client_name: "post_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          token_endpoint_auth_method: "client_secret_post",
    +        }
    +
    +        expect(response.status).to eq 201
    +
    +        doorkeeper_application = Doorkeeper::Application.first
    +        expect(doorkeeper_application.confidential).to be true
    +
    +        body = JSON.parse(response.body)
    +        expect(body['token_endpoint_auth_method']).to eq('client_secret_post')
    +        expect(body['client_secret']).to be_present
    +      end
    +    end
    +
    +    context "when token_endpoint_auth_method is none" do
    +      it "creates a public client and omits client_secret from the response" do
    +        post :register, params: {
    +          client_name: "public_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          token_endpoint_auth_method: "none",
    +        }
    +
    +        expect(response.status).to eq 201
    +
    +        doorkeeper_application = Doorkeeper::Application.first
    +        expect(doorkeeper_application.confidential).to be false
    +
    +        body = JSON.parse(response.body)
    +        expect(body['token_endpoint_auth_method']).to eq('none')
    +        expect(body).not_to have_key('client_secret')
    +      end
    +    end
    +
    +    context "when token_endpoint_auth_method is private_key_jwt" do
    +      it "rejects the request with invalid_client_metadata" do
    +        post :register, params: {
    +          client_name: "jwt_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          token_endpoint_auth_method: "private_key_jwt",
    +        }
    +
    +        expect(response.status).to eq 400
    +        expect(Doorkeeper::Application.count).to eq(0)
    +
    +        body = JSON.parse(response.body)
    +        expect(body['error']).to eq('invalid_client_metadata')
    +        expect(body['error_description']).to include('private_key_jwt')
    +      end
    +    end
    +
    +    context "when token_endpoint_auth_method is an unknown value" do
    +      it "rejects the request with invalid_client_metadata" do
    +        post :register, params: {
    +          client_name: "weird_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          token_endpoint_auth_method: "unknown_value",
    +        }
    +
    +        expect(response.status).to eq 400
    +        expect(Doorkeeper::Application.count).to eq(0)
    +
    +        body = JSON.parse(response.body)
    +        expect(body['error']).to eq('invalid_client_metadata')
    +        expect(body['error_description']).to include('unknown_value')
    +      end
    +    end
    +
    +    context "token_endpoint_auth_methods_supported in the response" do
    +      it "matches the server's configured client_credentials methods" do
    +        Doorkeeper.configure do
    +          orm :active_record
    +          client_credentials :from_basic
    +        end
    +
    +        post :register, params: {
    +          client_name: "cfg_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +        }
    +
    +        expect(response.status).to eq 201
    +        body = JSON.parse(response.body)
    +        expect(body['token_endpoint_auth_methods_supported']).to eq(%w[client_secret_basic])
    +      end
    +    end
    +
    +    context "security regression: confidential client cannot bypass credentials" do
    +      it "is not returned by by_uid_and_secret(uid, nil)" do
    +        post :register, params: {
    +          client_name: "secure_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +        }
    +
    +        expect(response.status).to eq 201
    +        doorkeeper_application = Doorkeeper::Application.first
    +        expect(doorkeeper_application.confidential).to be true
    +
    +        expect(Doorkeeper::Application.by_uid_and_secret(doorkeeper_application.uid, nil)).to be_nil
    +      end
    +
    +      it "is returned by by_uid_and_secret(uid, nil) when token_endpoint_auth_method is none" do
    +        post :register, params: {
    +          client_name: "intentionally_public",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          token_endpoint_auth_method: "none",
    +        }
    +
    +        expect(response.status).to eq 201
    +        doorkeeper_application = Doorkeeper::Application.first
    +        expect(doorkeeper_application.confidential).to be false
    +
    +        expect(Doorkeeper::Application.by_uid_and_secret(doorkeeper_application.uid, nil)).to eq(doorkeeper_application)
    +      end
    +    end
    +
    +    context "with invalid redirect_uris" do
    +      it "errors and returns errors" do
    +        post :register, params: {
    +          client_name: "dummy_client",
    +          redirect_uris: [
    +            'http://test.host/registration_success',
    +          ],
    +          scope: "openid"
    +        }
    +
    +        expect(response.status).to eq 400
    +        expect(Doorkeeper::Application.count).to eq(0)
    +        expect(JSON.parse(response.body)).to eq({
    +          "error" => "invalid_client_params",
    +          "error_description" => "Redirect URI must be an HTTPS/SSL URI."
    +        })
    +      end
         end
       end
     end
    
20048d906dc1
208e1211f008
22b55df75f70

Extract DCR validation into a dedicated class with Doorkeeper::Validations DSL

https://github.com/doorkeeper-gem/doorkeeper-openid_connectKenta IshizakiMay 6, 2026Fixed in 1.10.0via ghsa-release-walk
7 files changed · +227 37
  • app/controllers/doorkeeper/openid_connect/dynamic_client_registration_controller.rb+15 37 modified
    @@ -3,68 +3,46 @@
     module Doorkeeper
       module OpenidConnect
         class DynamicClientRegistrationController < ::Doorkeeper::ApplicationMetalController
    -      include GrantTypesSupportedMixin
    -      include TokenEndpointAuthMethodsSupportedMixin
    -
    -      DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_basic"
    -      PUBLIC_CLIENT_AUTH_METHOD = "none"
    -
           def register
    -        unless supported_auth_methods.include?(requested_auth_method)
    -          render json: {
    -            error: "invalid_client_metadata",
    -            error_description: "token_endpoint_auth_method '#{requested_auth_method}' is not supported. " \
    -                               "Supported methods: #{supported_auth_methods.join(", ")}",
    -          }, status: :bad_request
    +        registration = OAuth::DynamicRegistrationRequest.new(::Doorkeeper.configuration, params)
    +
    +        unless registration.valid?
    +          render json: registration.error_response, status: :bad_request
               return
             end
     
    -        client = Doorkeeper::Application.create!(application_params)
    -        render json: registration_response(client), status: :created
    +        client = Doorkeeper::Application.create!(application_params(registration))
    +        render json: registration_response(client, registration), status: :created
           rescue ActiveRecord::RecordInvalid => e
             render json: { error: "invalid_client_params", error_description: e.record.errors.full_messages.join(", ") },
               status: :bad_request
           end
     
           private
     
    -      def application_params
    +      def application_params(registration)
             {
               name: params.dig(:client_name),
               redirect_uri: params.dig(:redirect_uris) || [],
               scopes: params.dig(:scope),
    -          confidential: confidential_client?,
    +          confidential: registration.confidential_client?,
             }
           end
     
    -      def requested_auth_method
    -        params[:token_endpoint_auth_method].presence || DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD
    -      end
    -
    -      def confidential_client?
    -        requested_auth_method != PUBLIC_CLIENT_AUTH_METHOD
    -      end
    -
    -      def supported_auth_methods
    -        token_endpoint_auth_methods_supported + [PUBLIC_CLIENT_AUTH_METHOD]
    -      end
    -
    -      def registration_response(doorkeeper_application)
    -        doorkeeper_config = ::Doorkeeper.configuration
    -
    +      def registration_response(doorkeeper_application, registration)
             response = {
               client_id: doorkeeper_application.uid,
               client_id_issued_at: doorkeeper_application.created_at.to_i,
               redirect_uris: doorkeeper_application.redirect_uri.split,
    -          token_endpoint_auth_method: requested_auth_method,
    -          token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
    -          response_types: doorkeeper_config.authorization_response_types,
    -          grant_types: grant_types_supported(doorkeeper_config),
    +          token_endpoint_auth_method: registration.token_endpoint_auth_method,
    +          token_endpoint_auth_methods_supported: registration.token_endpoint_auth_methods_supported,
    +          response_types: registration.requested_response_types,
    +          grant_types: registration.requested_grant_types,
               scope: doorkeeper_application.scopes.to_s,
    -          application_type: "web",
    +          application_type: registration.requested_application_type,
             }
     
    -        if confidential_client?
    +        if registration.confidential_client?
               response[:client_secret] =
                 doorkeeper_application.plaintext_secret || doorkeeper_application.secret
             end
    
  • CHANGELOG.md+1 0 modified
    @@ -14,6 +14,7 @@
     - [#263] **Security/Breaking:** Determine dynamically registered client's `confidential` flag from `token_endpoint_auth_method` per RFC 7591 — previously every dynamically registered client was created as public (`confidential: false`), which let callers authenticate with only `client_id` (`by_uid_and_secret(uid, nil)` bypass). Default is now `client_secret_basic` (confidential); `none` produces a public client; unsupported values (e.g. `private_key_jwt`) are rejected with `invalid_client_metadata`. Also derive `token_endpoint_auth_methods_supported` in the response from `Doorkeeper.configuration.client_credentials_methods` instead of a hardcoded list, matching #236
     - [#264] Apply safe RuboCop autocorrections and fix resulting artifacts
     - [#265] Add Dynamic Client Registration section to README
    +- [#266] Validate `application_type`, `response_types`, and `grant_types` parameters in dynamic client registration per RFC 7591 — reject unsupported values with `invalid_client_metadata` and echo the requested values back in the registration response, instead of silently ignoring them and returning the server's global configuration
     
     ## v1.9.0 (2026-03-16)
     
    
  • lib/doorkeeper/openid_connect/grant_types_supported_mixin.rb+0 0 renamed
  • lib/doorkeeper/openid_connect/oauth/dynamic_registration_request.rb+108 0 added
    @@ -0,0 +1,108 @@
    +# frozen_string_literal: true
    +
    +module Doorkeeper
    +  module OpenidConnect
    +    module OAuth
    +      class DynamicRegistrationRequest
    +        include Doorkeeper::Validations
    +        include Doorkeeper::OpenidConnect::TokenEndpointAuthMethodsSupportedMixin
    +        include Doorkeeper::OpenidConnect::GrantTypesSupportedMixin
    +
    +        DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD = "client_secret_basic"
    +        PUBLIC_CLIENT_AUTH_METHOD = "none"
    +        DEFAULT_APPLICATION_TYPE = "web"
    +        SUPPORTED_APPLICATION_TYPES = %w[web native].freeze
    +
    +        validate :token_endpoint_auth_method, error: :invalid_client_metadata
    +        validate :application_type,           error: :invalid_client_metadata
    +        validate :response_types,             error: :invalid_client_metadata
    +        validate :grant_types,                error: :invalid_client_metadata
    +
    +        def initialize(server, params)
    +          @server = server
    +          @params = params
    +        end
    +
    +        def token_endpoint_auth_method
    +          @params[:token_endpoint_auth_method].presence || DEFAULT_TOKEN_ENDPOINT_AUTH_METHOD
    +        end
    +
    +        def requested_application_type
    +          @params[:application_type].presence || DEFAULT_APPLICATION_TYPE
    +        end
    +
    +        def requested_response_types
    +          types = Array(@params[:response_types]).compact_blank
    +          types.presence || server_response_types
    +        end
    +
    +        def requested_grant_types
    +          types = Array(@params[:grant_types]).compact_blank
    +          types.presence || server_grant_types
    +        end
    +
    +        def confidential_client?
    +          token_endpoint_auth_method != PUBLIC_CLIENT_AUTH_METHOD
    +        end
    +
    +        def error_response
    +          { error: error.to_s, error_description: @error_description }
    +        end
    +
    +        private
    +
    +        attr_reader :server
    +
    +        def validate_token_endpoint_auth_method
    +          return true if supported_auth_methods.include?(token_endpoint_auth_method)
    +
    +          @error_description =
    +            "token_endpoint_auth_method '#{token_endpoint_auth_method}' is not supported. " \
    +            "Supported methods: #{supported_auth_methods.join(", ")}"
    +          false
    +        end
    +
    +        def validate_application_type
    +          return true if SUPPORTED_APPLICATION_TYPES.include?(requested_application_type)
    +
    +          @error_description =
    +            "application_type '#{requested_application_type}' is not supported. " \
    +            "Supported types: #{SUPPORTED_APPLICATION_TYPES.join(", ")}"
    +          false
    +        end
    +
    +        def validate_response_types
    +          unsupported = requested_response_types - server_response_types
    +          return true if unsupported.empty?
    +
    +          @error_description =
    +            "response_types #{unsupported.join(", ")} are not supported. " \
    +            "Supported types: #{server_response_types.join(", ")}"
    +          false
    +        end
    +
    +        def validate_grant_types
    +          unsupported = requested_grant_types - server_grant_types
    +          return true if unsupported.empty?
    +
    +          @error_description =
    +            "grant_types #{unsupported.join(", ")} are not supported. " \
    +            "Supported types: #{server_grant_types.join(", ")}"
    +          false
    +        end
    +
    +        def server_response_types
    +          server.authorization_response_types
    +        end
    +
    +        def server_grant_types
    +          grant_types_supported(server)
    +        end
    +
    +        def supported_auth_methods
    +          token_endpoint_auth_methods_supported + [PUBLIC_CLIENT_AUTH_METHOD]
    +        end
    +      end
    +    end
    +  end
    +end
    
  • lib/doorkeeper/openid_connect.rb+4 0 modified
    @@ -25,8 +25,12 @@
     
     require "doorkeeper/openid_connect/helpers/controller"
     
    +require "doorkeeper/openid_connect/grant_types_supported_mixin"
    +require "doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin"
    +
     require "doorkeeper/openid_connect/oauth/authorization/code"
     require "doorkeeper/openid_connect/oauth/authorization_code_request"
    +require "doorkeeper/openid_connect/oauth/dynamic_registration_request"
     require "doorkeeper/openid_connect/oauth/password_access_token_request"
     require "doorkeeper/openid_connect/oauth/pre_authorization"
     require "doorkeeper/openid_connect/oauth/token_response"
    
  • lib/doorkeeper/openid_connect/token_endpoint_auth_methods_supported_mixin.rb+0 0 renamed
  • spec/controllers/dynamic_client_registration_controller_spec.rb+99 0 modified
    @@ -196,6 +196,105 @@
           end
         end
     
    +    context "when application_type is native" do
    +      it "echoes application_type back in the response" do
    +        post :register, params: {
    +          client_name: "native_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          application_type: "native",
    +        }
    +
    +        expect(response.status).to eq 201
    +        body = JSON.parse(response.body)
    +        expect(body["application_type"]).to eq("native")
    +      end
    +    end
    +
    +    context "when application_type is not supported" do
    +      it "rejects the request with invalid_client_metadata" do
    +        post :register, params: {
    +          client_name: "weird_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          application_type: "service",
    +        }
    +
    +        expect(response.status).to eq 400
    +        expect(Doorkeeper::Application.count).to eq(0)
    +
    +        body = JSON.parse(response.body)
    +        expect(body["error"]).to eq("invalid_client_metadata")
    +        expect(body["error_description"]).to include("application_type 'service'")
    +      end
    +    end
    +
    +    context "when response_types is a subset of the server's supported types" do
    +      it "echoes the requested response_types back in the response" do
    +        post :register, params: {
    +          client_name: "code_only_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          response_types: ["code"],
    +        }
    +
    +        expect(response.status).to eq 201
    +        body = JSON.parse(response.body)
    +        expect(body["response_types"]).to eq(["code"])
    +      end
    +    end
    +
    +    context "when response_types contains an unsupported value" do
    +      it "rejects the request with invalid_client_metadata" do
    +        post :register, params: {
    +          client_name: "bad_response_type_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          response_types: %w[code unsupported_type],
    +        }
    +
    +        expect(response.status).to eq 400
    +        expect(Doorkeeper::Application.count).to eq(0)
    +
    +        body = JSON.parse(response.body)
    +        expect(body["error"]).to eq("invalid_client_metadata")
    +        expect(body["error_description"]).to include("unsupported_type")
    +      end
    +    end
    +
    +    context "when grant_types is a subset of the server's supported types" do
    +      it "echoes the requested grant_types back in the response" do
    +        post :register, params: {
    +          client_name: "code_grant_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          grant_types: ["authorization_code"],
    +        }
    +
    +        expect(response.status).to eq 201
    +        body = JSON.parse(response.body)
    +        expect(body["grant_types"]).to eq(["authorization_code"])
    +      end
    +    end
    +
    +    context "when grant_types contains an unsupported value" do
    +      it "rejects the request with invalid_client_metadata" do
    +        post :register, params: {
    +          client_name: "bad_grant_client",
    +          redirect_uris: redirect_uris,
    +          scope: "public",
    +          grant_types: %w[authorization_code password],
    +        }
    +
    +        expect(response.status).to eq 400
    +        expect(Doorkeeper::Application.count).to eq(0)
    +
    +        body = JSON.parse(response.body)
    +        expect(body["error"]).to eq("invalid_client_metadata")
    +        expect(body["error_description"]).to include("password")
    +      end
    +    end
    +
         context "with invalid redirect_uris" do
           it "errors and returns errors" do
             post :register, params: {
    
2754bbb5a0cc

Treat auth_time_from_resource_owner as optional in IdToken

https://github.com/doorkeeper-gem/doorkeeper-openid_connectKenta IshizakiApr 24, 2026Fixed in 1.10.0via ghsa-release-walk
3 files changed · +26 1
  • CHANGELOG.md+2 1 modified
    @@ -3,9 +3,10 @@
     - Please add here
     - [#241] Fix NameError on doorkeeper master by deferring AR model loading in run_hooks (see [Doorkeeper PR](https://github.com/doorkeeper-gem/doorkeeper/pull/1804))
     - [#246] Fix `at_hash` to use correct hash algorithm based on `signing_algorithm`
    -* [#250] Return configured `issuer` instead of `root_url` in WebFinger response (thanks to @sato11 for the original work in #172)
    +- [#250] Return configured `issuer` instead of `root_url` in WebFinger response (thanks to @sato11 for the original work in #172)
     - [#248] Fix `max_age` always triggering reauthentication when `auth_time_from_resource_owner` returns Integer
     - [#254] **Breaking:** Omit `expires_in` from the `response_type=id_token` response (OIDC Core §3.2.2.5 — `expires_in` represents the Access Token lifetime; it is still returned for `response_type=id_token token`)
    +- [#252] Treat `auth_time_from_resource_owner` as optional in `IdToken` — omit `auth_time` claim when unconfigured instead of raising `InvalidConfiguration`
     
     ## v1.9.0 (2026-03-16)
     
    
  • lib/doorkeeper/openid_connect/id_token.rb+2 0 modified
    @@ -74,6 +74,8 @@ def issued_at
     
           def auth_time
             Doorkeeper::OpenidConnect.configuration.auth_time_from_resource_owner.call(@resource_owner).try(:to_i)
    +      rescue Errors::InvalidConfiguration
    +        nil
           end
         end
       end
    
  • spec/lib/id_token_spec.rb+22 0 modified
    @@ -100,6 +100,28 @@
             expect(claims[:iss]).to eq "#{user.id}-#{access_token.application.uid}"
           end
         end
    +
    +    context 'when auth_time_from_resource_owner is not configured' do
    +      before do
    +        Doorkeeper::OpenidConnect.configure do
    +          issuer 'dummy'
    +
    +          resource_owner_from_access_token do |access_token|
    +            User.find_by(id: access_token.resource_owner_id)
    +          end
    +
    +          subject do |resource_owner|
    +            resource_owner.id
    +          end
    +        end
    +      end
    +
    +      it 'builds claims without raising and omits auth_time' do
    +        expect { subject.claims }.not_to raise_error
    +        expect(subject.claims[:auth_time]).to be_nil
    +        expect(subject.as_json).not_to include(:auth_time)
    +      end
    +    end
       end
     
       describe '#as_json' do
    
2071176e6122
207a4c5b003c

Vulnerability mechanics

Root cause

"The Dynamic Client Registration feature incorrectly creates public clients with a client secret."

Attack vector

An attacker must know the client_id of a dynamically registered application. The attacker can then authenticate to the token endpoint using only the client_id, as the system treats a missing client secret as valid for public clients. This allows the attacker to obtain an access token without providing the actual client secret [ref_id=1].

Affected code

The vulnerability lies within the `DynamicClientRegistrationController#register` action, specifically in the code responsible for creating new applications. This action hard-codes `confidential: false` when creating applications, even when a client secret is generated, as indicated in `dynamic_client_registration_controller.rb:18-25` [ref_id=1].

What the fix does

The patch ensures that when dynamic client registration creates an application, it correctly sets the `confidential` attribute to `true` if a client secret is generated. This aligns the application's confidentiality status with the presence of a secret, preventing public clients from being issued secrets and thus closing the authentication bypass vulnerability [ref_id=1].

Preconditions

  • configDynamic Client Registration must be enabled in the initializer.
  • inputThe attacker must know the client_id of a dynamically registered application.

Reproduction

1. Enable dynamic client registration in the initializer. 2. POST /oauth/registration with client_name, redirect_uris, and scope. 3. Observe: response returns client_secret, but the created Doorkeeper::Application has confidential: false. 4. Call `Doorkeeper::Application.by_uid_and_secret(client_id, nil)` — it returns the application (credentials bypass). 5. POST /oauth/token with grant_type=client_credentials and only client_id (no client_secret) — the token endpoint issues an access token without any secret verification [ref_id=1].

Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.