Files
clinch/app/views/device_authorizations/show.html.erb
T
Dan MilneandClaude Opus 4.8 f65f8da2e7 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
2026-07-19 14:14:44 +10:00

65 lines
3.8 KiB
ERB

<div class="mx-auto max-w-md">
<div class="bg-white dark:bg-gray-800 py-8 px-6 shadow rounded-lg sm:px-10">
<% case @state %>
<% when :confirm %>
<div class="mb-8 text-center">
<% if @application.icon.attached? %>
<div class="mx-auto h-20 w-20 mb-4">
<%= app_icon_picture @application, class: "mx-auto h-20 w-20 rounded-xl object-cover border-2 border-gray-200 dark:border-gray-700 shadow-sm" %>
</div>
<% else %>
<div class="mx-auto mb-4">
<%= render "shared/app_monogram", name: @application.name, class: "h-20 w-20 rounded-xl shadow-sm" %>
</div>
<% end %>
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Authorize device</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<strong><%= @application.name %></strong> is requesting access to your account from a device or command line.
</p>
</div>
<div class="rounded-md bg-gray-50 dark:bg-gray-900/40 p-4 mb-6 text-center">
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Code shown on your device</p>
<p class="mt-1 font-mono text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100"><%= @device_code.user_code %></p>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Only approve if this matches the code your tool is showing.</p>
</div>
<% 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>
<%= render "shared/scope_list", scopes: @scopes %>
</div>
<% end %>
<%= form_with url: device_verification_path, method: :post, class: "space-y-3", data: { turbo: false }, local: true do |form| %>
<%= form.hidden_field :user_code, value: @device_code.user_code %>
<%= form.submit "Approve",
class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500" %>
<%= button_tag "Deny",
type: :submit,
name: :deny,
value: "1",
class: "w-full flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500" %>
<% end %>
<% when :prompt %>
<div class="mb-6 text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Enter device code</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Type the code shown by your tool or command line.</p>
</div>
<%= form_with url: device_verification_path, method: :get, class: "space-y-4", data: { turbo: false }, local: true do |form| %>
<%= form.text_field :user_code,
autofocus: true,
autocomplete: "off",
placeholder: "WDJB-MJHT",
class: "block w-full text-center font-mono text-xl tracking-widest uppercase rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500" %>
<%= form.submit "Continue",
class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500" %>
<% end %>
<% else %>
<%= render "terminal_state", state: @state %>
<% end %>
</div>
</div>