Files
fizzy/lib/action_pack/web_authn/authenticator/assertion_response.rb
T
Stanko K.R. 017bcc9ce1 Polish up Passkey interface
- Fix indentation
- Split awkward initializer into separate methods
- Rename credentials to passkeys
- Simplify authenticator registry loading
- Flatten nested errors under ActionPack::WebAuthn
- Set passkey current params only requests that use it
- Inline anemic methods
- Replace custom validation with ActiveModel::Validation
- Rename identity to holder in Passkey
- Rename credentials to passkeys in JS
- Extract framework library out of controllers
- Pass params hashes down to ActionPack
- Attempt to simplify public interface
- Push data decoding down to the classes representing the data
- Introduce has_passkeys
- Add CBOR bigint support
- Rename ActionPack::WebAuthn::Passkey to ActionPack::Passkey
- Add create_passkey_button helper
- Rename public-key to creation-options
- Add sign_in_with_passkey_button helper
- Dispatch events for the whole Passkey lifecycle
- Add ED25519 support
- Prevent crash on missing meta tag
- Validate resident key options
- Don't clobber existing ActionPack config options
- Validate cryptographic params
- Move CurrentWebAuthnRequest into ActionPack::Passkey
- Use ActiveModel::Attributes for Options objects
- Implement expiring challanges
- Add lifecycle events
- Extract param helpers into Request
- Add passkey_creation_options and passkey_request_options helpers
- Add create_passkey_challenge to make it easier to override the create method if needed
- Prefix all view helpers with passkey_
- Auto-include ActionPack::Passkey::Holder
- Make the passkey challange url configurable
- Add a reminder about Passkeys to the magic link email
2026-03-18 11:51:10 +01:00

86 lines
3.0 KiB
Ruby

# = Action Pack WebAuthn Assertion Response
#
# Handles the authenticator response from a WebAuthn authentication ceremony.
# When a user authenticates with an existing credential, the authenticator
# returns an assertion response containing a signature that proves possession
# of the private key.
#
# == Usage
#
# # Look up the credential by ID
# credential = user.credentials.find_by!(
# credential_id: params[:id]
# )
#
# response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(
# client_data_json: params[:response][:clientDataJSON],
# authenticator_data: params[:response][:authenticatorData],
# signature: params[:response][:signature],
# credential: credential.to_public_key_credential,
# challenge: ActionPack::WebAuthn::Current.challenge,
# origin: "https://example.com"
# )
#
# response.validate!
#
# == Validation
#
# In addition to the base Response validations, this class verifies:
#
# * The client data type is "webauthn.get"
# * The signature is valid for the credential's public key
#
class ActionPack::WebAuthn::Authenticator::AssertionResponse < ActionPack::WebAuthn::Authenticator::Response
attr_reader :credential, :authenticator_data, :signature
validate :client_data_type_must_be_get
validate :signature_must_be_valid
validate :sign_count_must_increase
def initialize(credential:, authenticator_data:, signature:, **attributes)
super(**attributes)
@credential = credential
@signature = signature
@signature = Base64.urlsafe_decode64(@signature) unless @signature.encoding == Encoding::BINARY
@authenticator_data = ActionPack::WebAuthn::Authenticator::Data.wrap(authenticator_data)
rescue ArgumentError
raise ActionPack::WebAuthn::InvalidResponseError, "Invalid base64 encoding in signature"
end
private
def client_data_type_must_be_get
unless client_data["type"] == "webauthn.get"
errors.add(:base, "Client data type is not webauthn.get")
end
end
def signature_must_be_valid
client_data_hash = Digest::SHA256.digest(client_data_json)
signed_data = authenticator_data.bytes.pack("C*") + client_data_hash
digest = credential.public_key.oid == "ED25519" ? nil : "SHA256"
unless credential.public_key.verify(digest, signature, signed_data)
errors.add(:base, "Invalid signature")
end
rescue OpenSSL::PKey::PKeyError
errors.add(:base, "Invalid signature")
end
def sign_count_must_increase
unless sign_count_increased?
errors.add(:base, "Sign count did not increase")
end
end
def sign_count_increased?
if authenticator_data.sign_count.zero? && credential.sign_count.zero?
# Some authenticators always return 0 for the sign count, even after multiple authentications.
# In that case, we have to check that both the stored and returned sign counts are 0,
# which indicates that the authenticator is likely not updating the sign count.
true
else
authenticator_data.sign_count > credential.sign_count
end
end
end