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:
Dan Milne
2026-07-19 13:59:35 +10:00
co-authored by Claude Opus 4.8
parent 71ee301dd2
commit 7eddea8356
3 changed files with 41 additions and 4 deletions
+13 -1
View File
@@ -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,
@@ -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"])