Improve readme and tests
This commit is contained in:
11
README.md
11
README.md
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
**A lightweight, self-hosted identity & SSO / IpD portal**
|
**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
|
* Create Admin user on first login
|
||||||
* TOTP ( QR Code ) 2FA, with backup codes ( encrypted at rest )
|
* 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 Applications available to the user on their Dashboard
|
||||||
* Display all logged in sessions and OIDC logged in sessions
|
* 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?
|
## Why Clinch?
|
||||||
|
|
||||||
@@ -106,8 +106,9 @@ Client apps (Audiobookshelf, Kavita, Grafana, etc.) redirect to Clinch for login
|
|||||||
#### Trusted-Header SSO (ForwardAuth)
|
#### Trusted-Header SSO (ForwardAuth)
|
||||||
Works with reverse proxies (Caddy, Traefik, Nginx):
|
Works with reverse proxies (Caddy, Traefik, Nginx):
|
||||||
1. Proxy sends every request to `/api/verify`
|
1. Proxy sends every request to `/api/verify`
|
||||||
2. **200 OK** → Proxy injects headers (`Remote-User`, `Remote-Groups`, `Remote-Email`) and forwards to app
|
2. Response handling:
|
||||||
3. **401/403** → Proxy redirects to Clinch login; after login, user returns to original URL
|
- **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.
|
Apps that speak OIDC use the OIDC flow; apps that only need "who is it?" headers use ForwardAuth.
|
||||||
|
|
||||||
|
|||||||
@@ -308,84 +308,6 @@ class WebauthnSecurityTest < ActionDispatch::SystemTestCase
|
|||||||
user.destroy
|
user.destroy
|
||||||
end
|
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
|
# WEBAUTHN AND PASSWORD LOGIN INTERACTION TESTS
|
||||||
# ====================
|
# ====================
|
||||||
|
|||||||
Reference in New Issue
Block a user