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:
@@ -47,14 +47,13 @@ class ActionPack::Passkey < Rails.configuration.action_pack.passkey.parent_class
|
|||||||
|
|
||||||
# Verifies the attestation response from the browser and persists a new passkey record.
|
# Verifies the attestation response from the browser and persists a new passkey record.
|
||||||
# The +passkey+ hash should contain +client_data_json+, +attestation_object+, and +transports+
|
# The +passkey+ hash should contain +client_data_json+, +attestation_object+, and +transports+
|
||||||
# as submitted by the registration form. The +challenge+ defaults to
|
# as submitted by the registration form. The challenge is extracted from the authenticator's
|
||||||
# +ActionPack::WebAuthn::Current.challenge+, which is automatically populated from the session
|
# +clientDataJSON+ response and verified server-side. Any additional +attributes+ (e.g. +holder+)
|
||||||
# by ActionPack::Passkey::Request. Any additional +attributes+ (e.g. +holder+) are passed
|
# are passed through to +create!+.
|
||||||
# through to +create!+.
|
|
||||||
#
|
#
|
||||||
# Raises ActionPack::WebAuthn::InvalidResponseError if the attestation is invalid.
|
# Raises ActionPack::WebAuthn::InvalidResponseError if the attestation is invalid.
|
||||||
def register(passkey, challenge: ActionPack::WebAuthn::Current.challenge, **attributes)
|
def register(passkey, **attributes)
|
||||||
credential = ActionPack::WebAuthn::PublicKeyCredential.register(passkey, challenge: challenge)
|
credential = ActionPack::WebAuthn::PublicKeyCredential.register(passkey)
|
||||||
|
|
||||||
create!(**credential.to_h, **attributes)
|
create!(**credential.to_h, **attributes)
|
||||||
end
|
end
|
||||||
@@ -74,17 +73,17 @@ class ActionPack::Passkey < Rails.configuration.action_pack.passkey.parent_class
|
|||||||
# Looks up a passkey by credential ID and verifies the assertion response from the browser.
|
# Looks up a passkey by credential ID and verifies the assertion response from the browser.
|
||||||
# Returns the authenticated Passkey record, or +nil+ if the credential is not found or
|
# Returns the authenticated Passkey record, or +nil+ if the credential is not found or
|
||||||
# verification fails.
|
# verification fails.
|
||||||
def authenticate(passkey, challenge: ActionPack::WebAuthn::Current.challenge)
|
def authenticate(passkey)
|
||||||
find_by(credential_id: passkey[:id])&.authenticate(passkey, challenge: challenge)
|
find_by(credential_id: passkey[:id])&.authenticate(passkey)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Verifies the assertion response against this passkey's stored credential and updates the
|
# Verifies the assertion response against this passkey's stored credential and updates the
|
||||||
# +sign_count+ and +backed_up+ attributes. Returns +self+ on success, or +nil+ if the
|
# +sign_count+ and +backed_up+ attributes. Returns +self+ on success, or +nil+ if the
|
||||||
# response is invalid.
|
# response is invalid.
|
||||||
def authenticate(passkey, challenge: ActionPack::WebAuthn::Current.challenge)
|
def authenticate(passkey)
|
||||||
credential = to_public_key_credential
|
credential = to_public_key_credential
|
||||||
credential.authenticate(passkey, challenge: challenge)
|
credential.authenticate(passkey)
|
||||||
update!(sign_count: credential.sign_count, backed_up: credential.backed_up)
|
update!(sign_count: credential.sign_count, backed_up: credential.backed_up)
|
||||||
self
|
self
|
||||||
rescue ActionPack::WebAuthn::InvalidResponseError
|
rescue ActionPack::WebAuthn::InvalidResponseError
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
# authentication ceremony so that the challenge is issued just-in-time rather
|
# authentication ceremony so that the challenge is issued just-in-time rather
|
||||||
# than embedded in the initial page load.
|
# than embedded in the initial page load.
|
||||||
#
|
#
|
||||||
# The generated challenge is stored in an encrypted, HTTP-only, same-site
|
# The generated challenge is returned in the JSON response body. The challenge
|
||||||
# cookie and simultaneously returned in the JSON response body. The cookie is
|
# is a signed, expiring token that the server can verify on the subsequent
|
||||||
# consumed by ActionPack::Passkey::Request on the subsequent form submission.
|
# form submission without needing server-side state — the challenge is
|
||||||
|
# extracted from the authenticator's +clientDataJSON+ response.
|
||||||
#
|
#
|
||||||
# == Route
|
# == Route
|
||||||
#
|
#
|
||||||
@@ -15,17 +16,11 @@
|
|||||||
# via +config.action_pack.passkey.routes_prefix+).
|
# via +config.action_pack.passkey.routes_prefix+).
|
||||||
#
|
#
|
||||||
class ActionPack::Passkey::ChallengesController < ActionController::Base
|
class ActionPack::Passkey::ChallengesController < ActionController::Base
|
||||||
COOKIE_NAME = :action_pack_passkey_challenge
|
|
||||||
|
|
||||||
include ActionPack::Passkey::Request
|
include ActionPack::Passkey::Request
|
||||||
|
|
||||||
# Generates a fresh challenge, stores it in an encrypted cookie, and returns
|
# Generates a fresh challenge and returns it as JSON.
|
||||||
# it as JSON. The cookie is consumed on the next passkey form submission.
|
|
||||||
def create
|
def create
|
||||||
challenge = create_passkey_challenge
|
render json: { challenge: create_passkey_challenge }
|
||||||
|
|
||||||
cookies.encrypted[COOKIE_NAME] = { value: challenge, httponly: true, same_site: :lax, secure: !request.local? && request.ssl? }
|
|
||||||
render json: { challenge: challenge }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -43,9 +43,7 @@
|
|||||||
# == Before Action
|
# == Before Action
|
||||||
#
|
#
|
||||||
# Automatically populates +ActionPack::WebAuthn::Current+ with the request
|
# Automatically populates +ActionPack::WebAuthn::Current+ with the request
|
||||||
# host, origin, and challenge (read from the encrypted cookie set by
|
# host and origin.
|
||||||
# ChallengesController). The cookie is deleted after being read to prevent
|
|
||||||
# replay.
|
|
||||||
#
|
#
|
||||||
module ActionPack::Passkey::Request
|
module ActionPack::Passkey::Request
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
@@ -54,8 +52,6 @@ module ActionPack::Passkey::Request
|
|||||||
before_action do
|
before_action do
|
||||||
ActionPack::WebAuthn::Current.host = request.host
|
ActionPack::WebAuthn::Current.host = request.host
|
||||||
ActionPack::WebAuthn::Current.origin = request.base_url
|
ActionPack::WebAuthn::Current.origin = request.base_url
|
||||||
ActionPack::WebAuthn::Current.challenge = cookies.encrypted[ActionPack::Passkey::ChallengesController::COOKIE_NAME]
|
|
||||||
cookies.delete(ActionPack::Passkey::ChallengesController::COOKIE_NAME)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
# authenticator_data: params[:response][:authenticatorData],
|
# authenticator_data: params[:response][:authenticatorData],
|
||||||
# signature: params[:response][:signature],
|
# signature: params[:response][:signature],
|
||||||
# credential: credential.to_public_key_credential,
|
# credential: credential.to_public_key_credential,
|
||||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
|
||||||
# origin: "https://example.com"
|
# origin: "https://example.com"
|
||||||
# )
|
# )
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
# response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
# response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
# client_data_json: params[:response][:clientDataJSON],
|
# client_data_json: params[:response][:clientDataJSON],
|
||||||
# attestation_object: params[:response][:attestationObject],
|
# attestation_object: params[:response][:attestationObject],
|
||||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
|
||||||
# origin: "https://example.com"
|
# origin: "https://example.com"
|
||||||
# )
|
# )
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
# authenticator_data: authenticator_data,
|
# authenticator_data: authenticator_data,
|
||||||
# signature: signature,
|
# signature: signature,
|
||||||
# credential: credential,
|
# credential: credential,
|
||||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
|
||||||
# origin: "https://example.com",
|
# origin: "https://example.com",
|
||||||
# user_verification: :required
|
# user_verification: :required
|
||||||
# )
|
# )
|
||||||
@@ -34,9 +33,9 @@ class ActionPack::WebAuthn::Authenticator::Response
|
|||||||
include ActiveModel::Validations
|
include ActiveModel::Validations
|
||||||
|
|
||||||
attr_reader :client_data_json
|
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 :challenge_must_not_be_expired
|
||||||
validate :origin_must_match
|
validate :origin_must_match
|
||||||
validate :must_not_be_cross_origin
|
validate :must_not_be_cross_origin
|
||||||
@@ -45,9 +44,8 @@ class ActionPack::WebAuthn::Authenticator::Response
|
|||||||
validate :user_must_be_present
|
validate :user_must_be_present
|
||||||
validate :user_must_be_verified_when_required
|
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
|
@client_data_json = client_data_json
|
||||||
@challenge = challenge
|
|
||||||
@origin = origin
|
@origin = origin
|
||||||
@user_verification = user_verification.to_sym
|
@user_verification = user_verification.to_sym
|
||||||
end
|
end
|
||||||
@@ -76,20 +74,16 @@ class ActionPack::WebAuthn::Authenticator::Response
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def challenge_must_match
|
def challenge_must_be_present
|
||||||
if challenge.blank?
|
if client_data["challenge"].blank?
|
||||||
errors.add(:base, "Challenge missing")
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def challenge_must_not_be_expired
|
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)
|
unless ActionPack::WebAuthn.challenge_verifier.verified(signed_message)
|
||||||
errors.add(:base, "Challenge has expired")
|
errors.add(:base, "Challenge has expired")
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
# The expected origin (typically +request.base_url+). Validated against the
|
# The expected origin (typically +request.base_url+). Validated against the
|
||||||
# +origin+ field in the authenticator's client data.
|
# +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
|
class ActionPack::WebAuthn::Current < ActiveSupport::CurrentAttributes
|
||||||
attribute :host, :origin, :challenge
|
attribute :host, :origin
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
#
|
#
|
||||||
# credential = ActionPack::WebAuthn::PublicKeyCredential.register(
|
# credential = ActionPack::WebAuthn::PublicKeyCredential.register(
|
||||||
# params[:passkey],
|
# params[:passkey],
|
||||||
# challenge: ActionPack::WebAuthn::Current.challenge,
|
|
||||||
# origin: ActionPack::WebAuthn::Current.origin
|
# origin: ActionPack::WebAuthn::Current.origin
|
||||||
# )
|
# )
|
||||||
#
|
#
|
||||||
@@ -80,11 +79,10 @@ class ActionPack::WebAuthn::PublicKeyCredential
|
|||||||
# PublicKeyCredential with the registered credential data.
|
# PublicKeyCredential with the registered credential data.
|
||||||
#
|
#
|
||||||
# Raises +InvalidResponseError+ if the attestation is invalid.
|
# 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(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: params[:client_data_json],
|
client_data_json: params[:client_data_json],
|
||||||
attestation_object: params[:attestation_object],
|
attestation_object: params[:attestation_object],
|
||||||
challenge: challenge,
|
|
||||||
origin: origin
|
origin: origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,13 +124,12 @@ class ActionPack::WebAuthn::PublicKeyCredential
|
|||||||
# Updates +sign_count+ and +backed_up+ on success.
|
# Updates +sign_count+ and +backed_up+ on success.
|
||||||
#
|
#
|
||||||
# Raises +InvalidResponseError+ if the assertion is invalid.
|
# 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(
|
response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(
|
||||||
client_data_json: params[:client_data_json],
|
client_data_json: params[:client_data_json],
|
||||||
authenticator_data: params[:authenticator_data],
|
authenticator_data: params[:authenticator_data],
|
||||||
signature: params[:signature],
|
signature: params[:signature],
|
||||||
credential: self,
|
credential: self,
|
||||||
challenge: challenge,
|
|
||||||
origin: origin
|
origin: origin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ class My::PasskeyChallengesControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stores challenge in cookie" do
|
|
||||||
untenanted do
|
|
||||||
post my_passkey_challenge_url
|
|
||||||
|
|
||||||
jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
|
|
||||||
assert_equal response.parsed_body["challenge"], jar.encrypted[ActionPack::Passkey::ChallengesController::COOKIE_NAME]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns a different challenge each time" do
|
test "returns a different challenge each time" do
|
||||||
untenanted do
|
untenanted do
|
||||||
post my_passkey_challenge_url
|
post my_passkey_challenge_url
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ActionPack::PasskeyTest < ActiveSupport::TestCase
|
|||||||
challenge = ActionPack::Passkey.request_options(credentials: [ @passkey ]).challenge
|
challenge = ActionPack::Passkey.request_options(credentials: [ @passkey ]).challenge
|
||||||
assertion = build_assertion(challenge: challenge)
|
assertion = build_assertion(challenge: challenge)
|
||||||
|
|
||||||
result = @passkey.authenticate(assertion, challenge: challenge)
|
result = @passkey.authenticate(assertion)
|
||||||
|
|
||||||
assert_equal @passkey, result
|
assert_equal @passkey, result
|
||||||
end
|
end
|
||||||
@@ -30,14 +30,14 @@ class ActionPack::PasskeyTest < ActiveSupport::TestCase
|
|||||||
assertion = build_assertion(challenge: challenge)
|
assertion = build_assertion(challenge: challenge)
|
||||||
assertion[:signature] = Base64.urlsafe_encode64("invalid", padding: false)
|
assertion[:signature] = Base64.urlsafe_encode64("invalid", padding: false)
|
||||||
|
|
||||||
assert_nil @passkey.authenticate(assertion, challenge: challenge)
|
assert_nil @passkey.authenticate(assertion)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "authenticate updates sign count and backed_up" do
|
test "authenticate updates sign count and backed_up" do
|
||||||
challenge = ActionPack::Passkey.request_options(credentials: [ @passkey ]).challenge
|
challenge = ActionPack::Passkey.request_options(credentials: [ @passkey ]).challenge
|
||||||
assertion = build_assertion(challenge: challenge, sign_count: 5, backed_up: true)
|
assertion = build_assertion(challenge: challenge, sign_count: 5, backed_up: true)
|
||||||
|
|
||||||
@passkey.authenticate(assertion, challenge: challenge)
|
@passkey.authenticate(assertion)
|
||||||
|
|
||||||
assert_equal 5, @passkey.reload.sign_count
|
assert_equal 5, @passkey.reload.sign_count
|
||||||
assert @passkey.backed_up?
|
assert @passkey.backed_up?
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
authenticator_data: @authenticator_data,
|
authenticator_data: @authenticator_data,
|
||||||
signature: @signature,
|
signature: @signature,
|
||||||
credential: @credential,
|
credential: @credential,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -56,7 +55,6 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
authenticator_data: @authenticator_data,
|
authenticator_data: @authenticator_data,
|
||||||
signature: sign(@authenticator_data, client_data_json),
|
signature: sign(@authenticator_data, client_data_json),
|
||||||
credential: @credential,
|
credential: @credential,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,7 +71,6 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
authenticator_data: @authenticator_data,
|
authenticator_data: @authenticator_data,
|
||||||
signature: Base64.urlsafe_encode64("invalid-signature", padding: false),
|
signature: Base64.urlsafe_encode64("invalid-signature", padding: false),
|
||||||
credential: @credential,
|
credential: @credential,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,14 +81,28 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
assert_equal "Invalid signature", error.message
|
assert_equal "Invalid signature", error.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate! raises when challenge does not match" do
|
test "validate! raises when challenge in client data is invalid" do
|
||||||
@response.challenge = "wrong-challenge"
|
client_data_json = {
|
||||||
|
challenge: "not-a-valid-signed-challenge",
|
||||||
|
origin: @origin,
|
||||||
|
type: "webauthn.get"
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
authenticator_data = build_authenticator_data(user_verified: true)
|
||||||
|
|
||||||
|
response = ActionPack::WebAuthn::Authenticator::AssertionResponse.new(
|
||||||
|
client_data_json: client_data_json,
|
||||||
|
authenticator_data: authenticator_data,
|
||||||
|
signature: sign(authenticator_data, client_data_json),
|
||||||
|
credential: @credential,
|
||||||
|
origin: @origin
|
||||||
|
)
|
||||||
|
|
||||||
error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
|
error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
|
||||||
@response.validate!
|
response.validate!
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_equal "Challenge does not match", error.message
|
assert_match /Challenge (is invalid|has expired)/, error.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate! raises when origin does not match" do
|
test "validate! raises when origin does not match" do
|
||||||
@@ -111,7 +122,6 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
authenticator_data: authenticator_data,
|
authenticator_data: authenticator_data,
|
||||||
signature: sign(authenticator_data, @client_data_json),
|
signature: sign(authenticator_data, @client_data_json),
|
||||||
credential: @credential,
|
credential: @credential,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin,
|
origin: @origin,
|
||||||
user_verification: :preferred
|
user_verification: :preferred
|
||||||
)
|
)
|
||||||
@@ -128,7 +138,6 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
authenticator_data: authenticator_data,
|
authenticator_data: authenticator_data,
|
||||||
signature: sign(authenticator_data, @client_data_json),
|
signature: sign(authenticator_data, @client_data_json),
|
||||||
credential: @credential,
|
credential: @credential,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin,
|
origin: @origin,
|
||||||
user_verification: :required
|
user_verification: :required
|
||||||
)
|
)
|
||||||
@@ -145,7 +154,6 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
authenticator_data: authenticator_data,
|
authenticator_data: authenticator_data,
|
||||||
signature: sign(authenticator_data, @client_data_json),
|
signature: sign(authenticator_data, @client_data_json),
|
||||||
credential: @credential,
|
credential: @credential,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin,
|
origin: @origin,
|
||||||
user_verification: :required
|
user_verification: :required
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
@response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
@response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
attestation_object: ATTESTATION_NONE_VERIFIED,
|
attestation_object: ATTESTATION_NONE_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -67,7 +66,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
attestation_object: ATTESTATION_NONE_NOT_VERIFIED,
|
attestation_object: ATTESTATION_NONE_NOT_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin,
|
origin: @origin,
|
||||||
user_verification: :preferred
|
user_verification: :preferred
|
||||||
)
|
)
|
||||||
@@ -81,7 +79,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
attestation_object: ATTESTATION_NONE_VERIFIED,
|
attestation_object: ATTESTATION_NONE_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin,
|
origin: @origin,
|
||||||
user_verification: :required
|
user_verification: :required
|
||||||
)
|
)
|
||||||
@@ -95,7 +92,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
attestation_object: ATTESTATION_NONE_NOT_VERIFIED,
|
attestation_object: ATTESTATION_NONE_NOT_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin,
|
origin: @origin,
|
||||||
user_verification: :required
|
user_verification: :required
|
||||||
)
|
)
|
||||||
@@ -117,7 +113,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: client_data_json,
|
client_data_json: client_data_json,
|
||||||
attestation_object: ATTESTATION_NONE_VERIFIED,
|
attestation_object: ATTESTATION_NONE_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,14 +123,24 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
assert_equal "Client data type is not webauthn.create", error.message
|
assert_equal "Client data type is not webauthn.create", error.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate! raises when challenge does not match" do
|
test "validate! raises when challenge in client data is invalid" do
|
||||||
@response.challenge = "wrong-challenge"
|
client_data_json = {
|
||||||
|
challenge: "not-a-valid-signed-challenge",
|
||||||
|
origin: @origin,
|
||||||
|
type: "webauthn.create"
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
|
client_data_json: client_data_json,
|
||||||
|
attestation_object: ATTESTATION_NONE_VERIFIED,
|
||||||
|
origin: @origin
|
||||||
|
)
|
||||||
|
|
||||||
error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
|
error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
|
||||||
@response.validate!
|
response.validate!
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_equal "Challenge does not match", error.message
|
assert_match /Challenge (is invalid|has expired)/, error.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate! raises when origin does not match" do
|
test "validate! raises when origin does not match" do
|
||||||
@@ -152,7 +157,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
attestation_object: ATTESTATION_PACKED_VERIFIED,
|
attestation_object: ATTESTATION_PACKED_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,7 +177,6 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
response = ActionPack::WebAuthn::Authenticator::AttestationResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
attestation_object: ATTESTATION_PACKED_VERIFIED,
|
attestation_object: ATTESTATION_PACKED_VERIFIED,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCas
|
|||||||
@response = TestableResponse.new(
|
@response = TestableResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
authenticator_data: @authenticator_data,
|
authenticator_data: @authenticator_data,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -41,9 +40,20 @@ class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCas
|
|||||||
assert @response.valid?
|
assert @response.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "valid? returns false when challenge does not match" do
|
test "valid? returns false when challenge in client data is invalid" do
|
||||||
@response.challenge = "wrong-challenge"
|
client_data_json = {
|
||||||
assert_not @response.valid?
|
challenge: "not-a-valid-signed-challenge",
|
||||||
|
origin: @origin,
|
||||||
|
type: "webauthn.create"
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
response = TestableResponse.new(
|
||||||
|
client_data_json: client_data_json,
|
||||||
|
authenticator_data: @authenticator_data,
|
||||||
|
origin: @origin
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_not response.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "valid? returns false when origin does not match" do
|
test "valid? returns false when origin does not match" do
|
||||||
@@ -51,14 +61,24 @@ class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCas
|
|||||||
assert_not @response.valid?
|
assert_not @response.valid?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate! raises when challenge does not match" do
|
test "validate! raises when challenge in client data is invalid" do
|
||||||
@response.challenge = "wrong-challenge"
|
client_data_json = {
|
||||||
|
challenge: "not-a-valid-signed-challenge",
|
||||||
|
origin: @origin,
|
||||||
|
type: "webauthn.create"
|
||||||
|
}.to_json
|
||||||
|
|
||||||
|
response = TestableResponse.new(
|
||||||
|
client_data_json: client_data_json,
|
||||||
|
authenticator_data: @authenticator_data,
|
||||||
|
origin: @origin
|
||||||
|
)
|
||||||
|
|
||||||
error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
|
error = assert_raises(ActionPack::WebAuthn::InvalidResponseError) do
|
||||||
@response.validate!
|
response.validate!
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_equal "Challenge does not match", error.message
|
assert_match /Challenge (is invalid|has expired)/, error.message
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate! raises when origin does not match" do
|
test "validate! raises when origin does not match" do
|
||||||
@@ -82,7 +102,6 @@ class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCas
|
|||||||
response = TestableResponse.new(
|
response = TestableResponse.new(
|
||||||
client_data_json: client_data_json,
|
client_data_json: client_data_json,
|
||||||
authenticator_data: @authenticator_data,
|
authenticator_data: @authenticator_data,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +127,6 @@ class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCas
|
|||||||
response = TestableResponse.new(
|
response = TestableResponse.new(
|
||||||
client_data_json: @client_data_json,
|
client_data_json: @client_data_json,
|
||||||
authenticator_data: wrong_rp_data,
|
authenticator_data: wrong_rp_data,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -130,7 +148,6 @@ class ActionPack::WebAuthn::Authenticator::ResponseTest < ActiveSupport::TestCas
|
|||||||
response = TestableResponse.new(
|
response = TestableResponse.new(
|
||||||
client_data_json: client_data_json,
|
client_data_json: client_data_json,
|
||||||
authenticator_data: @authenticator_data,
|
authenticator_data: @authenticator_data,
|
||||||
challenge: @challenge,
|
|
||||||
origin: @origin
|
origin: @origin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user