Fix tests. Remove tests which test rails functionality
This commit is contained in:
344
test/system/webauthn_security_test.rb
Normal file
344
test/system/webauthn_security_test.rb
Normal file
@@ -0,0 +1,344 @@
|
||||
require "test_helper"
|
||||
require "webauthn/fake_client"
|
||||
|
||||
class WebauthnSecurityTest < ActionDispatch::SystemTest
|
||||
# ====================
|
||||
# REPLAY ATTACK PREVENTION (SIGN COUNT TRACKING) TESTS
|
||||
# ====================
|
||||
|
||||
test "detects suspicious sign count for replay attacks" do
|
||||
user = User.create!(email_address: "webauthn_replay_test@example.com", password: "password123")
|
||||
|
||||
# Create a WebAuthn credential
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# Simulate a suspicious sign count (decreased or reused)
|
||||
credential.update!(sign_count: 100)
|
||||
|
||||
# Try to authenticate with a lower sign count (potential replay)
|
||||
suspicious = credential.suspicious_sign_count?(99)
|
||||
|
||||
assert suspicious, "Should detect suspicious sign count indicating potential replay attack"
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "sign count is incremented after successful authentication" do
|
||||
user = User.create!(email_address: "webauthn_signcount_test@example.com", password: "password123")
|
||||
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 50,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# Simulate authentication with new sign count
|
||||
credential.update_usage!(
|
||||
sign_count: 51,
|
||||
ip_address: "192.168.1.1",
|
||||
user_agent: "Mozilla/5.0"
|
||||
)
|
||||
|
||||
credential.reload
|
||||
assert_equal 51, credential.sign_count, "Sign count should be incremented"
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# USER HANDLE BINDING TESTS
|
||||
# ====================
|
||||
|
||||
test "user handle is properly bound to WebAuthn credential" do
|
||||
user = User.create!(email_address: "webauthn_handle_test@example.com", password: "password123")
|
||||
|
||||
# Create a WebAuthn credential with user handle
|
||||
user_handle = SecureRandom.uuid
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key",
|
||||
user_handle: user_handle
|
||||
)
|
||||
|
||||
# Verify user handle is associated with the credential
|
||||
assert_equal user_handle, credential.user_handle
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "WebAuthn authentication validates user handle" do
|
||||
user = User.create!(email_address: "webauthn_handle_auth_test@example.com", password: "password123")
|
||||
|
||||
user_handle = SecureRandom.uuid
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key",
|
||||
user_handle: user_handle
|
||||
)
|
||||
|
||||
# Sign in with WebAuthn
|
||||
# The implementation should verify the user handle matches
|
||||
# This test documents the expected behavior
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# ORIGIN VALIDATION TESTS
|
||||
# ====================
|
||||
|
||||
test "WebAuthn request validates origin" do
|
||||
user = User.create!(email_address: "webauthn_origin_test@example.com", password: "password123")
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# Test WebAuthn challenge from valid origin
|
||||
post webauthn_challenge_path, params: { email: "webauthn_origin_test@example.com" },
|
||||
headers: { "HTTP_ORIGIN": "http://localhost:3000" }
|
||||
|
||||
# Should succeed for valid origin
|
||||
|
||||
# Test WebAuthn challenge from invalid origin
|
||||
post webauthn_challenge_path, params: { email: "webauthn_origin_test@example.com" },
|
||||
headers: { "HTTP_ORIGIN": "http://evil.com" }
|
||||
|
||||
# Should reject invalid origin
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "WebAuthn verification includes origin validation" do
|
||||
user = User.create!(email_address: "webauthn_verify_origin_test@example.com", password: "password123")
|
||||
user.update!(webauthn_id: SecureRandom.uuid)
|
||||
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# Sign in with WebAuthn
|
||||
post webauthn_challenge_path, params: { email: "webauthn_verify_origin_test@example.com" }
|
||||
assert_response :success
|
||||
|
||||
challenge = JSON.parse(@response.body)["challenge"]
|
||||
|
||||
# Simulate WebAuthn verification with wrong origin
|
||||
# This should fail
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# ATTESTATION FORMAT VALIDATION TESTS
|
||||
# ====================
|
||||
|
||||
test "WebAuthn accepts standard attestation formats" do
|
||||
user = User.create!(email_address: "webauthn_attestation_test@example.com", password: "password123")
|
||||
|
||||
# Register WebAuthn credential
|
||||
# Standard attestation formats: none, packed, tpm, android-key, android-safetynet, fido-u2f, etc.
|
||||
|
||||
# Test with 'none' attestation (most common for privacy)
|
||||
attestation_object = {
|
||||
fmt: "none",
|
||||
attStmt: {},
|
||||
authData: Base64.strict_encode64("fake_auth_data")
|
||||
}
|
||||
|
||||
# The implementation should accept standard attestation formats
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "WebAuthn rejects invalid attestation formats" do
|
||||
user = User.create!(email_address: "webauthn_invalid_attestation_test@example.com", password: "password123")
|
||||
|
||||
# Try to register with invalid attestation format
|
||||
invalid_attestation = {
|
||||
fmt: "invalid_format",
|
||||
attStmt: {},
|
||||
authData: Base64.strict_encode64("fake_auth_data")
|
||||
}
|
||||
|
||||
# Should reject invalid attestation format
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# CREDENTIAL CLONING DETECTION TESTS
|
||||
# ====================
|
||||
|
||||
test "detects credential cloning through sign count anomalies" do
|
||||
user = User.create!(email_address: "webauthn_clone_test@example.com", password: "password123")
|
||||
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 100,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# Simulate authentication from a cloned credential (sign count doesn't increase properly)
|
||||
# First auth: sign count = 101
|
||||
credential.update_usage!(sign_count: 101, ip_address: "192.168.1.1", user_agent: "Browser A")
|
||||
|
||||
# Second auth from different location but sign count = 101 again (cloned!)
|
||||
suspicious = credential.suspicious_sign_count?(101)
|
||||
|
||||
assert suspicious, "Should detect potential credential cloning"
|
||||
|
||||
# Verify logging for security monitoring
|
||||
# The application should log suspicious sign count anomalies
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "tracks IP address and user agent for WebAuthn authentications" do
|
||||
user = User.create!(email_address: "webauthn_tracking_test@example.com", password: "password123")
|
||||
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# Update usage with tracking information
|
||||
credential.update_usage!(
|
||||
sign_count: 1,
|
||||
ip_address: "192.168.1.100",
|
||||
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
)
|
||||
|
||||
credential.reload
|
||||
assert_equal "192.168.1.100", credential.last_ip_address
|
||||
assert_equal "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", credential.last_user_agent
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# CREDENTIAL EXCLUSION TESTS
|
||||
# ====================
|
||||
|
||||
test "prevents duplicate credential registration" do
|
||||
user = User.create!(email_address: "webauthn_duplicate_test@example.com", password: "password123")
|
||||
|
||||
credential_id = Base64.urlsafe_encode64("unique_credential_id")
|
||||
|
||||
# Register first credential
|
||||
user.webauthn_credentials.create!(
|
||||
external_id: credential_id,
|
||||
public_key: Base64.urlsafe_encode64("public_key_1"),
|
||||
sign_count: 0,
|
||||
nickname: "Key 1"
|
||||
)
|
||||
|
||||
# Try to register same credential ID again
|
||||
# Should reject or update existing credential
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# USER PRESENCE TESTS
|
||||
# ====================
|
||||
|
||||
test "WebAuthn requires user presence for authentication" do
|
||||
user = User.create!(email_address: "webauthn_presence_test@example.com", password: "password123")
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("fake_credential_id"),
|
||||
public_key: Base64.urlsafe_encode64("fake_public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Test Key"
|
||||
)
|
||||
|
||||
# WebAuthn authenticator response should include user presence flag (UP)
|
||||
# The implementation should verify this flag is set to true
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# CREDENTIAL MANAGEMENT TESTS
|
||||
# ====================
|
||||
|
||||
test "users can view and revoke their WebAuthn credentials" do
|
||||
user = User.create!(email_address: "webauthn_mgmt_test@example.com", password: "password123")
|
||||
|
||||
# Create multiple credentials
|
||||
credential1 = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("credential_1"),
|
||||
public_key: Base64.urlsafe_encode64("public_key_1"),
|
||||
sign_count: 0,
|
||||
nickname: "USB Key"
|
||||
)
|
||||
|
||||
credential2 = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("credential_2"),
|
||||
public_key: Base64.urlsafe_encode64("public_key_2"),
|
||||
sign_count: 0,
|
||||
nickname: "Laptop Key"
|
||||
)
|
||||
|
||||
# User should be able to view their credentials
|
||||
assert_equal 2, user.webauthn_credentials.count
|
||||
|
||||
# User should be able to revoke a credential
|
||||
credential1.destroy
|
||||
assert_equal 1, user.webauthn_credentials.count
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
# ====================
|
||||
# WEBAUTHN AND PASSWORD LOGIN INTERACTION TESTS
|
||||
# ====================
|
||||
|
||||
test "WebAuthn can be required for authentication" do
|
||||
user = User.create!(email_address: "webauthn_required_test@example.com", password: "password123")
|
||||
user.update!(webauthn_enabled: true)
|
||||
|
||||
# Sign in with password should still work
|
||||
post signin_path, params: { email_address: "webauthn_required_test@example.com", password: "password123" }
|
||||
|
||||
# If WebAuthn is enabled, should offer WebAuthn as an option
|
||||
# Implementation should handle password + WebAuthn or passwordless flow
|
||||
|
||||
user.destroy
|
||||
end
|
||||
|
||||
test "WebAuthn can be used for passwordless authentication" do
|
||||
user = User.create!(email_address: "webauthn_passwordless_test@example.com", password: "password123")
|
||||
user.update!(webauthn_enabled: true)
|
||||
|
||||
credential = user.webauthn_credentials.create!(
|
||||
external_id: Base64.urlsafe_encode64("passwordless_credential"),
|
||||
public_key: Base64.urlsafe_encode64("public_key"),
|
||||
sign_count: 0,
|
||||
nickname: "Passwordless Key"
|
||||
)
|
||||
|
||||
# User should be able to sign in with WebAuthn alone
|
||||
# Test passwordless flow
|
||||
|
||||
user.destroy
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user