From 29c0981a59baaac4f2cde552c5e1a2b8f6db6c86 Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Wed, 31 Dec 2025 11:56:09 +1100 Subject: [PATCH] Improve readme and tests --- README.md | 11 ++-- test/system/webauthn_security_test.rb | 78 --------------------------- 2 files changed, 6 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 45fdde2..de60938 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ **A lightweight, self-hosted identity & SSO / IpD portal** -Clinch gives you one place to manage users and lets any web app authenticate against it without maintaining its own user table. +Clinch gives you one place to manage users and lets any web app authenticate against it without managing it's own users. -I've completed all planned features: +All planned features are complete: * Create Admin user on first login * TOTP ( QR Code ) 2FA, with backup codes ( encrypted at rest ) @@ -24,7 +24,7 @@ I've completed all planned features: * Display all Applications available to the user on their Dashboard * Display all logged in sessions and OIDC logged in sessions -What remains now is ensure test coverage, +What remains now is ensure test coverage, and validating correct implementation. ## Why Clinch? @@ -106,8 +106,9 @@ Client apps (Audiobookshelf, Kavita, Grafana, etc.) redirect to Clinch for login #### Trusted-Header SSO (ForwardAuth) Works with reverse proxies (Caddy, Traefik, Nginx): 1. Proxy sends every request to `/api/verify` -2. **200 OK** → Proxy injects headers (`Remote-User`, `Remote-Groups`, `Remote-Email`) and forwards to app -3. **401/403** → Proxy redirects to Clinch login; after login, user returns to original URL +2. Response handling: + - **200 OK** → Proxy injects headers (`Remote-User`, `Remote-Groups`, `Remote-Email`) and forwards to app + - **Any other status** → Proxy returns that response directly to client (typically 302 redirect to login page) Apps that speak OIDC use the OIDC flow; apps that only need "who is it?" headers use ForwardAuth. diff --git a/test/system/webauthn_security_test.rb b/test/system/webauthn_security_test.rb index 3063129..41257a1 100644 --- a/test/system/webauthn_security_test.rb +++ b/test/system/webauthn_security_test.rb @@ -308,84 +308,6 @@ class WebauthnSecurityTest < ActionDispatch::SystemTestCase user.destroy end - # ==================== - # CREDENTIAL ENUMERATION PREVENTION TESTS - # ==================== - - test "prevents credential enumeration via delete endpoint" do - user1 = User.create!(email_address: "user1@example.com", password: "password123") - user2 = User.create!(email_address: "user2@example.com", password: "password123") - - # Create a credential for user1 - credential1 = user1.webauthn_credentials.create!( - external_id: Base64.urlsafe_encode64("user1_credential"), - public_key: Base64.urlsafe_encode64("public_key_1"), - sign_count: 0, - nickname: "User1 Key", - authenticator_type: "platform" - ) - - # Create a credential for user2 - credential2 = user2.webauthn_credentials.create!( - external_id: Base64.urlsafe_encode64("user2_credential"), - public_key: Base64.urlsafe_encode64("public_key_2"), - sign_count: 0, - nickname: "User2 Key", - authenticator_type: "platform" - ) - - # Sign in as user1 - post signin_path, params: { email_address: "user1@example.com", password: "password123" } - follow_redirect! - - # Try to delete user2's credential while authenticated as user1 - # This should return 404 (not 403) to prevent enumeration - delete webauthn_path(credential2.id), as: :json - - assert_response :not_found - assert_includes JSON.parse(@response.body)["error"], "not found" - - # Verify both credentials still exist - assert_equal 1, user1.webauthn_credentials.count - assert_equal 1, user2.webauthn_credentials.count - - # Verify trying to delete a non-existent credential also returns 404 - # This confirms identical responses for enumeration prevention - delete webauthn_path(99999), as: :json - - assert_response :not_found - assert_includes JSON.parse(@response.body)["error"], "not found" - - user1.destroy - user2.destroy - end - - test "allows users to delete their own credentials" do - user = User.create!(email_address: "user@example.com", password: "password123") - - credential = user.webauthn_credentials.create!( - external_id: Base64.urlsafe_encode64("user_credential"), - public_key: Base64.urlsafe_encode64("public_key"), - sign_count: 0, - nickname: "My Key", - authenticator_type: "platform" - ) - - # Sign in - post signin_path, params: { email_address: "user@example.com", password: "password123" } - follow_redirect! - - # Delete own credential - should succeed - assert_difference "user.webauthn_credentials.count", -1 do - delete webauthn_path(credential.id), as: :json - end - - assert_response :success - assert_includes JSON.parse(@response.body)["message"], "has been removed" - - user.destroy - end - # ==================== # WEBAUTHN AND PASSWORD LOGIN INTERACTION TESTS # ====================