Commit Graph
36 Commits
Author SHA1 Message Date
Dan MilneandClaude Opus 4.8 45a1203647 Fix 500 when deleting an application with device codes
Deleting an application left its oidc_device_codes orphaned, tripping the
FK constraint and raising SQLite3::ConstraintException. Unlike the other
child tables, oidc_device_codes had neither a dependent: :destroy
association nor an on_delete: :cascade FK, so any app that had started a
device-authorization flow could not be deleted (regardless of groups/users,
since a pending device code has a null user_id).

Add the missing has_many :oidc_device_codes, dependent: :destroy and a
migration to cascade at the DB level, mirroring application_user_claims.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LARKs4cVsGrvKCkKZhPcy6
2026-07-19 20:39:33 +10:00
Dan MilneandClaude Opus 4.8 8b146db54e Device code: retry user_code generation on collision
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
2026-07-19 14:43:03 +10:00
Dan MilneandClaude Opus 4.8 e3f0bd4cab Extract OidcUserConsent.record! upsert for shared consent
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
2026-07-19 13:59:44 +10:00
Dan MilneandClaude Opus 4.8 7149b98b7b Add OAuth device flow, token introspection, and dynamic client registration
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
2026-07-19 12:30:02 +10:00
Dan MilneandClaude Fable 5 44892e3301 Make WebAuthn clone detection actually block, and fix false positives
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>
2026-06-11 20:28:38 +10:00
Dan MilneandClaude Fable 5 89bd5f1432 Enforce account-active status across the auth lifecycle
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>
2026-06-11 19:53:50 +10:00
Dan MilneandClaude Fable 5 406a79d9eb Block SSRF via backchannel_logout_uri
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>
2026-06-11 08:14:45 +10:00
Dan MilneandClaude Opus 4.7 bfad9c4e9d Generated monogram fallback + optional dark-mode icon per application
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
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>
2026-06-07 17:02:53 +10:00
Dan MilneandClaude Opus 4.7 5b41db2c6a Fix FileNotFoundError when uploading an SVG icon
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
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>
2026-06-07 16:43:24 +10:00
Dan MilneandClaude Opus 4.7 03dfdbd83a Default-deny access control with group flags and access enumeration
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
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>
2026-06-07 15:53:27 +10:00
Dan MilneandClaude Opus 4.7 2e427a0520 Add SvgScrubber to strip XSS payloads from uploaded app icons
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>
2026-05-02 23:57:22 +10:00
Dan MilneandClaude Opus 4.6 2235924f37 Harden OIDC, add SVG sanitization, improve form UX and security defaults
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>
2026-04-06 21:06:51 +10:00
Dan MilneandClaude Opus 4.6 fd8785a43d Add API keys / bearer tokens for forward auth
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
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>
2026-03-05 21:45:40 +11:00
Dan Milne cc6d4fcc65 Add test files, update checklist
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / scan_container (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-01-05 23:28:55 +11:00
Dan Milne d597ca8810 Fix tests 2026-01-01 14:52:24 +11:00
Dan Milne 93a0edb0a2 StandardRB fixes
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2026-01-01 13:29:44 +11:00
Dan Milne 4b4afe277e Include auth_time in ID token. Switch from upsert -> find_and_create_by so we actually get sid values for consent on the creation of the record 2025-12-31 16:36:32 +11:00
Dan Milne 364e6e21dd Fixes for tests and AR Encryption
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-12-31 16:08:05 +11:00
Dan Milne 71d59e7367 Remove plain text token from everywhere
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-12-30 11:58:11 +11:00
Dan Milne 0761c424c1 Fix tests. Remove tests which test rails functionality
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-12-30 00:18:19 +11:00
Dan Milne d6029556d3 Add OIDC fixes, add prefered_username, add application-user claims 2025-11-25 16:29:40 +11:00
Dan Milne 038801f34b Add pkce
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-11-09 10:21:29 +11:00
Dan Milne fb14ce032f Strip out more inline javascript code. Encrypt backup codes and treat the backup codes attribute as a json array 2025-11-04 18:46:11 +11:00
Dan Milne ef15db77f9 Massive refactor. Merge forward_auth into App, remove references to unimplemented OIDC federation and SAML features. Add group and user custom claims. Groups now allocate which apps a user can use
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-11-04 13:21:55 +11:00
Dan Milne cfab21b130 More tests 2025-10-28 08:27:19 +11:00
Dan Milne 8dd3e60071 Add a list_sign_in_at field for users so magick links work 2025-10-26 22:40:54 +11:00
Dan Milne e4e7a0873e Fixes 2025-10-26 22:03:03 +11:00
Dan Milne b5b1d94d47 Fix the CLINCH_HOST issue. 2025-10-26 21:59:27 +11:00
Dan Milne 52cfd6122c Typo. More tests 2025-10-26 20:42:18 +11:00
Dan Milne 227e29ce0a Fix/add some tests. Configure email sending address 2025-10-26 20:13:39 +11:00
Dan MilneandClaude d98f777e7d Refactor email delivery and background jobs system
- 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>
2025-10-26 16:30:02 +11:00
Dan Milne 0af3dbefed Remember that we concented.
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-10-24 17:01:03 +11:00
Dan Milne 12e0ef66ed OIDC app creation with encrypted secrets and application roles
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-10-24 14:47:24 +11:00
Dan Milne fc9afcd1b7 Separate Forward auth into it's own models + controller
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-10-24 10:56:27 +11:00
Dan Milne 56f7dd7b3c First crack
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-10-23 16:45:00 +11:00
Dan Milne 1ff0a95392 First commit
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
2025-10-23 16:19:56 +11:00