2 Commits

Author SHA1 Message Date
Dan Milne
ae99d3d9cf Fix webauthn bug. Fix tests. Update docs
Some checks failed
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 15:24:56 +11:00
Dan Milne
1afcd041f9 Update README, fix a test 2026-01-01 15:17:28 +11:00
5 changed files with 18 additions and 7 deletions

View File

@@ -11,6 +11,8 @@ Clinch gives you one place to manage users and lets any web app authenticate aga
Do you host your own web apps? MeTube, Kavita, Audiobookshelf, Gitea, Grafana, Proxmox? Rather than managing all those separate user accounts, set everyone up on Clinch and let it do the authentication and user management. Do you host your own web apps? MeTube, Kavita, Audiobookshelf, Gitea, Grafana, Proxmox? Rather than managing all those separate user accounts, set everyone up on Clinch and let it do the authentication and user management.
Clinch runs as a single Docker container, using SQLite as the database, the job queue (Solid Queue) and the shared cache (Solid Cache). The webserver, Puma, runs the job queue in-process, avoiding the need for another container.
Clinch sits in a sweet spot between two excellent open-source identity solutions: Clinch sits in a sweet spot between two excellent open-source identity solutions:
**[Authelia](https://www.authelia.com)** is a fantastic choice for those who prefer external user management through LDAP and enjoy comprehensive YAML-based configuration. It's lightweight, secure, and works beautifully with reverse proxies. **[Authelia](https://www.authelia.com)** is a fantastic choice for those who prefer external user management through LDAP and enjoy comprehensive YAML-based configuration. It's lightweight, secure, and works beautifully with reverse proxies.

View File

@@ -1,6 +1,9 @@
class WebauthnCredential < ApplicationRecord class WebauthnCredential < ApplicationRecord
belongs_to :user belongs_to :user
# Set default authenticator_type if not provided
after_initialize :set_default_authenticator_type, if: :new_record?
# Validations # Validations
validates :external_id, presence: true, uniqueness: true validates :external_id, presence: true, uniqueness: true
validates :public_key, presence: true validates :public_key, presence: true
@@ -77,6 +80,10 @@ class WebauthnCredential < ApplicationRecord
private private
def set_default_authenticator_type
self.authenticator_type ||= "cross-platform"
end
def time_ago_in_words(time) def time_ago_in_words(time)
seconds = Time.current - time seconds = Time.current - time
minutes = seconds / 60 minutes = seconds / 60

View File

@@ -136,7 +136,7 @@ This checklist ensures Clinch meets security, quality, and documentation standar
- [ ] Document required vs. optional configuration - [ ] Document required vs. optional configuration
- [ ] Provide sensible defaults - [ ] Provide sensible defaults
- [ ] Validate production SMTP configuration - [ ] Validate production SMTP configuration
- [ ] Ensure OIDC private key generation process is documented - [x] Ensure OIDC private key generation process is documented
### Database ### Database
- [x] Migrations are idempotent - [x] Migrations are idempotent
@@ -187,7 +187,7 @@ This checklist ensures Clinch meets security, quality, and documentation standar
## Known Limitations & Risks ## Known Limitations & Risks
### Documented Risks ### Documented Risks
- [ ] Document that ForwardAuth requires same-domain setup - [x] Document that ForwardAuth requires same-domain setup
- [ ] Document HTTPS requirement for production - [ ] Document HTTPS requirement for production
- [ ] Document backup code security (single-use, store securely) - [ ] Document backup code security (single-use, store securely)
- [ ] Document admin password security requirements - [ ] Document admin password security requirements

View File

@@ -271,7 +271,7 @@ class ForwardAuthAdvancedTest < ActionDispatch::IntegrationTest
else else
# Should have no auth headers # Should have no auth headers
auth_headers = response.headers.select { |k, v| k.match?(/^(x-remote-|x-webauth-|x-admin-)/i) } auth_headers = response.headers.select { |k, v| k.match?(/^(x-remote-|x-webauth-|x-admin-)/i) }
assert_empty auth_headers, "Should have no headers for #{app[:domain]}, got: #{auth_headers.keys.join(', ')}" assert_empty auth_headers, "Should have no headers for #{app[:domain]}, got: #{auth_headers.keys.join(", ")}"
end end
end end
end end
@@ -348,5 +348,4 @@ class ForwardAuthAdvancedTest < ActionDispatch::IntegrationTest
rps = request_count / total_time rps = request_count / total_time
assert rps > 10, "Requests per second #{rps} is too low" assert rps > 10, "Requests per second #{rps} is too low"
end end
end end

View File

@@ -128,7 +128,10 @@ class WebauthnSecurityTest < ActionDispatch::IntegrationTest
nickname: "Test Key" nickname: "Test Key"
) )
# Sign in with WebAuthn # Sign in first
post signin_path, params: {email_address: user.email_address, password: "password123"}
# Get WebAuthn challenge
post webauthn_challenge_path, params: {email: "webauthn_verify_origin_test@example.com"} post webauthn_challenge_path, params: {email: "webauthn_verify_origin_test@example.com"}
assert_response :success assert_response :success
@@ -224,8 +227,8 @@ class WebauthnSecurityTest < ActionDispatch::IntegrationTest
) )
credential.reload credential.reload
assert_equal "192.168.1.100", credential.last_ip_address assert_equal "192.168.1.100", credential.last_used_ip
assert_equal "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", credential.last_user_agent assert_equal "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", credential.user_agent
user.destroy user.destroy
end end