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