From 7eddea8356560e3abd03a2322c922bd9cdd8c920 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Sun, 19 Jul 2026 13:59:35 +1000 Subject: [PATCH] DCR: accept every grant type discovery advertises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovery advertised the device_code grant in grant_types_supported, but dynamic client registration only allowed authorization_code/refresh_token, so a client listing the device grant in its RFC 7591 metadata was rejected with invalid_client_metadata. Introduce a single source of truth — OidcController::SUPPORTED_GRANT_TYPES — used by discovery (grant_types_supported) and by registration validation, so advertisement and registration can never drift. The token dispatcher already handles exactly these grants for all clients (they are user-context grants gated by consent + user_allowed?, so there is no per-client grant restriction), making advertisement, registration, and enforcement consistent. Tests: a client registering with the device_code grant now succeeds, plus an assertion that registration's accepted set equals discovery's advertised set. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq --- app/controllers/oidc_controller.rb | 14 +++++++++++++- .../oidc_registration_controller.rb | 12 +++++++++--- .../oidc_registration_controller_test.rb | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/controllers/oidc_controller.rb b/app/controllers/oidc_controller.rb index f7c07f9..8fb4a96 100644 --- a/app/controllers/oidc_controller.rb +++ b/app/controllers/oidc_controller.rb @@ -1,6 +1,18 @@ class OidcController < ApplicationController SUPPORTED_SCOPES = %w[openid profile email groups offline_access].freeze + # Grant types this authorization server supports. Single source of truth: + # advertised in discovery (grant_types_supported), accepted at dynamic client + # registration, and dispatched by the token endpoint. clinch offers all of these + # to every OIDC client — they are all user-context grants gated by consent and + # Application#user_allowed?, so there is no per-client grant restriction to + # enforce. Keep this in sync with the `case grant_type` dispatch in #token. + SUPPORTED_GRANT_TYPES = [ + "authorization_code", + "refresh_token", + "urn:ietf:params:oauth:grant-type:device_code" + ].freeze + # Discovery and JWKS endpoints are public # authorize is also unauthenticated to handle prompt=none and prompt=login specially allow_unauthenticated_access only: [:discovery, :jwks, :token, :revoke, :introspect, :userinfo, :logout, :authorize, :device_authorization] @@ -54,7 +66,7 @@ class OidcController < ApplicationController end_session_endpoint: "#{base_url}/logout", response_types_supported: ["code"], response_modes_supported: ["query"], - grant_types_supported: ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"], + grant_types_supported: SUPPORTED_GRANT_TYPES, subject_types_supported: ["pairwise"], id_token_signing_alg_values_supported: ["RS256"], scopes_supported: SUPPORTED_SCOPES, diff --git a/app/controllers/oidc_registration_controller.rb b/app/controllers/oidc_registration_controller.rb index 2aaaa26..81b97a3 100644 --- a/app/controllers/oidc_registration_controller.rb +++ b/app/controllers/oidc_registration_controller.rb @@ -16,9 +16,15 @@ class OidcRegistrationController < ApplicationController } AUTH_METHODS = %w[none client_secret_basic client_secret_post].freeze - SUPPORTED_GRANT_TYPES = %w[authorization_code refresh_token].freeze SUPPORTED_RESPONSE_TYPES = %w[code].freeze + # Accept exactly the grant types the authorization server advertises in + # discovery (single source of truth), so a client cannot be rejected for + # requesting a grant the server actually supports (e.g. the device_code grant). + def supported_grant_types + OidcController::SUPPORTED_GRANT_TYPES + end + # POST /oauth/register def create unless Application.dynamic_registration_enabled? @@ -37,8 +43,8 @@ class OidcRegistrationController < ApplicationController end grant_types = Array(metadata["grant_types"].presence || ["authorization_code"]) - if (grant_types - SUPPORTED_GRANT_TYPES).any? - return register_error("invalid_client_metadata", "Unsupported grant_types; only #{SUPPORTED_GRANT_TYPES.join(", ")} are allowed") + if (grant_types - supported_grant_types).any? + return register_error("invalid_client_metadata", "Unsupported grant_types; only #{supported_grant_types.join(", ")} are allowed") end response_types = Array(metadata["response_types"].presence || ["code"]) diff --git a/test/controllers/oidc_registration_controller_test.rb b/test/controllers/oidc_registration_controller_test.rb index a793410..e7bda37 100644 --- a/test/controllers/oidc_registration_controller_test.rb +++ b/test/controllers/oidc_registration_controller_test.rb @@ -89,6 +89,25 @@ class OidcRegistrationControllerTest < ActionDispatch::IntegrationTest assert_equal "invalid_client_metadata", JSON.parse(@response.body)["error"] end + test "registers a client requesting the device_code grant advertised in discovery" do + enable_dcr + register( + redirect_uris: ["https://client.example.com/cb"], + token_endpoint_auth_method: "none", + grant_types: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"] + ) + assert_response :created + assert_includes JSON.parse(@response.body)["grant_types"], "urn:ietf:params:oauth:grant-type:device_code" + end + + test "registration accepts exactly the grant types discovery advertises" do + get "/.well-known/openid-configuration" + advertised = JSON.parse(@response.body)["grant_types_supported"] + # Single source of truth: what we advertise is what registration accepts. + assert_equal OidcController::SUPPORTED_GRANT_TYPES, advertised + assert_includes advertised, "urn:ietf:params:oauth:grant-type:device_code" + end + test "rejects a non-JSON body" do enable_dcr post "/oauth/register", params: "not json", headers: JSON_HEADERS