WIP: Rename Webauthn to DiscourseWebauthn (#23077)
This commit is contained in:
parent
16c6ab8661
commit
10c6b2a0c2
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 fmt’s verification procedure given attStmt, authData and hash.
|
# by using the attestation statement format fmt’s 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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue