Add a purpose to challanges
This commit is contained in:
@@ -28,14 +28,19 @@ class ActionPack::Passkey::ChallengesController < ActionController::Base
|
|||||||
private
|
private
|
||||||
def create_passkey_challenge
|
def create_passkey_challenge
|
||||||
ActionPack::WebAuthn::PublicKeyCredential::Options.new(
|
ActionPack::WebAuthn::PublicKeyCredential::Options.new(
|
||||||
challenge_expiration: challenge_expiration
|
challenge_expiration: challenge_expiration,
|
||||||
|
challenge_purpose: challenge_purpose
|
||||||
).challenge
|
).challenge
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def challenge_purpose
|
||||||
|
params[:purpose] == "registration" ? "registration" : "authentication"
|
||||||
|
end
|
||||||
|
|
||||||
def challenge_expiration
|
def challenge_expiration
|
||||||
config = Rails.configuration.action_pack.web_authn
|
config = Rails.configuration.action_pack.web_authn
|
||||||
|
|
||||||
if params[:purpose] == "registration"
|
if challenge_purpose == "registration"
|
||||||
config.creation_challenge_expiration
|
config.creation_challenge_expiration
|
||||||
else
|
else
|
||||||
config.request_challenge_expiration
|
config.request_challenge_expiration
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponse < ActionPack::WebAu
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def challenge_purpose
|
||||||
|
"authentication"
|
||||||
|
end
|
||||||
|
|
||||||
def client_data_type_must_be_get
|
def client_data_type_must_be_get
|
||||||
unless client_data["type"] == "webauthn.get"
|
unless client_data["type"] == "webauthn.get"
|
||||||
errors.add(:base, "Client data type is not webauthn.get")
|
errors.add(:base, "Client data type is not webauthn.get")
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponse < ActionPack::Web
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def challenge_purpose
|
||||||
|
"registration"
|
||||||
|
end
|
||||||
|
|
||||||
def client_data_type_must_be_create
|
def client_data_type_must_be_create
|
||||||
unless client_data["type"] == "webauthn.create"
|
unless client_data["type"] == "webauthn.create"
|
||||||
errors.add(:base, "Client data type is not webauthn.create")
|
errors.add(:base, "Client data type is not webauthn.create")
|
||||||
|
|||||||
@@ -85,13 +85,17 @@ class ActionPack::WebAuthn::Authenticator::Response
|
|||||||
|
|
||||||
signed_message = Base64.urlsafe_decode64(client_data["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, purpose: challenge_purpose)
|
||||||
errors.add(:base, "Challenge has expired")
|
errors.add(:base, "Challenge has expired")
|
||||||
end
|
end
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
errors.add(:base, "Challenge is invalid")
|
errors.add(:base, "Challenge is invalid")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def challenge_purpose
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
def origin_must_match
|
def origin_must_match
|
||||||
if origin.blank?
|
if origin.blank?
|
||||||
errors.add(:base, "Origin missing")
|
errors.add(:base, "Origin missing")
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptions < ActionPack::W
|
|||||||
attribute :exclude_credentials, default: -> { [] }
|
attribute :exclude_credentials, default: -> { [] }
|
||||||
attribute :attestation, default: :none
|
attribute :attestation, default: :none
|
||||||
attribute :challenge_expiration, default: -> { Rails.configuration.action_pack.web_authn.creation_challenge_expiration }
|
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 :id, :name, :display_name, presence: true
|
||||||
validates :resident_key, inclusion: { in: RESIDENT_KEY_OPTIONS }
|
validates :resident_key, inclusion: { in: RESIDENT_KEY_OPTIONS }
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::Options
|
|||||||
attribute :user_verification, default: :preferred
|
attribute :user_verification, default: :preferred
|
||||||
attribute :relying_party, default: -> { ActionPack::WebAuthn.relying_party }
|
attribute :relying_party, default: -> { ActionPack::WebAuthn.relying_party }
|
||||||
attribute :challenge_expiration
|
attribute :challenge_expiration
|
||||||
|
attribute :challenge_purpose
|
||||||
|
|
||||||
validates :user_verification, inclusion: { in: USER_VERIFICATION_OPTIONS }
|
validates :user_verification, inclusion: { in: USER_VERIFICATION_OPTIONS }
|
||||||
|
|
||||||
@@ -70,7 +71,8 @@ class ActionPack::WebAuthn::PublicKeyCredential::Options
|
|||||||
@challenge ||= Base64.urlsafe_encode64(
|
@challenge ||= Base64.urlsafe_encode64(
|
||||||
ActionPack::WebAuthn.challenge_verifier.generate(
|
ActionPack::WebAuthn.challenge_verifier.generate(
|
||||||
Base64.strict_encode64(SecureRandom.random_bytes(CHALLENGE_LENGTH)),
|
Base64.strict_encode64(SecureRandom.random_bytes(CHALLENGE_LENGTH)),
|
||||||
expires_in: challenge_expiration
|
expires_in: challenge_expiration,
|
||||||
|
purpose: challenge_purpose
|
||||||
),
|
),
|
||||||
padding: false
|
padding: false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
class ActionPack::WebAuthn::PublicKeyCredential::RequestOptions < ActionPack::WebAuthn::PublicKeyCredential::Options
|
class ActionPack::WebAuthn::PublicKeyCredential::RequestOptions < ActionPack::WebAuthn::PublicKeyCredential::Options
|
||||||
attribute :credentials, default: -> { [] }
|
attribute :credentials, default: -> { [] }
|
||||||
attribute :challenge_expiration, default: -> { Rails.configuration.action_pack.web_authn.request_challenge_expiration }
|
attribute :challenge_expiration, default: -> { Rails.configuration.action_pack.web_authn.request_challenge_expiration }
|
||||||
|
attribute :challenge_purpose, default: "authentication"
|
||||||
|
|
||||||
def initialize(attributes = {})
|
def initialize(attributes = {})
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ class My::PasskeyChallengesControllerTest < ActionDispatch::IntegrationTest
|
|||||||
signed_message = Base64.urlsafe_decode64(challenge)
|
signed_message = Base64.urlsafe_decode64(challenge)
|
||||||
|
|
||||||
travel Rails.configuration.action_pack.web_authn.creation_challenge_expiration - 1.second
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,10 +49,10 @@ class My::PasskeyChallengesControllerTest < ActionDispatch::IntegrationTest
|
|||||||
signed_message = Base64.urlsafe_decode64(challenge)
|
signed_message = Base64.urlsafe_decode64(challenge)
|
||||||
|
|
||||||
travel Rails.configuration.action_pack.web_authn.request_challenge_expiration - 1.second
|
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
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class My::PasskeysControllerTest < ActionDispatch::IntegrationTest
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "register a passkey" do
|
test "register a passkey" do
|
||||||
challenge = request_webauthn_challenge
|
challenge = request_webauthn_challenge(purpose: "registration")
|
||||||
|
|
||||||
assert_difference -> { identities(:kevin).passkeys.count }, 1 do
|
assert_difference -> { identities(:kevin).passkeys.count }, 1 do
|
||||||
post my_passkeys_path, params: build_attestation_params(challenge: challenge)
|
post my_passkeys_path, params: build_attestation_params(challenge: challenge)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ActionPack::WebAuthn::Authenticator::AssertionResponseTest < ActiveSupport
|
|||||||
setup do
|
setup do
|
||||||
ActionPack::WebAuthn::Current.host = "example.com"
|
ActionPack::WebAuthn::Current.host = "example.com"
|
||||||
|
|
||||||
@challenge = webauthn_challenge
|
@challenge = webauthn_challenge(purpose: "authentication")
|
||||||
@origin = "https://example.com"
|
@origin = "https://example.com"
|
||||||
@client_data_json = {
|
@client_data_json = {
|
||||||
challenge: @challenge,
|
challenge: @challenge,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ActionPack::WebAuthn::Authenticator::AttestationResponseTest < ActiveSuppo
|
|||||||
setup do
|
setup do
|
||||||
ActionPack::WebAuthn::Current.host = "example.com"
|
ActionPack::WebAuthn::Current.host = "example.com"
|
||||||
|
|
||||||
@challenge = webauthn_challenge
|
@challenge = webauthn_challenge(purpose: "registration")
|
||||||
@origin = "https://example.com"
|
@origin = "https://example.com"
|
||||||
@client_data_json = {
|
@client_data_json = {
|
||||||
challenge: @challenge,
|
challenge: @challenge,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::CreationOptionsTest < ActiveSup
|
|||||||
|
|
||||||
test "generates signed challenge containing nonce" do
|
test "generates signed challenge containing nonce" do
|
||||||
signed_message = Base64.urlsafe_decode64(@options.challenge)
|
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_not_nil nonce
|
||||||
assert_equal 32, Base64.strict_decode64(nonce).bytesize
|
assert_equal 32, Base64.strict_decode64(nonce).bytesize
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ActionPack::WebAuthn::PublicKeyCredential::RequestOptionsTest < ActiveSupp
|
|||||||
|
|
||||||
test "generates signed challenge containing nonce" do
|
test "generates signed challenge containing nonce" do
|
||||||
signed_message = Base64.urlsafe_decode64(@options.challenge)
|
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_not_nil nonce
|
||||||
assert_equal 32, Base64.strict_decode64(nonce).bytesize
|
assert_equal 32, Base64.strict_decode64(nonce).bytesize
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ module WebauthnTestHelper
|
|||||||
[ "a363666d74646e6f6e656761747453746d74a068617574684461746158a4" ].pack("H*")
|
[ "a363666d74646e6f6e656761747453746d74a068617574684461746158a4" ].pack("H*")
|
||||||
|
|
||||||
private
|
private
|
||||||
def request_webauthn_challenge
|
def request_webauthn_challenge(purpose: "authentication")
|
||||||
untenanted { post my_passkey_challenge_url }
|
untenanted { post my_passkey_challenge_url, params: { purpose: purpose } }
|
||||||
response.parsed_body["challenge"]
|
response.parsed_body["challenge"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def webauthn_challenge
|
def webauthn_challenge(purpose: nil)
|
||||||
ActionPack::WebAuthn::PublicKeyCredential::Options.new.challenge
|
ActionPack::WebAuthn::PublicKeyCredential::Options.new(challenge_purpose: purpose).challenge
|
||||||
end
|
end
|
||||||
|
|
||||||
def webauthn_private_key
|
def webauthn_private_key
|
||||||
|
|||||||
Reference in New Issue
Block a user