Clean up and secure web_authn controller
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

This commit is contained in:
Dan Milne
2025-12-31 11:44:11 +11:00
parent 9530c8284f
commit 9d402fcd92
3 changed files with 267 additions and 17 deletions

View File

@@ -1,7 +1,7 @@
require "test_helper"
require "webauthn/fake_client"
class WebauthnSecurityTest < ActionDispatch::SystemTest
class WebauthnSecurityTest < ActionDispatch::SystemTestCase
# ====================
# REPLAY ATTACK PREVENTION (SIGN COUNT TRACKING) TESTS
# ====================
@@ -308,6 +308,84 @@ class WebauthnSecurityTest < ActionDispatch::SystemTest
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
# ====================