Files
clinch/app/views/oidc/consent.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

48 lines
2.6 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">
<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 Application</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<strong><%= @application.name %></strong> is requesting access to your account.
</p>
</div>
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">This application will be able to:</h3>
<%= render "shared/scope_list", scopes: @scopes %>
</div>
<div class="rounded-md bg-blue-50 dark:bg-blue-900/30 p-4 mb-6">
<div class="flex">
<svg class="h-5 w-5 text-blue-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>
<div class="text-sm text-blue-700 dark:text-blue-300">
<p>You'll be redirected to:</p>
<p class="mt-1 font-mono text-xs break-all"><%= @redirect_uri %></p>
</div>
</div>
</div>
<%= form_with url: "/oauth/authorize/consent", method: :post, class: "space-y-3", data: { turbo: false }, local: true do |form| %>
<%= form.submit "Authorize",
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 %>
</div>
</div>