OIDC: IPv6 loopback DCR, dedup error redirects + scope labels

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
This commit is contained in:
Dan Milne
2026-07-19 14:14:44 +10:00
co-authored by Claude Opus 4.8
parent 79a7524fda
commit f65f8da2e7
9 changed files with 118 additions and 104 deletions
+2 -26
View File
@@ -27,17 +27,7 @@
<% if @scopes.any? %>
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">This will be able to:</h3>
<ul class="space-y-2">
<% scope_labels = { "openid" => "Verify your identity", "email" => "Access your email address (#{Current.user.email_address})", "profile" => "Access your profile information", "groups" => "Access your group memberships", "offline_access" => "Stay signed in (refresh access)" } %>
<% @scopes.each do |scope| %>
<li class="flex items-start">
<svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<span class="text-sm text-gray-700 dark:text-gray-300"><%= scope_labels[scope] || scope %></span>
</li>
<% end %>
</ul>
<%= render "shared/scope_list", scopes: @scopes %>
</div>
<% end %>
@@ -68,21 +58,7 @@
<% end %>
<% else %>
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
<%= @state == :expired ? "Code expired" : (@state == :already_handled ? "Code already used" : "Code not found") %>
</h2>
<p class="mt-3 text-sm text-gray-600 dark:text-gray-400">
<% if @state == :expired %>
This device code has expired. Start again from your tool to get a fresh code.
<% elsif @state == :already_handled %>
This device code has already been approved or denied. Start again from your tool if you need a new one.
<% else %>
We couldn't find that code. Check the code your tool is showing and try again.
<% end %>
</p>
<%= link_to "Enter a different code", device_verification_path, class: "mt-6 inline-block text-sm font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400" %>
</div>
<%= render "terminal_state", state: @state %>
<% end %>
</div>
</div>