4bb0a2929c
- Add a CBOR decoder - Add public key credential options - Add COSE key decoder - Add authenticator attestation and assertion - Add credentials - Add exclude_credentials
118 lines
3.0 KiB
Ruby
118 lines
3.0 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.validate!(
|
|
# challenge: session[:webauthn_challenge],
|
|
# origin: "https://example.com",
|
|
# user_verification: :required
|
|
# )
|
|
#
|
|
class ActionPack::WebAuthn::Authenticator::Response
|
|
# Raised when response validation fails.
|
|
class InvalidResponseError < StandardError; end
|
|
|
|
attr_reader :client_data_json
|
|
|
|
def initialize(client_data_json:)
|
|
@client_data_json = client_data_json
|
|
end
|
|
|
|
def valid?(**args)
|
|
validate!(**args)
|
|
true
|
|
rescue InvalidResponseError
|
|
false
|
|
end
|
|
|
|
def validate!(challenge:, origin:, user_verification: :preferred)
|
|
unless challenge_matches?(challenge)
|
|
raise InvalidResponseError, "Challenge does not match"
|
|
end
|
|
|
|
unless origin_matches?(origin)
|
|
raise InvalidResponseError, "Origin does not match"
|
|
end
|
|
|
|
if cross_origin?
|
|
raise InvalidResponseError, "Cross-origin requests are not supported"
|
|
end
|
|
|
|
if token_binding_present?
|
|
raise InvalidResponseError, "Token binding is not supported"
|
|
end
|
|
|
|
unless relying_party_id_matches?
|
|
raise InvalidResponseError, "Relying party ID does not match"
|
|
end
|
|
|
|
unless user_present?
|
|
raise InvalidResponseError, "User presence is required"
|
|
end
|
|
|
|
if user_verification == :required && !user_verified?
|
|
raise InvalidResponseError, "User verification is required"
|
|
end
|
|
end
|
|
|
|
def relying_party
|
|
ActionPack::WebAuthn.relying_party
|
|
end
|
|
|
|
def client_data
|
|
@client_data ||= JSON.parse(client_data_json)
|
|
end
|
|
|
|
def authenticator_data
|
|
nil
|
|
end
|
|
|
|
private
|
|
def challenge_matches?(expected_challenge)
|
|
ActiveSupport::SecurityUtils.secure_compare(expected_challenge, client_data["challenge"])
|
|
end
|
|
|
|
def origin_matches?(expected_origin)
|
|
ActiveSupport::SecurityUtils.secure_compare(expected_origin, client_data["origin"])
|
|
end
|
|
|
|
def relying_party_id_matches?
|
|
ActiveSupport::SecurityUtils.secure_compare(
|
|
Digest::SHA256.digest(relying_party.id),
|
|
authenticator_data&.relying_party_id_hash || ""
|
|
)
|
|
end
|
|
|
|
def cross_origin?
|
|
client_data["crossOrigin"] == true
|
|
end
|
|
|
|
def token_binding_present?
|
|
client_data.dig("tokenBinding", "status") == "present"
|
|
end
|
|
|
|
def user_present?
|
|
authenticator_data&.user_present?
|
|
end
|
|
|
|
def user_verified?
|
|
authenticator_data&.user_verified?
|
|
end
|
|
end
|