The supported-scope list lived on OidcController, so the device authorization
controller (and a test) had to reach across into OidcController::SUPPORTED_SCOPES.
Move it to a shared OidcScopes::SUPPORTED module — one source of truth for the
OIDC controller, the device flow, and consent handling — and update every
reference. No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Q4ATZHoMCWqvSpE2yYoie
Security-review follow-ups on the OIDC flows.
IPv6 loopback redirect_uri in dynamic client registration (RFC 8252):
- valid_redirect_uri? compared URI#host, which returns the bracketed "[::1]"
for IPv6 literals, so http://[::1]:PORT/... was always rejected. Compare
#hostname (unbracketed) instead, unblocking IPv6-only native clients.
Extract redirect_authorize_error (removes 12 copies of the boilerplate):
- The authorize/consent flows built the error redirect by hand ~a dozen times
(error_uri = "...?error=..."; += "&error_description=#{CGI.escape ...}"; +=
state; redirect_to). One helper now composes the query, and — unlike every
copy — appends "&error=..." when the redirect_uri already has a query string
instead of a malformed second "?".
Extract token-endpoint helpers (removes ~80 duplicated lines):
- authenticate_token_client: the identical client-auth preamble shared by the
authorization-code, refresh, and device-code grants.
- render_token_triple: the access + refresh + id_token mint and RFC 6749 §5.1
response shared by the authorization-code and device-code grants.
Single source of truth for scope descriptions:
- New OidcHelper#scope_description + shared shared/_scope_list partial, used by
both the browser consent screen and the device authorization screen; drops the
scope_labels hash and the hard-coded per-scope blocks that would have drifted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Q4ATZHoMCWqvSpE2yYoie