Commit Graph

209 Commits

Author SHA1 Message Date
Dan Milne
7d352654fd Fix broken password reset email templates
Some checks failed
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
Both templates called `@user.password_reset_token` and
`@user.password_reset_token_expires_in`, which don't exist —
`generates_token_for` only adds class-level helpers, not instance
accessors. Every password reset email was failing at render time.
Use `generate_token_for(:password_reset)` and a literal expiry string
matching the 1-hour TTL on the token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:40:43 +10:00
Dan Milne
e39721c7e6 Fix broken invitation email text template
Some checks failed
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 text part used non-existent helpers (`invite_url`,
`@user.invitation_login_token`) and Ruby string interpolation in an ERB
file, so multipart delivery failed at render time and no invite mail
went out. Mirror the HTML template instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:39:29 +10:00
Dan Milne
5178cf3d81 Drop redundant MemoryStore internals peek from fa_token creation test
The refute_match on response.location already proves create_forward_auth_token
did nothing: the cache.write and the URL rewrite are back-to-back with no
branch between them, so the URL lacking fa_token= implies no cache entry
was written. The instance_variable_get(:@data) inspection was both redundant
and coupled to MemoryStore's private layout.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 20:28:28 +10:00
Dan Milne
2d5650e620 Bind forward-auth fa_token to its destination host
An observed fa_token (via Referer leaks, access logs, JS monitors)
could previously be redeemed against a different reverse-proxied app
within the 60s TTL. The token now stores the destination host at
creation and the verifier rejects mismatches without burning the cache
entry, so legitimate destinations can still redeem.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 19:04:53 +10:00
Dan Milne
7f0d3d3900 Tighten TOTP enrollment comments to explain the threat, not the change
Replace the changelog-flavored "view no longer round-trips" line with a
one-liner naming the actual threat (session-holder substituting a secret
they control). Drop the narration comment above session.delete +
deliver_later -- the identifiers already say what the two lines do.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 18:58:39 +10:00
Dan Milne
b876e02c3a Hold TOTP enrollment secret server-side and email user on activation
TOTP enrollment previously round-tripped the generated secret through a
hidden form field and saved whatever the client submitted, letting an
attacker with session access enroll a 2FA device they control by posting
their own secret plus a matching code. Stash the secret in the session
at GET /totp/new, read it only from the session at POST /totp, and drop
the hidden field from the view. Notify the user by email on successful
enrollment so unauthorized activations are visible even if a new vector
appears later.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 18:17:50 +10:00
Dan Milne
93d8381214 Nullify token to auth-code FK on delete so cleanup job can purge codes
The FK added in b7fa499 defaulted to ON DELETE RESTRICT, which means
OidcTokenCleanupJob#perform would fail when deleting auth codes older
than 7 days if any refresh token (whose expiry is days-to-weeks) still
referenced them. Switch both token FKs to ON DELETE SET NULL so token
rows survive the code deletion with a NULL FK, preserving the audit
trail the cleanup job deliberately keeps.

Add a regression test covering the exact scenario: a 10-day-old auth
code with a token still pointing at it -> cleanup deletes the code,
token survives, token FK is nulled.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 18:12:03 +10:00
Dan Milne
2068675173 Collapse auth-code replay revocation to two update_all queries
The previous implementation iterated find_each(&:revoke!) on both the
access-token and refresh-token associations. OidcAccessToken#revoke!
also cascades to its refresh tokens, so a chain of N access tokens with
their refresh tokens produced ~3N UPDATEs (outer loop + cascade +
outer refresh loop double-writing) all while holding a pessimistic
lock on the auth_code row. Replace with scoped update_all on each
association -- 2 UPDATEs total, no behavior change.

Also hoist the repeated refresh_token_record.oidc_authorization_code
lookup in the rotation path to a named local and drop the duplicated
inline comment.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 18:11:54 +10:00
Dan Milne
b7fa49953c Revoke full token chain on OIDC authorization-code replay
The replay handler previously used a created_at time-range filter to
target access tokens and called update_all(expires_at:), which left
revoked_at nil, skipped refresh tokens entirely, and could miss or
falsely catch tokens from concurrent flows. Add an oidc_authorization_code
FK on both token tables, carry it through refresh-token rotation, and
use the association to revoke every descendant via revoke! (which sets
revoked_at and cascades access -> refresh).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 17:39:08 +10:00
Dan Milne
b7dd3c02e7 Extract client_id and redirect_uri validation into before_actions
The authorize action opened with ~55 lines of parameter validation that
ran before any business logic. Move the two RFC 6749 §4.1.2.1 checks
(client_id lookup, redirect_uri registration) into set_application and
validate_redirect_uri before_actions. The action body now starts at the
point where errors may legitimately redirect back to the client.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 17:29:36 +10:00
Dan Milne
17a464fd15 Fix OIDC claims validation against undefined scopes variable
The authorize action called validate_claims_against_scopes with
requested_scopes before that local was assigned (assignment was ~100
lines later), raising NameError whenever a client passed a claims=
parameter. Move the scope normalization above the claims validation.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 17:26:46 +10:00
Dan Milne
9197524c88 Add remember me checkbox, center and narrow sign-in form
- Add "Remember me for 30 days" checkbox (30-day vs 24-hour session expiry)
- Center heading and constrain form width to max-w-md
- Preserve remember_me preference through TOTP and WebAuthn auth flows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:22:51 +10:00
Dan Milne
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 Milne
c7d9df48b5 Remove auto-trigger of passkey authentication on page load
Some checks failed
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
Safari's WebAuthn dialog can become undismissable when invoked without
a user gesture. Always require the user to click "Continue with Passkey"
instead of auto-triggering navigator.credentials.get().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:38:48 +11:00
Dan Milne
3d98261a51 Add dark mode with toggle and localStorage persistence
Uses Tailwind v4 class-based dark mode with a Stimulus controller for
toggling. Respects prefers-color-scheme as default, prevents FOUC with
an inline script, and persists the user's choice in localStorage. All
views updated with dark: variants for backgrounds, text, borders,
badges, buttons, and form inputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:37:58 +11:00
Dan Milne
43958f50ce Add @tailwindcss/forms plugin and improve application form UX
Install @tailwindcss/forms to fix missing padding on form inputs across
the app. Move the Application Type selector earlier in the new application
form (after slug, before description) so it gates type-specific fields
sooner. On the edit page, replace the confusing disabled dropdown with a
read-only badge since the type can't be changed after creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 00:36:37 +11:00
Dan Milne
d8d8000b92 Add tests for forward auth cache gaps: invalidation, rate limiting, and debounce
- Test ApplicationGroup cache busting on add and remove
- Test first failure persists in rate limit cache (increment fallback)
- Test bearer token failures count toward rate limit
- Test rd parameter rejected for deactivated applications
- Test last_activity_at updates after debounce window expires
- Test successful requests don't reset failure counter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:59:18 +11:00
Dan Milne
6844c5fab3 Clean up forward auth caching: remove duplication, fix rate limiting, and plug cache gaps
- Remove duplicated app_allows_user_cached?/headers_for_user_cached methods; call model methods directly
- Fix sliding-window rate limit bug by using increment instead of write (avoids TTL reset)
- Use cached app lookup in validate_redirect_url instead of hitting DB on every unauthorized request
- Add cache busting to ApplicationGroup so group assignment changes invalidate the cache
- Eager-load user groups (includes(user: :groups)) to eliminate N+1 queries
- Replace pluck(:name) with map(&:name) to use already-loaded associations
- Remove hardcoded fallback domain, dead methods, and unnecessary comments
- Fix test indentation and make group-order assertions deterministic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:54:19 +11:00
Dan Milne
5505f99287 Add rate limiting and in-memory caching for forward auth endpoint
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>
2026-03-07 11:15:54 +11:00
Dan Milne
1b691ad341 Bump Rails from 8.1.1 to 8.1.2
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:11:13 +11:00
Dan Milne
f65df76d99 Show user-friendly error when passkey authentication fails
Add error target to login page so WebAuthn errors are visible instead
of only appearing in the console. Use a helpful fallback message that
suggests a browser extension may be interfering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:11:43 +11:00
Dan Milne
c5898bd9a4 Add passkey option on TOTP page and auto-trigger passkey for TOTP users
When a user has both passkeys and TOTP configured, auto-trigger the
passkey flow on login to save them from the password→TOTP path. Also
add a "Use Passkey Instead" button on the TOTP verification page as
an escape hatch for users who end up there.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:09:01 +11:00
Dan Milne
9dbde8ea31 Fix README: don't claim OIDC certification, just conformance
Some checks failed
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
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:39:10 +11:00
Dan Milne
191a7b5fb3 Update README: add API keys docs, VoidAuth, highlight conformance
Some checks failed
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
- Document API keys / bearer tokens for forward auth
- Add VoidAuth to alternatives list
- Move OIDC conformance certification and test counts to top
- Update Ruby requirement to 4.0+, test count to 450

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 22:36:12 +11:00
Dan Milne
7a9348c1f1 Add voidauth to the list of alternatives
Some checks failed
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-03-05 22:30:08 +11:00
Dan Milne
225d8ae5ca Update the README
Some checks failed
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-03-05 22:27:24 +11:00
Dan Milne
65c19fa732 Upgrade to Ruby 4.0.1, bump version to 0.9.0
Some checks failed
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
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>
v0.9.0
2026-03-05 21:52:11 +11:00
Dan Milne
fd8785a43d Add API keys / bearer tokens for forward auth
Some checks failed
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>
v0.8.8
2026-03-05 21:45:40 +11:00
Dan Milne
444ae6291c Add missing files, fix formatting
Some checks failed
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:34:11 +11:00
Dan Milne
233fb723d5 More accurate language around passing the OpenID Conformance tests
Some checks failed
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:32:34 +11:00
Dan Milne
cc6d4fcc65 Add test files, update checklist
Some checks failed
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
5268f10eb3 Don't allow claim escalation
Some checks failed
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 16:40:11 +11:00
Dan Milne
5c5662eaab Expose 'username' via forward auth headers 2026-01-05 15:12:24 +11:00
Dan Milne
27d77ebf47 Expose 'username' via forward auth headers 2026-01-05 15:12:02 +11:00
Dan Milne
ba08158c85 Bug fix for background jobs 2026-01-05 14:43:06 +11:00
Dan Milne
a6480b0860 Verion Bump
Some checks failed
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 13:08:22 +11:00
Dan Milne
75cc223329 303 is the correct response 2026-01-05 13:05:24 +11:00
Dan Milne
46ae65f4d2 Move the 'remove_query_param' to the application controller 2026-01-05 13:03:03 +11:00
Dan Milne
95d0d844e9 Add a method to remove parameters from urls, so we can redirect without risk of infinite redirect. Fix a bunch of redirects to login afer being foced to log out. Add missing migrations 2026-01-05 13:01:32 +11:00
Dan Milne
524a7719c3 Merge branch 'main' into feature/claims 2026-01-05 12:11:53 +11:00
Dan Milne
8110d547dd Fix bug with session deletion when logout forced and we have a redirect to follow 2026-01-05 12:11:52 +11:00
Dan Milne
25e1043312 Add skip-consent, correctly use 303, rather than 302, actually rename per app 'logout' to 'require re-auth'. Add helper methods for token lifetime - allowing 10d for 10days for example. 2026-01-05 12:03:01 +11:00
Dan Milne
074a734c0c Accidentally added skip-consent to this branch 2026-01-05 12:01:04 +11:00
Dan Milne
4a48012a82 Add claims support 2026-01-05 12:00:29 +11:00
Dan Milne
e631f606e7 Better error messages
Some checks failed
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
0.8.6
2026-01-03 12:29:27 +11:00
Dan Milne
f4a697ae9b More OpenID Conformance test fixes - work with POST, correct auth code character set, correct no-store cache headers 2026-01-03 12:28:43 +11:00
Dan Milne
16e34ffaf0 Updates for oidc conformance 2026-01-03 10:11:10 +11:00
Dan Milne
0bb84f08d6 OpenID conformance test: we get a warning for not having a value for every claim. But we can explictly list support claims. Nothing we can do about a warning in the complience.
Some checks failed
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
2026-01-02 16:35:12 +11:00
Dan Milne
182682024d OpenID Conformance: Include all required scopes when profile is requested, even if they're empty
Some checks failed
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-02 15:47:40 +11:00
Dan Milne
b517ebe809 OpenID conformance test: Allow posting the access token in the body for userinfo endpoint
Some checks failed
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-02 15:41:07 +11:00