determine_base_url fell back to request.host when CLINCH_HOST was unset. Rails
resolves request.host from X-Forwarded-Host behind a trusted proxy, so a spoofed
header could make the forward-auth login redirect point at an attacker origin
(host-header phishing).
- Add config/initializers/clinch_host.rb: fail fast at boot in any non-local
environment when CLINCH_HOST is blank. It anchors the OIDC issuer, WebAuthn
RP ID, and login redirect, so it must be explicit, never inferred.
- determine_base_url now uses CLINCH_HOST (guaranteed in production) with a safe
localhost default for dev/test, and never reads the request host.
- Simplify the spoofed-host regression test now that the fallback is safe.
Verified: production boot aborts with a clear message when CLINCH_HOST is blank,
and boots normally when set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Applications index used to render "All users" whenever an app had
no allowed_groups; under default-deny that's the opposite of the truth.
Replaced with a "No one" badge and, when groups are present, a
"N users · M groups" cell so the access reality is visible at a glance.
Added a small stats strip above the apps table: applications, users
with access, and groups granting access. Backed by preloaded counts in
the controller to avoid N+1.
Added /admin/access — a small "Access check" tool that takes a user
and an application and reports whether the user can reach it, with the
granting group(s) when allowed, and the specific reason when not
(inactive app/user, no allowed groups, or no shared group). Wired into
the admin sidebar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The admin users index only exposed Edit / Delete actions per row, so
the Accessible-applications panel on the user show page was unreachable
without typing the URL by hand. Adds a View action and turns the email
into a link to the show page — mirroring how the applications and
groups indexes already work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous <picture media="(prefers-color-scheme: dark)"> only fires
when the OS is in dark mode. Clinch toggles dark mode with a class on
<html> via dark_mode_controller and Tailwind's @custom-variant dark
(&:where(.dark, .dark *)), so the picture source never swapped when
users clicked the in-app theme toggle. Render both <img> tags and
use Tailwind's dark:hidden / hidden dark:block so the swap follows
whatever strategy Tailwind is configured for.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the icon dropzone into a reusable partial so the dark mode
icon gets the same upload / drag-and-drop / paste affordances as the
light icon. Slims the dropzone to a single-row layout (small cloud
icon plus Upload / drag-and-drop / paste hint) and a tiny format hint
below, instead of the previous tall vertically-centred block.
When an application has no icon attached, render a deterministic
monogram SVG instead of the generic picture-frame placeholder. Initials
are picked from capital letters in the name (ShelfLife -> SL); fall
back to the first two letters when fewer than two capitals exist
(Audiobookshelf -> AU). Background colour is hashed from the name for
stable per-app identity across visits.
Adds an optional second icon attachment, icon_dark, alongside the main
icon. When present, render a <picture> with a prefers-color-scheme:
dark source so the browser swaps automatically; when absent, the main
icon is used in both modes. The SVG sanitization, content-type fix,
and size/format validation now run over both attachments uniformly.
Bumps to 0.14.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sanitize_svg_icon before_validation callback called icon.download,
but Active Storage uploads pending blobs in before_save — so at
before_validation time the file only existed in the request tempfile,
not at the configured storage path. Read from the pending attachable
(UploadedFile / IO hash) instead. Guards against the recursive callback
that icon.attach would otherwise trigger by tracking the cleaned
attachable by object identity. Bumps to 0.13.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the implicit "empty allowed_groups means public" rule with
explicit default-deny across both OIDC and ForwardAuth. Adds two boolean
flags on Group — auto_assign (Keycloak-style auto-join on user create)
and admin (members can reach the admin panel) — and drops the
users.admin column entirely. Adds "Users with access" and "Accessible
applications" panels with via-group badges on the application/user show
pages.
BEHAVIOR CHANGE: a ForwardAuth app with no allowed_groups previously
bypassed authentication entirely; it now returns 403 like any other
unauthorized request. The data migration seeds an "everyone" group and
attaches it to all previously group-less apps to preserve behavior on
existing installs. An "admins" group is seeded and backfilled from any
user with the old admin column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch release covering the Ruby 4.0.3, Rails 8.1.3, and transitive
gem updates landed since 0.10.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substantial scope since 0.9.0: API keys for forward auth, SecurityMailer
alerts on 8 account-security events, dark mode, Remember-me with proper
browser-session cookie semantics, SvgScrubber for icon XSS, OIDC
auth-code replay revocation, forward-auth caching + rate limiting, and
fixes for broken invitation / password-reset emails.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove PKCE plain method support (S256 only), enforce openid scope requirement,
filter to supported scopes, strip reserved claims from custom claims as
defense-in-depth, sanitize SVG icons with Loofah, add global input padding,
switch session cookies to SameSite=Lax, use Session.active scope, and remove
unsafe-eval from CSP.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rate limit failed attempts (50/min per IP) with 429 + Retry-After.
Cache forward auth applications in a dedicated MemoryStore (8MB LRU)
to avoid loading all apps from SQLite on every request. Debounce
last_activity_at writes to at most once per minute per session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace CGI.parse (removed in Ruby 4.0) with Rack::Utils.parse_query
in application controller, sessions controller, and OIDC tests.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enables server-to-server authentication for forward auth applications
(e.g., video players accessing WebDAV) where browser cookies aren't
available. API keys use clk_ prefixed tokens stored as HMAC hashes.
Bearer token auth is checked before cookie auth in /api/verify.
Invalid tokens return 401 JSON (no redirect). Requests without
bearer tokens fall through to existing cookie flow unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>