Class: Booth::Testing::Support::VirtualAuthenticator
- Inherits:
-
Object
- Object
- Booth::Testing::Support::VirtualAuthenticator
- Includes:
- Logging, Capybara::DSL
- Defined in:
- lib/booth/testing/support/virtual_authenticator.rb
Overview
Represents a virtual WebAuthn credential that can be transferred between browser sessions. Stores the credential data (including private key) and tracks which authenticator ID exists in each browser session.
Instance Attribute Summary collapse
-
#authenticator_ids ⇒ Object
Maps Capybara session names to their authenticator IDs Each browser session has its own isolated authenticator with the same credential.
-
#credential_data ⇒ Object
Relying Party ID for this credential.
-
#has_user_verification ⇒ Object
Relying Party ID for this credential.
-
#rp_id ⇒ Object
Relying Party ID for this credential.
Instance Method Summary collapse
-
#credential_id ⇒ Object
Returns the credential ID.
-
#current_authenticator_id ⇒ Object
Gets the authenticator ID for the current browser session.
-
#initialize(authenticator_id:, has_user_verification: true) ⇒ VirtualAuthenticator
constructor
A new instance of VirtualAuthenticator.
-
#private_key ⇒ Object
Returns the private key (base64 encoded).
-
#pull ⇒ Object
Pulls credential data from the current browser session.
-
#push ⇒ Object
Pushes the stored sign_count to the current browser session.
-
#register_authenticator_id(authenticator_id, session_name = Capybara.session_name.to_s) ⇒ Object
Registers an authenticator ID for a specific browser session.
-
#sign_count ⇒ Object
Returns the current sign count.
Constructor Details
#initialize(authenticator_id:, has_user_verification: true) ⇒ VirtualAuthenticator
15 16 17 18 19 20 21 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 15 def initialize(authenticator_id:, has_user_verification: true) @authenticator_ids = {} @has_user_verification = has_user_verification @credential_data = nil # Register the initial authenticator for the current session register_authenticator_id(authenticator_id, .session_name.to_s) end |
Instance Attribute Details
#authenticator_ids ⇒ Object
Maps Capybara session names to their authenticator IDs Each browser session has its own isolated authenticator with the same credential
12 13 14 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 12 def authenticator_ids @authenticator_ids end |
#credential_data ⇒ Object
Relying Party ID for this credential
13 14 15 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 13 def credential_data @credential_data end |
#has_user_verification ⇒ Object
Relying Party ID for this credential
13 14 15 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 13 def has_user_verification @has_user_verification end |
#rp_id ⇒ Object
Relying Party ID for this credential
13 14 15 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 13 def rp_id @rp_id end |
Instance Method Details
#credential_id ⇒ Object
Returns the credential ID
157 158 159 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 157 def credential_id credential_data&.dig('credentialId') end |
#current_authenticator_id ⇒ Object
Gets the authenticator ID for the current browser session
29 30 31 32 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 29 def current_authenticator_id session_key = .session_name.to_s authenticator_ids[session_key] || raise("No authenticator registered for session #{session_key}. Call import() first if this is a new browser session.") end |
#private_key ⇒ Object
Returns the private key (base64 encoded)
167 168 169 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 167 def private_key credential_data&.dig('privateKey') end |
#pull ⇒ Object
Pulls credential data from the current browser session. On first pull (after WebAuthn registration), this captures the private key. On subsequent pulls, this updates the stored sign_count.
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 37 def pull auth_id = current_authenticator_id log do "Pulling credentials from session #{Capybara.session_name} for authenticator #{auth_id}" end credentials = get_credentials_from_session(auth_id) if credentials.empty? raise "No credentials found in authenticator #{auth_id}. Ensure WebAuthn registration completed before pulling." end # Store the first (and typically only) credential @credential_data = credentials.first # rpId might not be returned by getCredentials in imported credentials # Use stored value if available, otherwise use what we got from browser pulled_rp_id = @credential_data['rpId'] if pulled_rp_id @rp_id = pulled_rp_id log do "Pulled credential #{credential_id} with sign_count #{sign_count} and rpId #{@rp_id}" end elsif @rp_id log do "Pulled credential #{credential_id} with sign_count #{sign_count} (rpId not returned, using stored: #{@rp_id})" end else raise "PULL FAILED: No rpId found in credential data and none stored! Keys: #{@credential_data.keys.inspect}" end log { "PUSH: Credential data keys: #{@credential_data.keys.inspect}" } self end |
#push ⇒ Object
Pushes the stored sign_count to the current browser session. Since WebAuthn.setCredentialProperties doesn’t support signCount, we remove and re-add the credential with the correct sign_count.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 76 def push unless credential_data raise 'No credential data to push. Call pull() first to capture credentials.' end auth_id = current_authenticator_id target_sign_count = sign_count # First, read current state from browser current_creds = get_credentials_from_session(auth_id) target_cred = current_creds.find { |c| c['credentialId'] == credential_id } if target_cred.nil? raise "Credential #{credential_id} not found in browser session #{Capybara.session_name}. Import this authenticator first." end current_browser_sign_count = target_cred['signCount'] log do "PUSH: Session #{Capybara.session_name}, Auth #{auth_id}, Credential #{credential_id}, Target sign_count: #{target_sign_count}, Current browser sign_count: #{current_browser_sign_count}" end if current_browser_sign_count == target_sign_count log { "PUSH: Sign count already up to date (#{target_sign_count})" } return self end # Remove and re-add credential with updated sign_count # (setCredentialProperties doesn't support signCount per CDP spec) log { "PUSH: Removing credential #{credential_id} from authenticator #{auth_id}" } remove_result = ( 'WebAuthn.removeCredential', authenticatorId: auth_id, credentialId: credential_id, ) log { "PUSH: removeCredential result: #{remove_result.inspect}" } log { "PUSH: Re-adding credential with sign_count #{target_sign_count}" } # Create credential data with updated sign_count updated_credential = credential_data.dup updated_credential['signCount'] = target_sign_count updated_credential['rpId'] = @rp_id # Debug: show what fields are in the credential log { "PUSH: Credential fields: #{updated_credential.keys.inspect}" } log { "PUSH: rpId in credential: #{updated_credential['rpId'].inspect}" } log { "PUSH: @rp_id instance variable: #{@rp_id.inspect}" } raise 'PUSH FAILED: @rp_id is nil!' unless @rp_id add_result = ( 'WebAuthn.addCredential', authenticatorId: auth_id, credential: updated_credential, ) log { "PUSH: addCredential result: #{add_result.inspect}" } # CRITICAL: Verify the update actually worked sleep 0.5 # Give browser time to apply update post_update_creds = get_credentials_from_session(auth_id) post_update_cred = post_update_creds.find { |c| c['credentialId'] == credential_id } if post_update_cred.nil? raise "PUSH FAILED: Credential #{credential_id} not found after re-add!" end actual_sign_count = post_update_cred['signCount'] log do "PUSH: Post-update browser sign_count: #{actual_sign_count} (expected: #{target_sign_count})" end unless actual_sign_count == target_sign_count raise "PUSH FAILED: Browser sign_count is #{actual_sign_count} but we expected #{target_sign_count}" end log { "PUSH: Successfully updated sign_count to #{target_sign_count} in browser" } self end |
#register_authenticator_id(authenticator_id, session_name = Capybara.session_name.to_s) ⇒ Object
Registers an authenticator ID for a specific browser session
24 25 26 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 24 def register_authenticator_id(authenticator_id, session_name = .session_name.to_s) authenticator_ids[session_name] = authenticator_id end |
#sign_count ⇒ Object
Returns the current sign count
162 163 164 |
# File 'lib/booth/testing/support/virtual_authenticator.rb', line 162 def sign_count credential_data&.dig('signCount') || 0 end |