Rely solely on the challange signature for verification
The cookie approach seems like the more secure aproach because it ties the authentication or registration attempt to the user's browser session, but it doesn't work reliably on Chrome for Windows. Also, a simila problem pops up on Chrome for Linux if the session is used instead of a separate cookie. It looks like the browser doesn't propagate the state change through fast enough which results in some requests contaiining the new/updated cookie, and others don't, which results in sporadic failures. Since we use a signed and expiring challange we still get protection from replay attacks and tampering which enables us to omit the cookie entierly and rely on the challange's signature to prove expiration and authenticity. The only thing we lose is the ability to tie and attemp to a single browser session. See: https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-challengeURL which proposes to add the same challange fetching logic as part of the standard See: https://github.com/w3c/webauthn/issues/1856 which discusses issues that arise from having expiring challanges (which the spec recommends) See: https://github.com/OneUptime/oneuptime/security/advisories/GHSA-gjjc-pcwp-c74m which is an explot that can happen if the server isn't able to verify the authenticity of challanges that are sent outside of a cookie
This commit is contained in:
@@ -17,7 +17,6 @@
|
||||
# authenticator_data: params[:response][:authenticatorData],
|
||||
# signature: params[:response][:signature],
|
||||
# credential: credential.to_public_key_credential,
|
||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
||||
# origin: "https://example.com"
|
||||
# )
|
||||
#
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
# response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||
# client_data_json: params[:response][:clientDataJSON],
|
||||
# attestation_object: params[:response][:attestationObject],
|
||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
||||
# origin: "https://example.com"
|
||||
# )
|
||||
#
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
# authenticator_data: authenticator_data,
|
||||
# signature: signature,
|
||||
# credential: credential,
|
||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
||||
# origin: "https://example.com",
|
||||
# user_verification: :required
|
||||
# )
|
||||
@@ -34,9 +33,9 @@ class ActionPack::WebAuthn::Authenticator::Response
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :client_data_json
|
||||
attr_accessor :challenge, :origin, :user_verification
|
||||
attr_accessor :origin, :user_verification
|
||||
|
||||
validate :challenge_must_match
|
||||
validate :challenge_must_be_present
|
||||
validate :challenge_must_not_be_expired
|
||||
validate :origin_must_match
|
||||
validate :must_not_be_cross_origin
|
||||
@@ -45,9 +44,8 @@ class ActionPack::WebAuthn::Authenticator::Response
|
||||
validate :user_must_be_present
|
||||
validate :user_must_be_verified_when_required
|
||||
|
||||
def initialize(client_data_json:, challenge: nil, origin: nil, user_verification: :preferred)
|
||||
def initialize(client_data_json:, origin: nil, user_verification: :preferred)
|
||||
@client_data_json = client_data_json
|
||||
@challenge = challenge
|
||||
@origin = origin
|
||||
@user_verification = user_verification.to_sym
|
||||
end
|
||||
@@ -76,20 +74,16 @@ class ActionPack::WebAuthn::Authenticator::Response
|
||||
end
|
||||
|
||||
private
|
||||
def challenge_must_match
|
||||
if challenge.blank?
|
||||
def challenge_must_be_present
|
||||
if client_data["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?
|
||||
return if errors.any?
|
||||
|
||||
signed_message = Base64.urlsafe_decode64(challenge)
|
||||
signed_message = Base64.urlsafe_decode64(client_data["challenge"])
|
||||
|
||||
unless ActionPack::WebAuthn.challenge_verifier.verified(signed_message)
|
||||
errors.add(:base, "Challenge has expired")
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
# The expected origin (typically +request.base_url+). Validated against the
|
||||
# +origin+ field in the authenticator's client data.
|
||||
#
|
||||
# [+challenge+]
|
||||
# The Base64URL-encoded challenge read from the encrypted cookie. Validated
|
||||
# against the +challenge+ field in the authenticator's client data.
|
||||
#
|
||||
class ActionPack::WebAuthn::Current < ActiveSupport::CurrentAttributes
|
||||
attribute :host, :origin, :challenge
|
||||
attribute :host, :origin
|
||||
end
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#
|
||||
# credential = ActionPack::WebAuthn::PublicKeyCredential.register(
|
||||
# params[:passkey],
|
||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
||||
# origin: ActionPack::WebAuthn::Current.origin
|
||||
# )
|
||||
#
|
||||
@@ -80,11 +79,10 @@ class ActionPack::WebAuthn::PublicKeyCredential
|
||||
# PublicKeyCredential with the registered credential data.
|
||||
#
|
||||
# Raises +InvalidResponseError+ if the attestation is invalid.
|
||||
def register(params, challenge: ActionPack::WebAuthn::Current.challenge, origin: ActionPack::WebAuthn::Current.origin)
|
||||
def register(params, origin: ActionPack::WebAuthn::Current.origin)
|
||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||
client_data_json: params[:client_data_json],
|
||||
attestation_object: params[:attestation_object],
|
||||
challenge: challenge,
|
||||
origin: origin
|
||||
)
|
||||
|
||||
@@ -126,13 +124,12 @@ class ActionPack::WebAuthn::PublicKeyCredential
|
||||
# Updates +sign_count+ and +backed_up+ on success.
|
||||
#
|
||||
# Raises +InvalidResponseError+ if the assertion is invalid.
|
||||
def authenticate(params, challenge: ActionPack::WebAuthn::Current.challenge, origin: ActionPack::WebAuthn::Current.origin)
|
||||
def authenticate(params, origin: ActionPack::WebAuthn::Current.origin)
|
||||
response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(
|
||||
client_data_json: params[:client_data_json],
|
||||
authenticator_data: params[:authenticator_data],
|
||||
signature: params[:signature],
|
||||
credential: self,
|
||||
challenge: challenge,
|
||||
origin: origin
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user