DCR: accept every grant type discovery advertises
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
71ee301dd2
commit
7eddea8356
@@ -1,6 +1,18 @@
|
|||||||
class OidcController < ApplicationController
|
class OidcController < ApplicationController
|
||||||
SUPPORTED_SCOPES = %w[openid profile email groups offline_access].freeze
|
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
|
# Discovery and JWKS endpoints are public
|
||||||
# authorize is also unauthenticated to handle prompt=none and prompt=login specially
|
# 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]
|
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",
|
end_session_endpoint: "#{base_url}/logout",
|
||||||
response_types_supported: ["code"],
|
response_types_supported: ["code"],
|
||||||
response_modes_supported: ["query"],
|
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"],
|
subject_types_supported: ["pairwise"],
|
||||||
id_token_signing_alg_values_supported: ["RS256"],
|
id_token_signing_alg_values_supported: ["RS256"],
|
||||||
scopes_supported: SUPPORTED_SCOPES,
|
scopes_supported: SUPPORTED_SCOPES,
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ class OidcRegistrationController < ApplicationController
|
|||||||
}
|
}
|
||||||
|
|
||||||
AUTH_METHODS = %w[none client_secret_basic client_secret_post].freeze
|
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
|
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
|
# POST /oauth/register
|
||||||
def create
|
def create
|
||||||
unless Application.dynamic_registration_enabled?
|
unless Application.dynamic_registration_enabled?
|
||||||
@@ -37,8 +43,8 @@ class OidcRegistrationController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
grant_types = Array(metadata["grant_types"].presence || ["authorization_code"])
|
grant_types = Array(metadata["grant_types"].presence || ["authorization_code"])
|
||||||
if (grant_types - SUPPORTED_GRANT_TYPES).any?
|
if (grant_types - supported_grant_types).any?
|
||||||
return register_error("invalid_client_metadata", "Unsupported grant_types; only #{SUPPORTED_GRANT_TYPES.join(", ")} are allowed")
|
return register_error("invalid_client_metadata", "Unsupported grant_types; only #{supported_grant_types.join(", ")} are allowed")
|
||||||
end
|
end
|
||||||
|
|
||||||
response_types = Array(metadata["response_types"].presence || ["code"])
|
response_types = Array(metadata["response_types"].presence || ["code"])
|
||||||
|
|||||||
@@ -89,6 +89,25 @@ class OidcRegistrationControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "invalid_client_metadata", JSON.parse(@response.body)["error"]
|
assert_equal "invalid_client_metadata", JSON.parse(@response.body)["error"]
|
||||||
end
|
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
|
test "rejects a non-JSON body" do
|
||||||
enable_dcr
|
enable_dcr
|
||||||
post "/oauth/register", params: "not json", headers: JSON_HEADERS
|
post "/oauth/register", params: "not json", headers: JSON_HEADERS
|
||||||
|
|||||||
Reference in New Issue
Block a user