From 54cb1a140f1741ed600d19d378cd2800f5699004 Mon Sep 17 00:00:00 2001 From: "Stanko K.R." Date: Wed, 25 Mar 2026 17:57:37 +0100 Subject: [PATCH] Add a purpose to challanges --- lib/action_pack/passkey/challenges_controller.rb | 9 +++++++-- .../web_authn/authenticator/assertion_response.rb | 4 ++++ .../web_authn/authenticator/attestation_response.rb | 4 ++++ lib/action_pack/web_authn/authenticator/response.rb | 6 +++++- .../web_authn/public_key_credential/creation_options.rb | 1 + .../web_authn/public_key_credential/options.rb | 4 +++- .../web_authn/public_key_credential/request_options.rb | 1 + .../controllers/my/passkey_challenges_controller_test.rb | 8 ++++---- test/controllers/my/passkeys_controller_test.rb | 2 +- .../web_authn/authenticator/assertion_response_test.rb | 2 +- .../web_authn/authenticator/attestation_response_test.rb | 2 +- .../public_key_credential/creation_options_test.rb | 2 +- .../public_key_credential/request_options_test.rb | 2 +- test/test_helpers/webauthn_test_helper.rb | 8 ++++---- 14 files changed, 38 insertions(+), 17 deletions(-) diff --git a/lib/action_pack/passkey/challenges_controller.rb b/lib/action_pack/passkey/challenges_controller.rb index 39e8bbf32..c9e321b9f 100644 --- a/lib/action_pack/passkey/challenges_controller.rb +++ b/lib/action_pack/passkey/challenges_controller.rb @@ -28,14 +28,19 @@ class ActionPack::Passkey::ChallengesController < ActionController::Base private def create_passkey_challenge ActionPack::WebAuthn::PublicKeyCredential::Options.new( - challenge_expiration: challenge_expiration + challenge_expiration: challenge_expiration, + challenge_purpose: challenge_purpose ).challenge end + def challenge_purpose + params[:purpose] == "registration" ? "registration" : "authentication" + end + def challenge_expiration config = Rails.configuration.action_pack.web_authn - if params[:purpose] == "registration" + if challenge_purpose == "registration" config.creation_challenge_expiration else config.request_challenge_expiration diff --git a/lib/action_pack/web_authn/authenticator/assertion_response.rb b/lib/action_pack/web_authn/authenticator/assertion_response.rb index b8f1292ec..eb3984612 100644 --- a/lib/action_pack/web_authn/authenticator/assertion_response.rb +++ b/lib/action_pack/web_authn/authenticator/assertion_response.rb @@ -47,6 +47,10 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponse < ActionPack::WebAu end private + def challenge_purpose + "authentication" + end + def client_data_type_must_be_get unless client_data["type"] == "webauthn.get" errors.add(:base, "Client data type is not webauthn.get") diff --git a/lib/action_pack/web_authn/authenticator/attestation_response.rb b/lib/action_pack/web_authn/authenticator/attestation_response.rb index 42ddda86f..da3381d0b 100644 --- a/lib/action_pack/web_authn/authenticator/attestation_response.rb +++ b/lib/action_pack/web_authn/authenticator/attestation_response.rb @@ -49,6 +49,10 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponse < ActionPack::Web end private + def challenge_purpose + "registration" + end + def client_data_type_must_be_create unless client_data["type"] == "webauthn.create" errors.add(:base, "Client data type is not webauthn.create") diff --git a/lib/action_pack/web_authn/authenticator/response.rb b/lib/action_pack/web_authn/authenticator/response.rb index fabb07815..3bc37db2a 100644 --- a/lib/action_pack/web_authn/authenticator/response.rb +++ b/lib/action_pack/web_authn/authenticator/response.rb @@ -85,13 +85,17 @@ class ActionPack::WebAuthn::Authenticator::Response signed_message = Base64.urlsafe_decode64(client_data["challenge"]) - unless ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + unless ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: challenge_purpose) errors.add(:base, "Challenge has expired") end rescue ArgumentError errors.add(:base, "Challenge is invalid") end + def challenge_purpose + nil + end + def origin_must_match if origin.blank? errors.add(:base, "Origin missing") diff --git a/lib/action_pack/web_authn/public_key_credential/creation_options.rb b/lib/action_pack/web_authn/public_key_credential/creation_options.rb index 3b33d04d0..345348037 100644 --- a/lib/action_pack/web_authn/public_key_credential/creation_options.rb +++ b/lib/action_pack/web_authn/public_key_credential/creation_options.rb @@ -53,6 +53,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptions < ActionPack::W attribute :exclude_credentials, default: -> { [] } attribute :attestation, default: :none attribute :challenge_expiration, default: -> { Rails.configuration.action_pack.web_authn.creation_challenge_expiration } + attribute :challenge_purpose, default: "registration" validates :id, :name, :display_name, presence: true validates :resident_key, inclusion: { in: RESIDENT_KEY_OPTIONS } diff --git a/lib/action_pack/web_authn/public_key_credential/options.rb b/lib/action_pack/web_authn/public_key_credential/options.rb index 0d3d107a8..1eee7ee96 100644 --- a/lib/action_pack/web_authn/public_key_credential/options.rb +++ b/lib/action_pack/web_authn/public_key_credential/options.rb @@ -36,6 +36,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::Options attribute :user_verification, default: :preferred attribute :relying_party, default: -> { ActionPack::WebAuthn.relying_party } attribute :challenge_expiration + attribute :challenge_purpose validates :user_verification, inclusion: { in: USER_VERIFICATION_OPTIONS } @@ -70,7 +71,8 @@ class ActionPack::WebAuthn::PublicKeyCredential::Options @challenge ||= Base64.urlsafe_encode64( ActionPack::WebAuthn.challenge_verifier.generate( Base64.strict_encode64(SecureRandom.random_bytes(CHALLENGE_LENGTH)), - expires_in: challenge_expiration + expires_in: challenge_expiration, + purpose: challenge_purpose ), padding: false ) diff --git a/lib/action_pack/web_authn/public_key_credential/request_options.rb b/lib/action_pack/web_authn/public_key_credential/request_options.rb index 99f06ccf4..3c1002312 100644 --- a/lib/action_pack/web_authn/public_key_credential/request_options.rb +++ b/lib/action_pack/web_authn/public_key_credential/request_options.rb @@ -26,6 +26,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::RequestOptions < ActionPack::WebAuthn::PublicKeyCredential::Options attribute :credentials, default: -> { [] } attribute :challenge_expiration, default: -> { Rails.configuration.action_pack.web_authn.request_challenge_expiration } + attribute :challenge_purpose, default: "authentication" def initialize(attributes = {}) super diff --git a/test/controllers/my/passkey_challenges_controller_test.rb b/test/controllers/my/passkey_challenges_controller_test.rb index caf0c5fa1..b13c3c602 100644 --- a/test/controllers/my/passkey_challenges_controller_test.rb +++ b/test/controllers/my/passkey_challenges_controller_test.rb @@ -32,10 +32,10 @@ class My::PasskeyChallengesControllerTest < ActionDispatch::IntegrationTest signed_message = Base64.urlsafe_decode64(challenge) travel Rails.configuration.action_pack.web_authn.creation_challenge_expiration - 1.second - assert ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + assert ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: "registration") travel 2.seconds - assert_nil ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + assert_nil ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: "registration") end end @@ -49,10 +49,10 @@ class My::PasskeyChallengesControllerTest < ActionDispatch::IntegrationTest signed_message = Base64.urlsafe_decode64(challenge) travel Rails.configuration.action_pack.web_authn.request_challenge_expiration - 1.second - assert ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + assert ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: "authentication") travel 2.seconds - assert_nil ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + assert_nil ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: "authentication") end end end diff --git a/test/controllers/my/passkeys_controller_test.rb b/test/controllers/my/passkeys_controller_test.rb index f6b343728..591e4f06f 100644 --- a/test/controllers/my/passkeys_controller_test.rb +++ b/test/controllers/my/passkeys_controller_test.rb @@ -13,7 +13,7 @@ class My::PasskeysControllerTest < ActionDispatch::IntegrationTest end test "register a passkey" do - challenge = request_webauthn_challenge + challenge = request_webauthn_challenge(purpose: "registration") assert_difference -> { identities(:kevin).passkeys.count }, 1 do post my_passkeys_path, params: build_attestation_params(challenge: challenge) diff --git a/test/lib/action_pack/web_authn/authenticator/assertion_response_test.rb b/test/lib/action_pack/web_authn/authenticator/assertion_response_test.rb index abdad8122..9a02a9ad7 100644 --- a/test/lib/action_pack/web_authn/authenticator/assertion_response_test.rb +++ b/test/lib/action_pack/web_authn/authenticator/assertion_response_test.rb @@ -6,7 +6,7 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport setup do ActionPack::WebAuthn::Current.host = "example.com" - @challenge = webauthn_challenge + @challenge = webauthn_challenge(purpose: "authentication") @origin = "https://example.com" @client_data_json = { challenge: @challenge, diff --git a/test/lib/action_pack/web_authn/authenticator/attestation_response_test.rb b/test/lib/action_pack/web_authn/authenticator/attestation_response_test.rb index 727cd9c5e..f7889dc29 100644 --- a/test/lib/action_pack/web_authn/authenticator/attestation_response_test.rb +++ b/test/lib/action_pack/web_authn/authenticator/attestation_response_test.rb @@ -37,7 +37,7 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo setup do ActionPack::WebAuthn::Current.host = "example.com" - @challenge = webauthn_challenge + @challenge = webauthn_challenge(purpose: "registration") @origin = "https://example.com" @client_data_json = { challenge: @challenge, diff --git a/test/lib/action_pack/web_authn/public_key_credential/creation_options_test.rb b/test/lib/action_pack/web_authn/public_key_credential/creation_options_test.rb index 9c9b37ea0..45896b96c 100644 --- a/test/lib/action_pack/web_authn/public_key_credential/creation_options_test.rb +++ b/test/lib/action_pack/web_authn/public_key_credential/creation_options_test.rb @@ -24,7 +24,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptionsTest < ActiveSup test "generates signed challenge containing nonce" do signed_message = Base64.urlsafe_decode64(@options.challenge) - nonce = ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + nonce = ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: "registration") assert_not_nil nonce assert_equal 32, Base64.strict_decode64(nonce).bytesize diff --git a/test/lib/action_pack/web_authn/public_key_credential/request_options_test.rb b/test/lib/action_pack/web_authn/public_key_credential/request_options_test.rb index d9631d73e..36a07d799 100644 --- a/test/lib/action_pack/web_authn/public_key_credential/request_options_test.rb +++ b/test/lib/action_pack/web_authn/public_key_credential/request_options_test.rb @@ -24,7 +24,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::RequestOptionsTest < ActiveSupp test "generates signed challenge containing nonce" do signed_message = Base64.urlsafe_decode64(@options.challenge) - nonce = ActionPack::WebAuthn.challenge_verifier.verified(signed_message) + nonce = ActionPack::WebAuthn.challenge_verifier.verified(signed_message, purpose: "authentication") assert_not_nil nonce assert_equal 32, Base64.strict_decode64(nonce).bytesize diff --git a/test/test_helpers/webauthn_test_helper.rb b/test/test_helpers/webauthn_test_helper.rb index 2c530aef6..e5f20d142 100644 --- a/test/test_helpers/webauthn_test_helper.rb +++ b/test/test_helpers/webauthn_test_helper.rb @@ -20,13 +20,13 @@ module WebauthnTestHelper [ "a363666d74646e6f6e656761747453746d74a068617574684461746158a4" ].pack("H*") private - def request_webauthn_challenge - untenanted { post my_passkey_challenge_url } + def request_webauthn_challenge(purpose: "authentication") + untenanted { post my_passkey_challenge_url, params: { purpose: purpose } } response.parsed_body["challenge"] end - def webauthn_challenge - ActionPack::WebAuthn::PublicKeyCredential::Options.new.challenge + def webauthn_challenge(purpose: nil) + ActionPack::WebAuthn::PublicKeyCredential::Options.new(challenge_purpose: purpose).challenge end def webauthn_private_key