mirror of
https://github.com/discourse/discourse.git
synced 2024-11-24 05:50:14 +08:00
0af6c5efdc
This is part 1 of 3, split up of PR #23529. This PR refactors the webauthn code to support passkey authentication/registration. Passkeys aren't used yet, that is coming in PRs 2 and 3. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
229 lines
8.4 KiB
Ruby
229 lines
8.4 KiB
Ruby
# frozen_string_literal: true
|
|
require "discourse_webauthn"
|
|
|
|
RSpec.describe DiscourseWebauthn::RegistrationService do
|
|
subject(:service) { described_class.new(current_user, params, **options) }
|
|
|
|
let(:secure_session) { SecureSession.new("tester") }
|
|
let(:client_data_challenge) { Base64.encode64(challenge) }
|
|
let(:client_data_webauthn_type) { "webauthn.create" }
|
|
let(:client_data_origin) { "http://localhost:3000" }
|
|
let(:client_data_param) do
|
|
{
|
|
challenge: client_data_challenge,
|
|
type: client_data_webauthn_type,
|
|
origin: client_data_origin,
|
|
}
|
|
end
|
|
##
|
|
# This attestation object was sourced by manually registering
|
|
# a key with `navigator.credentials.create` and capturing the
|
|
# results in localhost. It does not have a user verification
|
|
# flag set (i.e. it is only usable as 2FA).
|
|
let(:attestation) do
|
|
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQFmvayWc8OPJ4jj4sevfxBmvUglDMZrFalyokYrdnqOVvudC0lQialaGQv72eBzJM2Qn1GfJI7lpBgFJMprisLSlAQIDJiABIVgg+23/BZux7LK0/KQgCiQGtdr51ar+vfTtHWpRtN17gOwiWCBstV918mugVBexg/rdZjTs0wN/upHFoyBiAJCaGVD8OA=="
|
|
end
|
|
let(:params) do
|
|
{
|
|
clientData: Base64.encode64(client_data_param.to_json),
|
|
attestation: attestation,
|
|
name: "My Yubikey",
|
|
}
|
|
end
|
|
##
|
|
# The above attestation was generated in localhost; Discourse.current_hostname
|
|
# returns test.localhost which we do not want
|
|
let(:options) do
|
|
{ session: secure_session, factor_type: UserSecurityKey.factor_types[:second_factor] }
|
|
end
|
|
|
|
let(:challenge) { DiscourseWebauthn.stage_challenge(current_user, secure_session).challenge }
|
|
|
|
let(:current_user) { Fabricate(:user) }
|
|
|
|
context "when the client data webauthn type is not webauthn.create" do
|
|
let(:client_data_webauthn_type) { "webauthn.explode" }
|
|
|
|
it "raises an InvalidTypeError" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::InvalidTypeError,
|
|
I18n.t("webauthn.validation.invalid_type_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the decoded challenge does not match the original challenge provided by the server" do
|
|
let(:client_data_challenge) { Base64.encode64("invalid challenge") }
|
|
|
|
it "raises a ChallengeMismatchError" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::ChallengeMismatchError,
|
|
I18n.t("webauthn.validation.challenge_mismatch_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the origin of the client data does not match the server origin" do
|
|
let(:client_data_origin) { "https://someothersite.com" }
|
|
|
|
it "raises a InvalidOriginError" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::InvalidOriginError,
|
|
I18n.t("webauthn.validation.invalid_origin_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the sha256 hash of the relaying party ID does not match the one in attestation.authData" do
|
|
it "raises a InvalidRelyingPartyIdError" do
|
|
DiscourseWebauthn.stubs(:rp_id).returns("bad_rp_id")
|
|
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::InvalidRelyingPartyIdError,
|
|
I18n.t("webauthn.validation.invalid_relying_party_id_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the public key algorithm is not supported by the server" do
|
|
before do
|
|
@original_supported_alg_value = DiscourseWebauthn::SUPPORTED_ALGORITHMS
|
|
silence_warnings { DiscourseWebauthn::SUPPORTED_ALGORITHMS = [-999] }
|
|
end
|
|
|
|
it "raises a UnsupportedPublicKeyAlgorithmError" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::UnsupportedPublicKeyAlgorithmError,
|
|
I18n.t("webauthn.validation.unsupported_public_key_algorithm_error"),
|
|
)
|
|
end
|
|
|
|
after do
|
|
silence_warnings { DiscourseWebauthn::SUPPORTED_ALGORITHMS = @original_supported_alg_value }
|
|
end
|
|
end
|
|
|
|
context "when the attestation format is not supported" do
|
|
before do
|
|
@original_supported_alg_value = DiscourseWebauthn::VALID_ATTESTATION_FORMATS
|
|
silence_warnings { DiscourseWebauthn::VALID_ATTESTATION_FORMATS = ["err"] }
|
|
end
|
|
|
|
it "raises a UnsupportedAttestationFormatError" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::UnsupportedAttestationFormatError,
|
|
I18n.t("webauthn.validation.unsupported_attestation_format_error"),
|
|
)
|
|
end
|
|
|
|
after do
|
|
silence_warnings do
|
|
DiscourseWebauthn::VALID_ATTESTATION_FORMATS = @original_supported_alg_value
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the credential id is already in use for any user" do
|
|
it "raises a CredentialIdInUseError" do
|
|
# register the key to the current user
|
|
security_key = service.register_security_key
|
|
|
|
# update the key to be on a different user
|
|
other_user = Fabricate(:user)
|
|
security_key.update(user: other_user)
|
|
|
|
# error!
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::CredentialIdInUseError,
|
|
I18n.t("webauthn.validation.credential_id_in_use_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the attestation data is malformed" do
|
|
let(:attestation) do
|
|
"blah/krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQFmvayWc8OPJ4jj4sevfxBmvUglDMZrFalyokYrdnqOVvudC0lQialaGQv72eBzJM2Qn1GfJI7lpBgFJMprisLSlAQIDJiABIVgg+23/BZux7LK0/KQgCiQGtdr51ar+vfTtHWpRtN17gOwiWCBstV918mugVBexg/rdZjTs0wN/upHFoyBiAJCaGVD8OA=="
|
|
end
|
|
|
|
it "raises a MalformedAttestationError" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::MalformedAttestationError,
|
|
I18n.t("webauthn.validation.malformed_attestation_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the user presence flag is false" do
|
|
it "raises a UserPresenceError" do
|
|
# simulate missing user presence by flipping first bit to 0
|
|
flags = "00000010"
|
|
overridenAuthData = service.send(:auth_data)
|
|
overridenAuthData[32] = [flags].pack("b*")
|
|
|
|
service.instance_variable_set(:@auth_data, overridenAuthData)
|
|
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::UserPresenceError,
|
|
I18n.t("webauthn.validation.user_presence_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
it "registers a valid second-factor key" do
|
|
key = service.register_security_key
|
|
expect(key).to be_a(UserSecurityKey)
|
|
expect(key.user).to eq(current_user)
|
|
expect(key.factor_type).to eq(UserSecurityKey.factor_types[:second_factor])
|
|
end
|
|
|
|
describe "registering a second factor key as first factor" do
|
|
let(:options) do
|
|
{ factor_type: UserSecurityKey.factor_types[:first_factor], session: secure_session }
|
|
end
|
|
|
|
it "does not work since second-factor key does not have the user verification flag" do
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::UserVerificationError,
|
|
I18n.t("webauthn.validation.user_verification_error"),
|
|
)
|
|
end
|
|
end
|
|
|
|
describe "registering a passkey" do
|
|
let(:options) do
|
|
{ factor_type: UserSecurityKey.factor_types[:first_factor], session: secure_session }
|
|
end
|
|
|
|
##
|
|
# key registered locally using
|
|
# - localhost:3000 as the origin (via an origin override in discourse_webauthn.rb)
|
|
# - frontend webauthn.create has user verification flag enabled
|
|
let(:attestation) do
|
|
"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAICRXq4sFZ9XpWZOzfJ8EguJmoEPMzNVyFMUWQfT5u1QzpQECAyYgASFYILjOiAHAwNrXkCk/tmyYRiE87QyV/15wUvhcXhr1JfwtIlggClQywgQvSxTsqV/FSK0cNHTTmuwfzzREqE6eLDmPxmI="
|
|
end
|
|
|
|
it "works with a valid key" do
|
|
key = service.register_security_key
|
|
expect(key).to be_a(UserSecurityKey)
|
|
expect(key.user).to eq(current_user)
|
|
expect(key.factor_type).to eq(UserSecurityKey.factor_types[:first_factor])
|
|
end
|
|
|
|
context "when the user verification flag in the key is false" do
|
|
it "raises a UserVerificationError" do
|
|
# simulate missing user verification by flipping third bit to 0
|
|
flags = "10000010" # correct flag sequence is "10100010"
|
|
overriden_auth_data = service.send(:auth_data)
|
|
overriden_auth_data[32] = [flags].pack("b*")
|
|
|
|
service.instance_variable_set(:@auth_data, overriden_auth_data)
|
|
|
|
expect { service.register_security_key }.to raise_error(
|
|
DiscourseWebauthn::UserVerificationError,
|
|
I18n.t("webauthn.validation.user_verification_error"),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|