017bcc9ce1
- 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
144 lines
4.5 KiB
Ruby
144 lines
4.5 KiB
Ruby
# = Action Pack WebAuthn Authenticator Response
|
|
#
|
|
# Abstract base class for WebAuthn authenticator responses. Provides common
|
|
# validation logic for both registration (attestation) and authentication
|
|
# (assertion) ceremonies.
|
|
#
|
|
# This class should not be instantiated directly. Use AttestationResponse for
|
|
# registration or AssertionResponse for authentication.
|
|
#
|
|
# == Validation
|
|
#
|
|
# The +validate!+ method performs security checks required by the WebAuthn
|
|
# specification:
|
|
#
|
|
# * Challenge verification - ensures the response matches the server-generated challenge
|
|
# * Origin verification - ensures the response comes from the expected origin
|
|
# * User verification - optionally requires biometric or PIN verification
|
|
#
|
|
# == Example
|
|
#
|
|
# response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(
|
|
# client_data_json: client_data_json,
|
|
# authenticator_data: authenticator_data,
|
|
# signature: signature,
|
|
# credential: credential,
|
|
# challenge: ActionPack::WebAuthn::Current.challenge,
|
|
# origin: "https://example.com",
|
|
# user_verification: :required
|
|
# )
|
|
#
|
|
# response.validate!
|
|
#
|
|
class ActionPack::WebAuthn::Authenticator::Response
|
|
include ActiveModel::Validations
|
|
|
|
attr_reader :client_data_json
|
|
attr_accessor :challenge, :origin, :user_verification
|
|
|
|
validate :challenge_must_match
|
|
validate :challenge_must_not_be_expired
|
|
validate :origin_must_match
|
|
validate :must_not_be_cross_origin
|
|
validate :must_not_have_token_binding
|
|
validate :relying_party_id_must_match
|
|
validate :user_must_be_present
|
|
validate :user_must_be_verified_when_required
|
|
|
|
def initialize(client_data_json:, challenge: nil, origin: nil, user_verification: :preferred)
|
|
@client_data_json = client_data_json
|
|
@challenge = challenge
|
|
@origin = origin
|
|
@user_verification = user_verification.to_sym
|
|
end
|
|
|
|
def validate!
|
|
super
|
|
rescue ActiveModel::ValidationError
|
|
raise ActionPack::WebAuthn::InvalidResponseError, errors.full_messages.join(", ")
|
|
end
|
|
|
|
# Returns the RelyingParty used for RP ID validation.
|
|
def relying_party
|
|
ActionPack::WebAuthn.relying_party
|
|
end
|
|
|
|
# Parses the client data JSON string into a Hash. Raises
|
|
# +InvalidResponseError+ if the JSON is malformed.
|
|
def client_data
|
|
@client_data ||= JSON.parse(client_data_json)
|
|
rescue JSON::ParserError
|
|
raise ActionPack::WebAuthn::InvalidResponseError, "Client data is not valid JSON"
|
|
end
|
|
|
|
def authenticator_data
|
|
nil
|
|
end
|
|
|
|
private
|
|
def challenge_must_match
|
|
if challenge.blank?
|
|
errors.add(:base, "Challenge missing")
|
|
elsif client_data["challenge"].blank?
|
|
errors.add(:base, "Challenge missing in client data")
|
|
elsif !ActiveSupport::SecurityUtils.secure_compare(challenge.to_s, client_data["challenge"].to_s)
|
|
errors.add(:base, "Challenge does not match")
|
|
end
|
|
end
|
|
|
|
def challenge_must_not_be_expired
|
|
return if errors.any? || challenge.blank?
|
|
|
|
signed_message = Base64.urlsafe_decode64(challenge)
|
|
|
|
unless ActionPack::WebAuthn.challenge_verifier.verified(signed_message)
|
|
errors.add(:base, "Challenge has expired")
|
|
end
|
|
rescue ArgumentError
|
|
errors.add(:base, "Challenge is invalid")
|
|
end
|
|
|
|
def origin_must_match
|
|
if origin.blank?
|
|
errors.add(:base, "Origin missing")
|
|
elsif client_data["origin"].blank?
|
|
errors.add(:base, "Origin missing in client data")
|
|
elsif !ActiveSupport::SecurityUtils.secure_compare(origin.to_s, client_data["origin"].to_s)
|
|
errors.add(:base, "Origin does not match")
|
|
end
|
|
end
|
|
|
|
def must_not_be_cross_origin
|
|
if client_data["crossOrigin"] == true
|
|
errors.add(:base, "Cross-origin requests are not supported")
|
|
end
|
|
end
|
|
|
|
def must_not_have_token_binding
|
|
if client_data.dig("tokenBinding", "status") == "present"
|
|
errors.add(:base, "Token binding is not supported")
|
|
end
|
|
end
|
|
|
|
def relying_party_id_must_match
|
|
unless ActiveSupport::SecurityUtils.secure_compare(
|
|
Digest::SHA256.digest(relying_party.id),
|
|
authenticator_data&.relying_party_id_hash || ""
|
|
)
|
|
errors.add(:base, "Relying party ID does not match")
|
|
end
|
|
end
|
|
|
|
def user_must_be_present
|
|
unless authenticator_data&.user_present?
|
|
errors.add(:base, "User presence is required")
|
|
end
|
|
end
|
|
|
|
def user_must_be_verified_when_required
|
|
if user_verification == :required && !authenticator_data&.user_verified?
|
|
errors.add(:base, "User verification is required")
|
|
end
|
|
end
|
|
end
|