Add OIDC fixes, add prefered_username, add application-user claims

This commit is contained in:
Dan Milne
2025-11-25 16:29:40 +11:00
parent 7796c38c08
commit d6029556d3
34 changed files with 1003 additions and 64 deletions

View File

@@ -0,0 +1,185 @@
<% oidc_apps = applications.select(&:oidc?) %>
<% forward_auth_apps = applications.select(&:forward_auth?) %>
<!-- OIDC Apps: Custom Claims -->
<% if oidc_apps.any? %>
<div class="mt-12 border-t pt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">OIDC App-Specific Claims</h2>
<p class="text-sm text-gray-600 mb-6">
Configure custom claims that apply only to specific OIDC applications. These override both group and user global claims and are included in ID tokens.
</p>
<div class="space-y-6">
<% oidc_apps.each do |app| %>
<% app_claim = user.application_user_claims.find_by(application: app) %>
<details class="border rounded-lg" <%= "open" if app_claim&.custom_claims&.any? %>>
<summary class="cursor-pointer bg-gray-50 px-4 py-3 hover:bg-gray-100 rounded-t-lg flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-medium text-gray-900"><%= app.name %></span>
<span class="text-xs px-2 py-1 rounded-full bg-blue-100 text-blue-700">
OIDC
</span>
<% if app_claim&.custom_claims&.any? %>
<span class="text-xs px-2 py-1 rounded-full bg-amber-100 text-amber-700">
<%= app_claim.custom_claims.keys.count %> claim(s)
</span>
<% end %>
</div>
<svg class="h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div class="p-4 space-y-4">
<%= form_with url: update_application_claims_admin_user_path(user), method: :post, class: "space-y-4", data: { controller: "json-validator" } do |form| %>
<%= hidden_field_tag :application_id, app.id %>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Custom Claims (JSON)</label>
<%= text_area_tag :custom_claims,
(app_claim&.custom_claims.present? ? JSON.pretty_generate(app_claim.custom_claims) : ""),
rows: 8,
class: "w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
placeholder: '{"kavita_groups": ["admin"], "library_access": "all"}',
data: {
action: "input->json-validator#validate blur->json-validator#format",
json_validator_target: "textarea"
} %>
<div class="mt-2 space-y-1">
<p class="text-xs text-gray-600">
Example for <%= app.name %>: Add claims that this app specifically needs to read.
</p>
<p class="text-xs text-amber-600">
<strong>Note:</strong> Do not use reserved claim names (<code class="bg-amber-50 px-1 rounded">groups</code>, <code class="bg-amber-50 px-1 rounded">email</code>, <code class="bg-amber-50 px-1 rounded">name</code>, etc.). Use app-specific names like <code class="bg-amber-50 px-1 rounded">kavita_groups</code> instead.
</p>
<div data-json-validator-target="status" class="text-xs font-medium"></div>
</div>
</div>
<div class="flex gap-3">
<%= button_tag type: :submit, class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500" do %>
<%= app_claim ? "Update" : "Add" %> Claims
<% end %>
<% if app_claim %>
<%= button_to "Remove Override",
delete_application_claims_admin_user_path(user, application_id: app.id),
method: :delete,
data: { turbo_confirm: "Remove app-specific claims for #{app.name}?" },
class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
<% end %>
</div>
<% end %>
<!-- Preview merged claims -->
<div class="mt-4 border-t pt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Preview: Final ID Token Claims for <%= app.name %></h4>
<div class="bg-gray-50 rounded-lg p-3">
<pre class="text-xs font-mono text-gray-800 overflow-x-auto"><%= JSON.pretty_generate(preview_user_claims(user, app)) %></pre>
</div>
<details class="mt-2">
<summary class="cursor-pointer text-xs text-gray-600 hover:text-gray-900">Show claim sources</summary>
<div class="mt-2 space-y-1">
<% claim_sources(user, app).each do |source| %>
<div class="flex gap-2 items-start text-xs">
<span class="px-2 py-1 rounded <%= source[:type] == :group ? 'bg-blue-100 text-blue-700' : (source[:type] == :user ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700') %>">
<%= source[:name] %>
</span>
<code class="text-gray-700"><%= source[:claims].to_json %></code>
</div>
<% end %>
</div>
</details>
</div>
</div>
</details>
<% end %>
</div>
</div>
<% end %>
<!-- ForwardAuth Apps: Headers Preview -->
<% if forward_auth_apps.any? %>
<div class="mt-12 border-t pt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">ForwardAuth Headers Preview</h2>
<p class="text-sm text-gray-600 mb-6">
ForwardAuth applications receive HTTP headers (not OIDC tokens). Headers are based on user's email, name, groups, and admin status.
</p>
<div class="space-y-6">
<% forward_auth_apps.each do |app| %>
<details class="border rounded-lg">
<summary class="cursor-pointer bg-gray-50 px-4 py-3 hover:bg-gray-100 rounded-t-lg flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="font-medium text-gray-900"><%= app.name %></span>
<span class="text-xs px-2 py-1 rounded-full bg-green-100 text-green-700">
FORWARD AUTH
</span>
<span class="text-xs text-gray-500">
<%= app.domain_pattern %>
</span>
</div>
<svg class="h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div class="p-4 space-y-4">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div class="flex items-start">
<svg class="h-5 w-5 text-blue-400 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<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>
</div>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-2">Headers Sent to <%= app.name %></h4>
<div class="bg-gray-50 rounded-lg p-3 border">
<% headers = app.headers_for_user(user) %>
<% if headers.any? %>
<dl class="space-y-2 text-xs font-mono">
<% headers.each do |header_name, value| %>
<div class="flex">
<dt class="text-blue-600 font-semibold w-48"><%= header_name %>:</dt>
<dd class="text-gray-800 flex-1"><%= value %></dd>
</div>
<% end %>
</dl>
<% else %>
<p class="text-xs text-gray-500 italic">All headers disabled for this application.</p>
<% end %>
</div>
<p class="mt-2 text-xs text-gray-500">
These headers are configured in the application settings and sent by your reverse proxy (Caddy/Traefik) to the upstream application.
</p>
</div>
<% if user.groups.any? %>
<div>
<h4 class="text-sm font-medium text-gray-700 mb-2">User's Groups</h4>
<div class="flex flex-wrap gap-2">
<% user.groups.each do |group| %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<%= group.name %>
</span>
<% end %>
</div>
</div>
<% end %>
</div>
</details>
<% end %>
</div>
</div>
<% end %>
<% if oidc_apps.empty? && forward_auth_apps.empty? %>
<div class="mt-12 border-t pt-8">
<div class="text-center py-12 bg-gray-50 rounded-lg">
<p class="text-gray-500">No active applications found.</p>
<p class="text-sm text-gray-400 mt-1">Create applications in the Admin panel first.</p>
</div>
</div>
<% end %>