Regenerate the user_code when a freshly generated one collides with an existing
code (up to USER_CODE_MAX_ATTEMPTS), so a client never gets a uniqueness error
just because two codes happened to match; the DB unique index stays the final
guard against a concurrent-insert race.
Replace the model test that asserted Rails' uniqueness validator message
("has already been taken") — per CLAUDE.md we don't test framework behavior —
with one that exercises the app-specific property: generation retries past a
collision and yields a unique code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
The consent record is unique on user+application and shared between the browser
authorization flow and device-flow approval. Centralize the upsert in
OidcUserConsent.record!(merge:) so a narrower device request unions its scopes
into any existing grant (merge: true) rather than overwriting it and wiping
stored claims, while the browser consent screen records exactly the approved
scopes (merge: false). scopes is now nil-safe for not-yet-saved records.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
Adds three OAuth surfaces to the OIDC provider so CLIs, terminal agents, and
MCP connectors can authenticate as real users instead of using static API keys.
Device Authorization Grant (RFC 8628):
- OidcDeviceCode model (HMAC device_code, short plaintext user_code, nullable
user until approval, slow_down polling state), mirroring OidcAuthorizationCode
- POST /oauth/device_authorization issues the code pair + verification URIs
- device_code grant on /oauth/token returns authorization_pending / slow_down /
access_denied / expired_token, then the standard token triple; single-use
- Authenticated /device approval page, gated by Application#user_allowed?
Token Introspection (RFC 7662):
- POST /oauth/introspect: confidential-caller-authenticated; returns active,
scope, and the user's groups so resource servers can authorize on membership
Dynamic Client Registration (RFC 7591):
- POST /oauth/register creates public/confidential clients (PKCE required)
- Runtime toggle via a new Setting store + admin switch on the Applications page;
off by default, env var CLINCH_DCR_ENABLED as bootstrap fallback
- New clients are default-deny (no allowed_groups) until an admin grants access
- RFC 8414 metadata alias at /.well-known/oauth-authorization-server;
registration_endpoint advertised only while the window is open
Discovery advertises all three grants/endpoints. Seeds add a clinch-cli public
client and a c2a2-introspection confidential client. ADRs in docs/decisions
record the opaque-vs-JWT, device-flow, and DCR-security decisions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7cwhwDJp3MJJDoNPVE6zq
Two problems with sign-count clone detection:
- suspicious_sign_count? flagged the case where both the stored and presented
counts are 0. Most synced passkeys (Apple/Google) report 0 every time, so every
legitimate sign-in was flagged — drowning real signals in noise. Per WebAuthn
§6.1.1 a 0 counter means "no counter"; only flag when BOTH counts are non-zero
and the new one does not advance.
- On a suspicious count the controller only logged a warning and then continued
to authenticate and overwrite the stored counter. A cloned credential therefore
worked indefinitely. webauthn_verify now rejects the sign-in (no session, no
counter update) and emails the user via a new SecurityMailer#suspicious_passkey_used.
Tests cover the corrected classification (synced/first-use/normal vs equal/
decreasing) and the new alert email.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
active? was only checked at the password step of sign-in. A user disabled
afterwards could (a) still complete the 2FA step and mint a valid session, and
(b) keep using any existing session until natural expiry, because per-request
auth only checked session expiry, not user status.
Three enforcement points:
- Mid-flow guard: verify_totp and webauthn_verify re-check active? before
start_new_session_for, clearing the pending session and rejecting if disabled.
- Request-time guard: find_session_by_cookie now uses Session.for_active_user,
so a session whose user is disabled no longer authenticates (authoritative,
catches any disable path including direct DB changes).
- Immediate cleanup: User#revoke_sessions_when_deactivated destroys a user's
sessions when status changes away from active, so access is revoked everywhere
at once rather than on the next request.
Tests cover the mid-flow TOTP rejection, request-time rejection of an existing
session after disable, session destruction on disable, and that unrelated
updates leave sessions intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
backchannel_logout_uri was validated only for scheme/HTTPS, so an admin (or a
compromised admin account) could point it at internal infrastructure — cloud
metadata (169.254.169.254), loopback, or RFC1918 hosts — and every user logout
would fire a server-side POST there.
Add PrivateAddressCheck (app/lib) and apply it as defense-in-depth:
- Application validation rejects URIs whose host is, or is a literal, internal
address (loopback / private / link-local / 0.0.0.0 / localhost / metadata
hostnames). Fast, DNS-free, immediate admin feedback.
- BackchannelLogoutJob re-checks at request time WITH DNS resolution and aborts
(no retry) if the host resolves to a non-public address — covering URIs that
predate the validation and public hostnames pointed at internal IPs.
Tests cover the address classification, the model validation, and updates an
existing test that used a localhost logout URI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Application#sanitize_svg_icon already runs a Loofah scrubber on every
icon upload, but the scrubber class itself was never tracked. Land it
along with tests covering the four shapes that matter:
- <script> elements stripped entirely
- on* event handlers (onload, onclick, …) removed but the carrying
element preserved
- attribute values pointing at javascript:/data: URIs rejected
- benign icons round-trip unchanged
Writing the benign-icon test caught a real bug: the attribute allowlist
holds canonical SVG case (viewBox, preserveAspectRatio, gradientUnits,
…) but safe_attribute? downcases the incoming name before comparing,
so legitimate icons were silently losing those attributes on upload.
Fix by comparing against a precomputed lowercase lookup set; the
constant stays readable as canonical SVG case for documentation.
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>
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>
- Switch from SolidQueue to async job processor for simpler background job handling
- Remove SolidQueue gem and related configuration files
- Add letter_opener gem for development email preview
- Fix invitation email template issues (invitation_login_token method and route helper)
- Configure SMTP settings via environment variables in application.rb
- Add email delivery configuration banner on admin users page
- Improve admin users page with inline action buttons and SMTP configuration warnings
- Update development and production environments to use async processor
- Add helper methods to detect SMTP configuration and filter out localhost settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>