WIP: Rename Webauthn to DiscourseWebauthn (#23077)

This commit is contained in:
Penar Musaraj 2023-08-18 08:39:10 -04:00 committed by GitHub
parent 16c6ab8661
commit 10c6b2a0c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 135 additions and 88 deletions

View File

@ -344,9 +344,9 @@ class SessionController < ApplicationController
end end
if matched_user&.security_keys_enabled? if matched_user&.security_keys_enabled?
Webauthn.stage_challenge(matched_user, secure_session) DiscourseWebauthn.stage_challenge(matched_user, secure_session)
response.merge!( response.merge!(
Webauthn.allowed_credentials(matched_user, secure_session).merge( DiscourseWebauthn.allowed_credentials(matched_user, secure_session).merge(
security_key_required: true, security_key_required: true,
), ),
) )
@ -433,8 +433,8 @@ class SessionController < ApplicationController
allowed_methods: challenge[:allowed_methods], allowed_methods: challenge[:allowed_methods],
) )
if user.security_keys_enabled? if user.security_keys_enabled?
Webauthn.stage_challenge(user, secure_session) DiscourseWebauthn.stage_challenge(user, secure_session)
json.merge!(Webauthn.allowed_credentials(user, secure_session)) json.merge!(DiscourseWebauthn.allowed_credentials(user, secure_session))
json[:security_keys_enabled] = true json[:security_keys_enabled] = true
else else
json[:security_keys_enabled] = false json[:security_keys_enabled] = false
@ -660,8 +660,8 @@ class SessionController < ApplicationController
if !second_factor_authentication_result.ok if !second_factor_authentication_result.ok
failure_payload = second_factor_authentication_result.to_h failure_payload = second_factor_authentication_result.to_h
if user.security_keys_enabled? if user.security_keys_enabled?
Webauthn.stage_challenge(user, secure_session) DiscourseWebauthn.stage_challenge(user, secure_session)
failure_payload.merge!(Webauthn.allowed_credentials(user, secure_session)) failure_payload.merge!(DiscourseWebauthn.allowed_credentials(user, secure_session))
end end
@second_factor_failure_payload = failed_json.merge(failure_payload) @second_factor_failure_payload = failed_json.merge(failure_payload)
return second_factor_authentication_result return second_factor_authentication_result

View File

@ -813,11 +813,11 @@ class UsersController < ApplicationController
format.html do format.html do
return render "password_reset", layout: "no_ember" if @error return render "password_reset", layout: "no_ember" if @error
Webauthn.stage_challenge(@user, secure_session) DiscourseWebauthn.stage_challenge(@user, secure_session)
store_preloaded( store_preloaded(
"password_reset", "password_reset",
MultiJson.dump( MultiJson.dump(
security_params.merge(Webauthn.allowed_credentials(@user, secure_session)), security_params.merge(DiscourseWebauthn.allowed_credentials(@user, secure_session)),
), ),
) )
@ -827,8 +827,9 @@ class UsersController < ApplicationController
format.json do format.json do
return render json: { message: @error } if @error return render json: { message: @error } if @error
Webauthn.stage_challenge(@user, secure_session) DiscourseWebauthn.stage_challenge(@user, secure_session)
render json: security_params.merge(Webauthn.allowed_credentials(@user, secure_session)) render json:
security_params.merge(DiscourseWebauthn.allowed_credentials(@user, secure_session))
end end
end end
end end
@ -895,7 +896,7 @@ class UsersController < ApplicationController
format.html do format.html do
return render "password_reset", layout: "no_ember" if @error return render "password_reset", layout: "no_ember" if @error
Webauthn.stage_challenge(@user, secure_session) DiscourseWebauthn.stage_challenge(@user, secure_session)
security_params = { security_params = {
is_developer: UsernameCheckerService.is_developer?(@user.email), is_developer: UsernameCheckerService.is_developer?(@user.email),
@ -904,7 +905,7 @@ class UsersController < ApplicationController
security_key_required: @user.security_keys_enabled?, security_key_required: @user.security_keys_enabled?,
backup_enabled: @user.backup_codes_enabled?, backup_enabled: @user.backup_codes_enabled?,
multiple_second_factor_methods: @user.has_multiple_second_factor_methods?, multiple_second_factor_methods: @user.has_multiple_second_factor_methods?,
}.merge(Webauthn.allowed_credentials(@user, secure_session)) }.merge(DiscourseWebauthn.allowed_credentials(@user, secure_session))
store_preloaded("password_reset", MultiJson.dump(security_params)) store_preloaded("password_reset", MultiJson.dump(security_params))
@ -1545,13 +1546,13 @@ class UsersController < ApplicationController
end end
def create_second_factor_security_key def create_second_factor_security_key
challenge_session = Webauthn.stage_challenge(current_user, secure_session) challenge_session = DiscourseWebauthn.stage_challenge(current_user, secure_session)
render json: render json:
success_json.merge( success_json.merge(
challenge: challenge_session.challenge, challenge: challenge_session.challenge,
rp_id: challenge_session.rp_id, rp_id: challenge_session.rp_id,
rp_name: challenge_session.rp_name, rp_name: challenge_session.rp_name,
supported_algorithms: ::Webauthn::SUPPORTED_ALGORITHMS, supported_algorithms: ::DiscourseWebauthn::SUPPORTED_ALGORITHMS,
user_secure_id: current_user.create_or_fetch_secure_identifier, user_secure_id: current_user.create_or_fetch_secure_identifier,
existing_active_credential_ids: existing_active_credential_ids:
current_user.second_factor_security_key_credential_ids, current_user.second_factor_security_key_credential_ids,
@ -1563,15 +1564,15 @@ class UsersController < ApplicationController
params.require(:attestation) params.require(:attestation)
params.require(:clientData) params.require(:clientData)
::Webauthn::SecurityKeyRegistrationService.new( ::DiscourseWebauthn::SecurityKeyRegistrationService.new(
current_user, current_user,
params, params,
challenge: Webauthn.challenge(current_user, secure_session), challenge: DiscourseWebauthn.challenge(current_user, secure_session),
rp_id: Webauthn.rp_id(current_user, secure_session), rp_id: DiscourseWebauthn.rp_id(current_user, secure_session),
origin: Discourse.base_url, origin: Discourse.base_url,
).register_second_factor_security_key ).register_second_factor_security_key
render json: success_json render json: success_json
rescue ::Webauthn::SecurityKeyError => err rescue ::DiscourseWebauthn::SecurityKeyError => err
render json: failed_json.merge(error: err.message) render json: failed_json.merge(error: err.message)
end end

View File

@ -127,11 +127,11 @@ class UsersEmailController < ApplicationController
else else
@show_second_factor = true if @user.totp_enabled? @show_second_factor = true if @user.totp_enabled?
if @user.security_keys_enabled? if @user.security_keys_enabled?
Webauthn.stage_challenge(@user, secure_session) DiscourseWebauthn.stage_challenge(@user, secure_session)
@show_security_key = params[:show_totp].to_s == "true" ? false : true @show_security_key = params[:show_totp].to_s == "true" ? false : true
@security_key_challenge = Webauthn.challenge(@user, secure_session) @security_key_challenge = DiscourseWebauthn.challenge(@user, secure_session)
@security_key_allowed_credential_ids = @security_key_allowed_credential_ids =
Webauthn.allowed_credentials(@user, secure_session)[:allowed_credential_ids] DiscourseWebauthn.allowed_credentials(@user, secure_session)[:allowed_credential_ids]
end end
end end

View File

@ -146,7 +146,7 @@ module SecondFactorManager
# if we have gotten down to this point without being # if we have gotten down to this point without being
# OK or invalid something has gone very weird. # OK or invalid something has gone very weird.
invalid_second_factor_method_result invalid_second_factor_method_result
rescue ::Webauthn::SecurityKeyError => err rescue ::DiscourseWebauthn::SecurityKeyError => err
invalid_security_key_result(err.message) invalid_security_key_result(err.message)
end end
@ -163,11 +163,11 @@ module SecondFactorManager
end end
def authenticate_security_key(secure_session, security_key_credential) def authenticate_security_key(secure_session, security_key_credential)
::Webauthn::SecurityKeyAuthenticationService.new( ::DiscourseWebauthn::SecurityKeyAuthenticationService.new(
self, self,
security_key_credential, security_key_credential,
challenge: Webauthn.challenge(self, secure_session), challenge: DiscourseWebauthn.challenge(self, secure_session),
rp_id: Webauthn.rp_id(self, secure_session), rp_id: DiscourseWebauthn.rp_id(self, secure_session),
origin: Discourse.base_url, origin: Discourse.base_url,
).authenticate_security_key ).authenticate_security_key
end end

View File

@ -4,7 +4,7 @@ require "webauthn/security_key_base_validation_service"
require "webauthn/security_key_registration_service" require "webauthn/security_key_registration_service"
require "webauthn/security_key_authentication_service" require "webauthn/security_key_authentication_service"
module Webauthn module DiscourseWebauthn
ACCEPTABLE_REGISTRATION_TYPE = "webauthn.create" ACCEPTABLE_REGISTRATION_TYPE = "webauthn.create"
ACCEPTABLE_AUTHENTICATION_TYPE = "webauthn.get" ACCEPTABLE_AUTHENTICATION_TYPE = "webauthn.get"
@ -51,7 +51,7 @@ module Webauthn
# they must respond with a valid webauthn response and # they must respond with a valid webauthn response and
# credentials. # credentials.
def self.stage_challenge(user, secure_session) def self.stage_challenge(user, secure_session)
::Webauthn::ChallengeGenerator.generate.commit_to_session(secure_session, user) ::DiscourseWebauthn::ChallengeGenerator.generate.commit_to_session(secure_session, user)
end end
def self.allowed_credentials(user, secure_session) def self.allowed_credentials(user, secure_session)
@ -60,19 +60,55 @@ module Webauthn
{ {
allowed_credential_ids: credential_ids, allowed_credential_ids: credential_ids,
challenge: challenge:
secure_session[Webauthn::ChallengeGenerator::ChallengeSession.session_challenge_key(user)], secure_session[
DiscourseWebauthn::ChallengeGenerator::ChallengeSession.session_challenge_key(user)
],
} }
end end
def self.rp_id(user, secure_session) def self.rp_id(user, secure_session)
secure_session[Webauthn::ChallengeGenerator::ChallengeSession.session_rp_id_key(user)] secure_session[DiscourseWebauthn::ChallengeGenerator::ChallengeSession.session_rp_id_key(user)]
end end
def self.rp_name(user, secure_session) def self.rp_name(user, secure_session)
secure_session[Webauthn::ChallengeGenerator::ChallengeSession.session_rp_name_key(user)] secure_session[
DiscourseWebauthn::ChallengeGenerator::ChallengeSession.session_rp_name_key(user)
]
end end
def self.challenge(user, secure_session) def self.challenge(user, secure_session)
secure_session[Webauthn::ChallengeGenerator::ChallengeSession.session_challenge_key(user)] secure_session[
DiscourseWebauthn::ChallengeGenerator::ChallengeSession.session_challenge_key(user)
]
end
def self.validate_first_factor_key(key)
pp key
webauthn_credential = DiscourseWebauthn::Credential.from_get(key)
p "webauthn_credential"
pp webauthn_credential
# stored_credential = user.credentials.find_by(webauthn_id: webauthn_credential.id)
# begin
# webauthn_credential.verify(
# session[:authentication_challenge],
# public_key: stored_credential.public_key,
# sign_count: stored_credential.sign_count
# )
# # Update the stored credential sign count with the value from `webauthn_credential.sign_count`
# stored_credential.update!(sign_count: webauthn_credential.sign_count)
# # Continue with successful sign in or 2FA verification...
# rescue ::WebAuthn::SignCountVerificationError => e
# # Cryptographic verification of the authenticator data succeeded, but the signature counter was less then or equal
# # to the stored value. This can have several reasons and depending on your risk tolerance you can choose to fail or
# # pass authentication. For more information see https://www.w3.org/TR/webauthn/#sign-counter
# pp e
# rescue ::WebAuthn::Error => e
# # Handle error
# end
end end
end end

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Webauthn module DiscourseWebauthn
class ChallengeGenerator class ChallengeGenerator
class ChallengeSession class ChallengeSession
attr_reader :challenge, :rp_id, :rp_name attr_reader :challenge, :rp_id, :rp_name

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cose" require "cose"
module Webauthn module DiscourseWebauthn
class SecurityKeyAuthenticationService < SecurityKeyBaseValidationService class SecurityKeyAuthenticationService < SecurityKeyBaseValidationService
## ##
# See https://w3c.github.io/webauthn/#sctn-verifying-assertion for # See https://w3c.github.io/webauthn/#sctn-verifying-assertion for
@ -30,7 +30,7 @@ module Webauthn
client_data client_data
# 8. Verify that the value of C.type is the string webauthn.get. # 8. Verify that the value of C.type is the string webauthn.get.
validate_webauthn_type(::Webauthn::ACCEPTABLE_AUTHENTICATION_TYPE) validate_webauthn_type(::DiscourseWebauthn::ACCEPTABLE_AUTHENTICATION_TYPE)
# 9. Verify that the value of C.challenge equals the base64url encoding of options.challenge. # 9. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
validate_challenge validate_challenge

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module Webauthn module DiscourseWebauthn
class SecurityKeyBaseValidationService class SecurityKeyBaseValidationService
def initialize(current_user, params, challenge_params) def initialize(current_user, params, challenge_params)
@current_user = current_user @current_user = current_user

View File

@ -2,7 +2,7 @@
require "cbor" require "cbor"
require "cose" require "cose"
module Webauthn module DiscourseWebauthn
class SecurityKeyRegistrationService < SecurityKeyBaseValidationService class SecurityKeyRegistrationService < SecurityKeyBaseValidationService
## ##
# See https://w3c.github.io/webauthn/#sctn-registering-a-new-credential for # See https://w3c.github.io/webauthn/#sctn-registering-a-new-credential for
@ -10,7 +10,7 @@ module Webauthn
# place in the step flow to make the process clearer. # place in the step flow to make the process clearer.
def register_second_factor_security_key def register_second_factor_security_key
# 4. Verify that the value of C.type is webauthn.create. # 4. Verify that the value of C.type is webauthn.create.
validate_webauthn_type(::Webauthn::ACCEPTABLE_REGISTRATION_TYPE) validate_webauthn_type(::DiscourseWebauthn::ACCEPTABLE_REGISTRATION_TYPE)
# 5. Verify that the value of C.challenge equals the base64url encoding of options.challenge. # 5. Verify that the value of C.challenge equals the base64url encoding of options.challenge.
validate_challenge validate_challenge
@ -51,7 +51,7 @@ module Webauthn
# codes. # codes.
credential_public_key, credential_public_key_bytes, credential_id = credential_public_key, credential_public_key_bytes, credential_id =
extract_public_key_and_credential_from_attestation(auth_data) extract_public_key_and_credential_from_attestation(auth_data)
if ::Webauthn::SUPPORTED_ALGORITHMS.exclude?(credential_public_key.alg) if ::DiscourseWebauthn::SUPPORTED_ALGORITHMS.exclude?(credential_public_key.alg)
raise( raise(
UnsupportedPublicKeyAlgorithmError, UnsupportedPublicKeyAlgorithmError,
I18n.t("webauthn.validation.unsupported_public_key_algorithm_error"), I18n.t("webauthn.validation.unsupported_public_key_algorithm_error"),
@ -72,7 +72,7 @@ module Webauthn
# name [WebAuthn-Registries]. # name [WebAuthn-Registries].
# 16. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, # 16. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature,
# by using the attestation statement format fmts verification procedure given attStmt, authData and hash. # by using the attestation statement format fmts verification procedure given attStmt, authData and hash.
if ::Webauthn::VALID_ATTESTATION_FORMATS.exclude?(attestation["fmt"]) || if ::DiscourseWebauthn::VALID_ATTESTATION_FORMATS.exclude?(attestation["fmt"]) ||
attestation["fmt"] != "none" attestation["fmt"] != "none"
raise( raise(
UnsupportedAttestationFormatError, UnsupportedAttestationFormatError,

View File

@ -182,7 +182,7 @@ RSpec.describe SecondFactorManager do
before do before do
disable_totp disable_totp
simulate_localhost_webauthn_challenge simulate_localhost_webauthn_challenge
Webauthn.stage_challenge(user, secure_session) DiscourseWebauthn.stage_challenge(user, secure_session)
end end
context "when security key params are valid" do context "when security key params are valid" do
@ -264,7 +264,7 @@ RSpec.describe SecondFactorManager do
before do before do
simulate_localhost_webauthn_challenge simulate_localhost_webauthn_challenge
Webauthn.stage_challenge(user, secure_session) DiscourseWebauthn.stage_challenge(user, secure_session)
end end
context "when method selected is invalid" do context "when method selected is invalid" do
@ -312,7 +312,7 @@ RSpec.describe SecondFactorManager do
before do before do
simulate_localhost_webauthn_challenge simulate_localhost_webauthn_challenge
Webauthn.stage_challenge(user, secure_session) DiscourseWebauthn.stage_challenge(user, secure_session)
end end
context "when security key params are valid" do context "when security key params are valid" do

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe Webauthn::ChallengeGenerator do RSpec.describe DiscourseWebauthn::ChallengeGenerator do
it "generates a Webauthn::ChallengeGenerator::ChallengeSession with correct params" do it "generates a DiscourseWebauthn::ChallengeGenerator::ChallengeSession with correct params" do
session = Webauthn::ChallengeGenerator.generate session = DiscourseWebauthn::ChallengeGenerator.generate
expect(session).to be_a(Webauthn::ChallengeGenerator::ChallengeSession) expect(session).to be_a(DiscourseWebauthn::ChallengeGenerator::ChallengeSession)
expect(session.challenge).not_to eq(nil) expect(session.challenge).not_to eq(nil)
expect(session.rp_id).to eq(Discourse.current_hostname) expect(session.rp_id).to eq(Discourse.current_hostname)
expect(session.rp_name).to eq(SiteSetting.title) expect(session.rp_name).to eq(SiteSetting.title)
@ -15,7 +15,7 @@ RSpec.describe Webauthn::ChallengeGenerator do
it "stores the challenge, rp id, and rp name in the provided session object" do it "stores the challenge, rp id, and rp name in the provided session object" do
secure_session = {} secure_session = {}
generated_session = Webauthn::ChallengeGenerator.generate generated_session = DiscourseWebauthn::ChallengeGenerator.generate
generated_session.commit_to_session(secure_session, user) generated_session.commit_to_session(secure_session, user)
expect(secure_session["staged-webauthn-challenge-#{user&.id}"]).to eq( expect(secure_session["staged-webauthn-challenge-#{user&.id}"]).to eq(

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require "webauthn" require "discourse_webauthn"
require "webauthn/security_key_registration_service" require "webauthn/security_key_registration_service"
## ##
@ -37,7 +37,7 @@ require "webauthn/security_key_registration_service"
# #
# The origin params just need to be whatever your localhost URL for Discourse is. # The origin params just need to be whatever your localhost URL for Discourse is.
RSpec.describe Webauthn::SecurityKeyAuthenticationService do RSpec.describe DiscourseWebauthn::SecurityKeyAuthenticationService do
subject(:service) { described_class.new(current_user, params, challenge_params) } subject(:service) { described_class.new(current_user, params, challenge_params) }
let(:security_key_user) { current_user } let(:security_key_user) { current_user }
@ -118,7 +118,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises a NotFoundError" do it "raises a NotFoundError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::NotFoundError, DiscourseWebauthn::NotFoundError,
I18n.t("webauthn.validation.not_found_error"), I18n.t("webauthn.validation.not_found_error"),
) )
end end
@ -129,7 +129,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises an OwnershipError" do it "raises an OwnershipError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::OwnershipError, DiscourseWebauthn::OwnershipError,
I18n.t("webauthn.validation.ownership_error"), I18n.t("webauthn.validation.ownership_error"),
) )
end end
@ -140,7 +140,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises an InvalidTypeError" do it "raises an InvalidTypeError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::InvalidTypeError, DiscourseWebauthn::InvalidTypeError,
I18n.t("webauthn.validation.invalid_type_error"), I18n.t("webauthn.validation.invalid_type_error"),
) )
end end
@ -151,7 +151,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises a ChallengeMismatchError" do it "raises a ChallengeMismatchError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::ChallengeMismatchError, DiscourseWebauthn::ChallengeMismatchError,
I18n.t("webauthn.validation.challenge_mismatch_error"), I18n.t("webauthn.validation.challenge_mismatch_error"),
) )
end end
@ -162,7 +162,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises a InvalidOriginError" do it "raises a InvalidOriginError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::InvalidOriginError, DiscourseWebauthn::InvalidOriginError,
I18n.t("webauthn.validation.invalid_origin_error"), I18n.t("webauthn.validation.invalid_origin_error"),
) )
end end
@ -173,7 +173,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises a InvalidRelyingPartyIdError" do it "raises a InvalidRelyingPartyIdError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::InvalidRelyingPartyIdError, DiscourseWebauthn::InvalidRelyingPartyIdError,
I18n.t("webauthn.validation.invalid_relying_party_id_error"), I18n.t("webauthn.validation.invalid_relying_party_id_error"),
) )
end end
@ -184,7 +184,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises a PublicKeyError" do it "raises a PublicKeyError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::PublicKeyError, DiscourseWebauthn::PublicKeyError,
I18n.t("webauthn.validation.public_key_error"), I18n.t("webauthn.validation.public_key_error"),
) )
end end
@ -195,7 +195,7 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
it "raises a UnknownCOSEAlgorithmError" do it "raises a UnknownCOSEAlgorithmError" do
expect { service.authenticate_security_key }.to raise_error( expect { service.authenticate_security_key }.to raise_error(
Webauthn::UnknownCOSEAlgorithmError, DiscourseWebauthn::UnknownCOSEAlgorithmError,
I18n.t("webauthn.validation.unknown_cose_algorithm_error"), I18n.t("webauthn.validation.unknown_cose_algorithm_error"),
) )
end end
@ -237,6 +237,8 @@ RSpec.describe Webauthn::SecurityKeyAuthenticationService do
end end
it "all supported algorithms are implemented" do it "all supported algorithms are implemented" do
Webauthn::SUPPORTED_ALGORITHMS.each { |alg| expect(COSE::Algorithm.find(alg)).not_to be_nil } DiscourseWebauthn::SUPPORTED_ALGORITHMS.each do |alg|
expect(COSE::Algorithm.find(alg)).not_to be_nil
end
end end
end end

View File

@ -1,8 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require "webauthn" require "discourse_webauthn"
require "webauthn/security_key_registration_service" require "webauthn/security_key_registration_service"
RSpec.describe Webauthn::SecurityKeyRegistrationService do RSpec.describe DiscourseWebauthn::SecurityKeyRegistrationService do
subject(:service) { described_class.new(current_user, params, challenge_params) } subject(:service) { described_class.new(current_user, params, challenge_params) }
let(:client_data_challenge) { Base64.encode64(challenge) } let(:client_data_challenge) { Base64.encode64(challenge) }
@ -42,7 +42,7 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
it "raises an InvalidTypeError" do it "raises an InvalidTypeError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::InvalidTypeError, DiscourseWebauthn::InvalidTypeError,
I18n.t("webauthn.validation.invalid_type_error"), I18n.t("webauthn.validation.invalid_type_error"),
) )
end end
@ -53,7 +53,7 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
it "raises a ChallengeMismatchError" do it "raises a ChallengeMismatchError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::ChallengeMismatchError, DiscourseWebauthn::ChallengeMismatchError,
I18n.t("webauthn.validation.challenge_mismatch_error"), I18n.t("webauthn.validation.challenge_mismatch_error"),
) )
end end
@ -64,7 +64,7 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
it "raises a InvalidOriginError" do it "raises a InvalidOriginError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::InvalidOriginError, DiscourseWebauthn::InvalidOriginError,
I18n.t("webauthn.validation.invalid_origin_error"), I18n.t("webauthn.validation.invalid_origin_error"),
) )
end end
@ -75,7 +75,7 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
it "raises a InvalidRelyingPartyIdError" do it "raises a InvalidRelyingPartyIdError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::InvalidRelyingPartyIdError, DiscourseWebauthn::InvalidRelyingPartyIdError,
I18n.t("webauthn.validation.invalid_relying_party_id_error"), I18n.t("webauthn.validation.invalid_relying_party_id_error"),
) )
end end
@ -83,35 +83,39 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
context "when the public key algorithm is not supported by the server" do context "when the public key algorithm is not supported by the server" do
before do before do
@original_supported_alg_value = Webauthn::SUPPORTED_ALGORITHMS @original_supported_alg_value = DiscourseWebauthn::SUPPORTED_ALGORITHMS
silence_warnings { Webauthn::SUPPORTED_ALGORITHMS = [-999] } silence_warnings { DiscourseWebauthn::SUPPORTED_ALGORITHMS = [-999] }
end end
it "raises a UnsupportedPublicKeyAlgorithmError" do it "raises a UnsupportedPublicKeyAlgorithmError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::UnsupportedPublicKeyAlgorithmError, DiscourseWebauthn::UnsupportedPublicKeyAlgorithmError,
I18n.t("webauthn.validation.unsupported_public_key_algorithm_error"), I18n.t("webauthn.validation.unsupported_public_key_algorithm_error"),
) )
end end
after { silence_warnings { Webauthn::SUPPORTED_ALGORITHMS = @original_supported_alg_value } } after do
silence_warnings { DiscourseWebauthn::SUPPORTED_ALGORITHMS = @original_supported_alg_value }
end
end end
context "when the attestation format is not supported" do context "when the attestation format is not supported" do
before do before do
@original_supported_alg_value = Webauthn::VALID_ATTESTATION_FORMATS @original_supported_alg_value = DiscourseWebauthn::VALID_ATTESTATION_FORMATS
silence_warnings { Webauthn::VALID_ATTESTATION_FORMATS = ["err"] } silence_warnings { DiscourseWebauthn::VALID_ATTESTATION_FORMATS = ["err"] }
end end
it "raises a UnsupportedAttestationFormatError" do it "raises a UnsupportedAttestationFormatError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::UnsupportedAttestationFormatError, DiscourseWebauthn::UnsupportedAttestationFormatError,
I18n.t("webauthn.validation.unsupported_attestation_format_error"), I18n.t("webauthn.validation.unsupported_attestation_format_error"),
) )
end end
after do after do
silence_warnings { Webauthn::VALID_ATTESTATION_FORMATS = @original_supported_alg_value } silence_warnings do
DiscourseWebauthn::VALID_ATTESTATION_FORMATS = @original_supported_alg_value
end
end end
end end
@ -126,7 +130,7 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
# error! # error!
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::CredentialIdInUseError, DiscourseWebauthn::CredentialIdInUseError,
I18n.t("webauthn.validation.credential_id_in_use_error"), I18n.t("webauthn.validation.credential_id_in_use_error"),
) )
end end
@ -139,7 +143,7 @@ RSpec.describe Webauthn::SecurityKeyRegistrationService do
it "raises a MalformedAttestationError" do it "raises a MalformedAttestationError" do
expect { service.register_second_factor_security_key }.to raise_error( expect { service.register_second_factor_security_key }.to raise_error(
Webauthn::MalformedAttestationError, DiscourseWebauthn::MalformedAttestationError,
I18n.t("webauthn.validation.malformed_attestation_error"), I18n.t("webauthn.validation.malformed_attestation_error"),
) )
end end

View File

@ -182,7 +182,7 @@ RSpec.configure do |config|
config.include RSpecHtmlMatchers config.include RSpecHtmlMatchers
config.include IntegrationHelpers, type: :request config.include IntegrationHelpers, type: :request
config.include SystemHelpers, type: :system config.include SystemHelpers, type: :system
config.include WebauthnIntegrationHelpers config.include DiscourseWebauthnIntegrationHelpers
config.include SiteSettingsHelpers config.include SiteSettingsHelpers
config.include SidekiqHelpers config.include SidekiqHelpers
config.include UploadsHelpers config.include UploadsHelpers

View File

@ -117,8 +117,10 @@ RSpec.describe SessionController do
[user_security_key.credential_id], [user_security_key.credential_id],
) )
secure_session = SecureSession.new(session["secure_session_id"]) secure_session = SecureSession.new(session["secure_session_id"])
expect(response_body_parsed["challenge"]).to eq(Webauthn.challenge(user, secure_session)) expect(response_body_parsed["challenge"]).to eq(
expect(Webauthn.rp_id(user, secure_session)).to eq(Discourse.current_hostname) DiscourseWebauthn.challenge(user, secure_session),
)
expect(DiscourseWebauthn.rp_id(user, secure_session)).to eq(Discourse.current_hostname)
end end
end end
end end

View File

@ -459,8 +459,8 @@ RSpec.describe UsersController do
it "stages a webauthn challenge and rp-id for the user" do it "stages a webauthn challenge and rp-id for the user" do
secure_session = SecureSession.new(session["secure_session_id"]) secure_session = SecureSession.new(session["secure_session_id"])
expect(Webauthn.challenge(user1, secure_session)).not_to eq(nil) expect(DiscourseWebauthn.challenge(user1, secure_session)).not_to eq(nil)
expect(Webauthn.rp_id(user1, secure_session)).to eq(Discourse.current_hostname) expect(DiscourseWebauthn.rp_id(user1, secure_session)).to eq(Discourse.current_hostname)
end end
it "changes password with valid security key challenge and authentication" do it "changes password with valid security key challenge and authentication" do
@ -5658,13 +5658,15 @@ RSpec.describe UsersController do
create_second_factor_security_key create_second_factor_security_key
secure_session = read_secure_session secure_session = read_secure_session
response_parsed = response.parsed_body response_parsed = response.parsed_body
expect(response_parsed["challenge"]).to eq(Webauthn.challenge(user1, secure_session)) expect(response_parsed["challenge"]).to eq(DiscourseWebauthn.challenge(user1, secure_session))
expect(response_parsed["rp_id"]).to eq(Webauthn.rp_id(user1, secure_session)) expect(response_parsed["rp_id"]).to eq(DiscourseWebauthn.rp_id(user1, secure_session))
expect(response_parsed["rp_name"]).to eq(Webauthn.rp_name(user1, secure_session)) expect(response_parsed["rp_name"]).to eq(DiscourseWebauthn.rp_name(user1, secure_session))
expect(response_parsed["user_secure_id"]).to eq( expect(response_parsed["user_secure_id"]).to eq(
user1.reload.create_or_fetch_secure_identifier, user1.reload.create_or_fetch_secure_identifier,
) )
expect(response_parsed["supported_algorithms"]).to eq(::Webauthn::SUPPORTED_ALGORITHMS) expect(response_parsed["supported_algorithms"]).to eq(
::DiscourseWebauthn::SUPPORTED_ALGORITHMS,
)
end end
context "if the user has security key credentials already" do context "if the user has security key credentials already" do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
module WebauthnIntegrationHelpers module DiscourseWebauthnIntegrationHelpers
## ##
# Usage notes: # Usage notes:
# #
@ -10,8 +10,8 @@ module WebauthnIntegrationHelpers
# #
# To make this all work together you need to # To make this all work together you need to
# create a UserSecurityKey for a user using valid_security_key_data, # create a UserSecurityKey for a user using valid_security_key_data,
# and you override Webauthn::ChallengeGenerator.generate to return # and you override DiscourseWebauthn::ChallengeGenerator.generate to return
# a Webauthn::ChallengeGenerator::ChallengeSession object using # a DiscourseWebauthn::ChallengeGenerator::ChallengeSession object using
# valid_security_key_challenge_data. # valid_security_key_challenge_data.
# #
# This is because the challenge is embedded # This is because the challenge is embedded
@ -64,8 +64,8 @@ module WebauthnIntegrationHelpers
def simulate_localhost_webauthn_challenge def simulate_localhost_webauthn_challenge
stub_as_dev_localhost stub_as_dev_localhost
Webauthn::ChallengeGenerator.stubs(:generate).returns( DiscourseWebauthn::ChallengeGenerator.stubs(:generate).returns(
Webauthn::ChallengeGenerator::ChallengeSession.new( DiscourseWebauthn::ChallengeGenerator::ChallengeSession.new(
challenge: valid_security_key_challenge_data[:challenge], challenge: valid_security_key_challenge_data[:challenge],
rp_id: Discourse.current_hostname, rp_id: Discourse.current_hostname,
), ),