mirror of
https://github.com/discourse/discourse.git
synced 2024-12-02 03:43:40 +08:00
SECURITY: Limit name field length of TOTP authenticators and security keys
This commit is contained in:
parent
d3c29c02b9
commit
48316d75cd
|
@ -27,6 +27,7 @@
|
||||||
@value={{this.securityKeyName}}
|
@value={{this.securityKeyName}}
|
||||||
id="security-key-name"
|
id="security-key-name"
|
||||||
placeholder="security key name"
|
placeholder="security key name"
|
||||||
|
maxlength={{this.maxSecondFactorNameLength}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import I18n from "I18n";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { MAX_SECOND_FACTOR_NAME_LENGTH } from "discourse/models/user";
|
||||||
|
|
||||||
export default class SecondFactorAddSecurityKey extends Component {
|
export default class SecondFactorAddSecurityKey extends Component {
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
|
@ -16,6 +17,8 @@ export default class SecondFactorAddSecurityKey extends Component {
|
||||||
@tracked errorMessage = null;
|
@tracked errorMessage = null;
|
||||||
@tracked securityKeyName;
|
@tracked securityKeyName;
|
||||||
|
|
||||||
|
maxSecondFactorNameLength = MAX_SECOND_FACTOR_NAME_LENGTH;
|
||||||
|
|
||||||
get webauthnUnsupported() {
|
get webauthnUnsupported() {
|
||||||
return !isWebauthnSupported();
|
return !isWebauthnSupported();
|
||||||
}
|
}
|
||||||
|
@ -30,7 +33,7 @@ export default class SecondFactorAddSecurityKey extends Component {
|
||||||
} else {
|
} else {
|
||||||
key = "user.second_factor.security_key.default_name";
|
key = "user.second_factor.security_key.default_name";
|
||||||
}
|
}
|
||||||
this.securityKeyName = key;
|
this.securityKeyName = I18n.t(key);
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.args.model.secondFactor
|
this.args.model.secondFactor
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
}}</label>
|
}}</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<SecondFactorInput
|
<SecondFactorInput
|
||||||
|
maxlength={{this.maxSecondFactorNameLength}}
|
||||||
@value={{this.secondFactorName}}
|
@value={{this.secondFactorName}}
|
||||||
@inputId="second-factor-name"
|
@inputId="second-factor-name"
|
||||||
@placeholder={{i18n "user.second_factor.totp.default_name"}}
|
@placeholder={{i18n "user.second_factor.totp.default_name"}}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import Component from "@glimmer/component";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
import { MAX_SECOND_FACTOR_NAME_LENGTH } from "discourse/models/user";
|
||||||
|
|
||||||
export default class SecondFactorAddTotp extends Component {
|
export default class SecondFactorAddTotp extends Component {
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
|
@ -11,6 +12,8 @@ export default class SecondFactorAddTotp extends Component {
|
||||||
@tracked errorMessage;
|
@tracked errorMessage;
|
||||||
@tracked secondFactorToken;
|
@tracked secondFactorToken;
|
||||||
|
|
||||||
|
maxSecondFactorNameLength = MAX_SECOND_FACTOR_NAME_LENGTH;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
totpRequested() {
|
totpRequested() {
|
||||||
this.args.model.secondFactor
|
this.args.model.secondFactor
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
}}</label>
|
}}</label>
|
||||||
<Input
|
<Input
|
||||||
name="security-key-name"
|
name="security-key-name"
|
||||||
|
maxlength={{this.maxSecondFactorNameLength}}
|
||||||
@type="text"
|
@type="text"
|
||||||
@value={{@model.securityKey.name}}
|
@value={{@model.securityKey.name}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { MAX_SECOND_FACTOR_NAME_LENGTH } from "discourse/models/user";
|
||||||
|
|
||||||
export default class SecondFactorEditSecurityKey extends Component {
|
export default class SecondFactorEditSecurityKey extends Component {
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
|
|
||||||
|
maxSecondFactorNameLength = MAX_SECOND_FACTOR_NAME_LENGTH;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
editSecurityKey() {
|
editSecurityKey() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
}}</label>
|
}}</label>
|
||||||
<Input
|
<Input
|
||||||
name="authenticator-name"
|
name="authenticator-name"
|
||||||
|
maxlength={{this.maxSecondFactorNameLength}}
|
||||||
@type="text"
|
@type="text"
|
||||||
@value={{@model.secondFactor.name}}
|
@value={{@model.secondFactor.name}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { MAX_SECOND_FACTOR_NAME_LENGTH } from "discourse/models/user";
|
||||||
|
|
||||||
export default class SecondFactorEdit extends Component {
|
export default class SecondFactorEdit extends Component {
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
|
|
||||||
|
maxSecondFactorNameLength = MAX_SECOND_FACTOR_NAME_LENGTH;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
editSecondFactor() {
|
editSecondFactor() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
|
@ -11,4 +11,5 @@
|
||||||
@autofocus="autofocus"
|
@autofocus="autofocus"
|
||||||
@placeholder={{this.placeholder}}
|
@placeholder={{this.placeholder}}
|
||||||
@input={{action "onInput"}}
|
@input={{action "onInput"}}
|
||||||
|
...attributes
|
||||||
/>
|
/>
|
|
@ -49,6 +49,8 @@ export const SECOND_FACTOR_METHODS = {
|
||||||
SECURITY_KEY: 3,
|
SECURITY_KEY: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MAX_SECOND_FACTOR_NAME_LENGTH = 300;
|
||||||
|
|
||||||
const TEXT_SIZE_COOKIE_NAME = "text_size";
|
const TEXT_SIZE_COOKIE_NAME = "text_size";
|
||||||
const COOKIE_EXPIRY_DAYS = 365;
|
const COOKIE_EXPIRY_DAYS = 365;
|
||||||
|
|
||||||
|
|
|
@ -1545,6 +1545,11 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_second_factor_security_key
|
def create_second_factor_security_key
|
||||||
|
if current_user.all_security_keys.count >= UserSecurityKey::MAX_KEYS_PER_USER
|
||||||
|
render_json_error(I18n.t("login.too_many_security_keys"), status: 422)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
challenge_session = Webauthn.stage_challenge(current_user, secure_session)
|
challenge_session = Webauthn.stage_challenge(current_user, secure_session)
|
||||||
render json:
|
render json:
|
||||||
success_json.merge(
|
success_json.merge(
|
||||||
|
|
|
@ -96,6 +96,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :directory_items
|
has_many :directory_items
|
||||||
has_many :email_logs
|
has_many :email_logs
|
||||||
has_many :security_keys, -> { where(enabled: true) }, class_name: "UserSecurityKey"
|
has_many :security_keys, -> { where(enabled: true) }, class_name: "UserSecurityKey"
|
||||||
|
has_many :all_security_keys, class_name: "UserSecurityKey"
|
||||||
|
|
||||||
has_many :badges, through: :user_badges
|
has_many :badges, through: :user_badges
|
||||||
has_many :default_featured_user_badges,
|
has_many :default_featured_user_badges,
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
class UserSecondFactor < ActiveRecord::Base
|
class UserSecondFactor < ActiveRecord::Base
|
||||||
include SecondFactorManager
|
include SecondFactorManager
|
||||||
|
|
||||||
|
MAX_TOTPS_PER_USER = 50
|
||||||
|
MAX_NAME_LENGTH = 300
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
scope :backup_codes, -> { where(method: UserSecondFactor.methods[:backup_codes], enabled: true) }
|
scope :backup_codes, -> { where(method: UserSecondFactor.methods[:backup_codes], enabled: true) }
|
||||||
|
@ -10,6 +14,10 @@ class UserSecondFactor < ActiveRecord::Base
|
||||||
|
|
||||||
scope :all_totps, -> { where(method: UserSecondFactor.methods[:totp]) }
|
scope :all_totps, -> { where(method: UserSecondFactor.methods[:totp]) }
|
||||||
|
|
||||||
|
validates :name, length: { maximum: MAX_NAME_LENGTH }, if: :name_changed?
|
||||||
|
|
||||||
|
validate :count_per_user_does_not_exceed_limit, on: :create
|
||||||
|
|
||||||
def self.methods
|
def self.methods
|
||||||
@methods ||= Enum.new(totp: 1, backup_codes: 2, security_key: 3)
|
@methods ||= Enum.new(totp: 1, backup_codes: 2, security_key: 3)
|
||||||
end
|
end
|
||||||
|
@ -21,6 +29,16 @@ class UserSecondFactor < ActiveRecord::Base
|
||||||
def totp_provisioning_uri
|
def totp_provisioning_uri
|
||||||
totp_object.provisioning_uri(user.email)
|
totp_object.provisioning_uri(user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def count_per_user_does_not_exceed_limit
|
||||||
|
if self.method == UserSecondFactor.methods[:totp]
|
||||||
|
if self.class.where(method: self.method, user_id: self.user_id).count >= MAX_TOTPS_PER_USER
|
||||||
|
errors.add(:base, I18n.t("login.too_many_authenticators"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
|
@ -35,7 +53,7 @@ end
|
||||||
# last_used :datetime
|
# last_used :datetime
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# name :string
|
# name :string(300)
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
|
@ -2,13 +2,26 @@
|
||||||
|
|
||||||
class UserSecurityKey < ActiveRecord::Base
|
class UserSecurityKey < ActiveRecord::Base
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
MAX_KEYS_PER_USER = 50
|
||||||
|
MAX_NAME_LENGTH = 300
|
||||||
|
|
||||||
scope :second_factors,
|
scope :second_factors,
|
||||||
-> { where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true) }
|
-> { where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true) }
|
||||||
|
|
||||||
|
validates :name, length: { maximum: MAX_NAME_LENGTH }, if: :name_changed?
|
||||||
|
validate :count_per_user_does_not_exceed_limit, on: :create
|
||||||
|
|
||||||
def self.factor_types
|
def self.factor_types
|
||||||
@factor_types ||= Enum.new(second_factor: 0, first_factor: 1, multi_factor: 2)
|
@factor_types ||= Enum.new(second_factor: 0, first_factor: 1, multi_factor: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def count_per_user_does_not_exceed_limit
|
||||||
|
if UserSecurityKey.where(user_id: self.user_id).count >= MAX_KEYS_PER_USER
|
||||||
|
errors.add(:base, I18n.t("login.too_many_security_keys"))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
|
@ -21,7 +34,7 @@ end
|
||||||
# public_key :string not null
|
# public_key :string not null
|
||||||
# factor_type :integer default(0), not null
|
# factor_type :integer default(0), not null
|
||||||
# enabled :boolean default(TRUE), not null
|
# enabled :boolean default(TRUE), not null
|
||||||
# name :string not null
|
# name :string(300) not null
|
||||||
# last_used :datetime
|
# last_used :datetime
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
|
|
|
@ -2675,6 +2675,8 @@ en:
|
||||||
invalid_security_key: "Invalid security key."
|
invalid_security_key: "Invalid security key."
|
||||||
missing_second_factor_name: "Please provide a name."
|
missing_second_factor_name: "Please provide a name."
|
||||||
missing_second_factor_code: "Please provide a code."
|
missing_second_factor_code: "Please provide a code."
|
||||||
|
too_many_authenticators: "Sorry, you can't have more than 50 authenticators. Please remove an existing one and try again."
|
||||||
|
too_many_security_keys: "Sorry, you can't have more than 50 security keys. Please remove an existing one and try again."
|
||||||
second_factor_toggle:
|
second_factor_toggle:
|
||||||
totp: "Use an authenticator app or security key instead"
|
totp: "Use an authenticator app or security key instead"
|
||||||
backup_code: "Use a backup code instead"
|
backup_code: "Use a backup code instead"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddLimitToUserSecondFactorName < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
execute(<<~SQL)
|
||||||
|
UPDATE user_second_factors
|
||||||
|
SET name = LEFT(name, 300)
|
||||||
|
WHERE name IS NOT NULL AND LENGTH(name) > 300
|
||||||
|
SQL
|
||||||
|
change_column :user_second_factors, :name, :string, limit: 300
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddLimitToUserSecurityKeyName < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
execute(<<~SQL)
|
||||||
|
UPDATE user_security_keys
|
||||||
|
SET name = LEFT(name, 300)
|
||||||
|
WHERE name IS NOT NULL AND LENGTH(name) > 300
|
||||||
|
SQL
|
||||||
|
change_column :user_security_keys, :name, :string, limit: 300
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
|
@ -109,7 +109,7 @@ module Webauthn
|
||||||
# then register the new credential with the account that was denoted in options.user, by
|
# then register the new credential with the account that was denoted in options.user, by
|
||||||
# associating it with the credentialId and credentialPublicKey in the attestedCredentialData
|
# associating it with the credentialId and credentialPublicKey in the attestedCredentialData
|
||||||
# in authData, as appropriate for the Relying Party's system.
|
# in authData, as appropriate for the Relying Party's system.
|
||||||
UserSecurityKey.create(
|
UserSecurityKey.create!(
|
||||||
user: @current_user,
|
user: @current_user,
|
||||||
credential_id: encoded_credential_id,
|
credential_id: encoded_credential_id,
|
||||||
public_key: endcoded_public_key,
|
public_key: endcoded_public_key,
|
||||||
|
|
|
@ -1,10 +1,83 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe UserSecondFactor do
|
RSpec.describe UserSecondFactor do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
describe ".methods" do
|
describe ".methods" do
|
||||||
it "should retain the right order" do
|
it "should retain the right order" do
|
||||||
expect(described_class.methods[:totp]).to eq(1)
|
expect(described_class.methods[:totp]).to eq(1)
|
||||||
expect(described_class.methods[:backup_codes]).to eq(2)
|
expect(described_class.methods[:backup_codes]).to eq(2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "name length validation" do
|
||||||
|
it "allows the name to be nil" do
|
||||||
|
Fabricate(:user_second_factor_totp, user: user, name: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow the name to be longer than the limit" do
|
||||||
|
expect do
|
||||||
|
Fabricate(
|
||||||
|
:user_second_factor_totp,
|
||||||
|
user: user,
|
||||||
|
name: "a" * (described_class::MAX_NAME_LENGTH + 1),
|
||||||
|
)
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid) do |error|
|
||||||
|
expect(error.message).to include(
|
||||||
|
I18n.t("activerecord.errors.messages.too_long", count: described_class::MAX_NAME_LENGTH),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows a name that is equal to or less than the limit" do
|
||||||
|
expect do
|
||||||
|
Fabricate(
|
||||||
|
:user_second_factor_totp,
|
||||||
|
user: user,
|
||||||
|
name: "a" * described_class::MAX_NAME_LENGTH,
|
||||||
|
)
|
||||||
|
end.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "per-user count validation" do
|
||||||
|
it "doesn't allow a user to have more authenticators than the limit allows" do
|
||||||
|
stub_const(UserSecondFactor, "MAX_TOTPS_PER_USER", 1) do
|
||||||
|
Fabricate(:user_second_factor_totp, user: user)
|
||||||
|
expect do Fabricate(:user_second_factor_totp, user: user) end.to raise_error(
|
||||||
|
ActiveRecord::RecordInvalid,
|
||||||
|
) do |error|
|
||||||
|
expect(error.message).to include(I18n.t("login.too_many_authenticators"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't count backup codes in the authenticators limit" do
|
||||||
|
user.generate_backup_codes
|
||||||
|
expect(user.user_second_factors.backup_codes.count).to eq(10)
|
||||||
|
|
||||||
|
stub_const(UserSecondFactor, "MAX_TOTPS_PER_USER", 1) do
|
||||||
|
Fabricate(:user_second_factor_totp, user: user)
|
||||||
|
expect do Fabricate(:user_second_factor_totp, user: user) end.to raise_error(
|
||||||
|
ActiveRecord::RecordInvalid,
|
||||||
|
) do |error|
|
||||||
|
expect(error.message).to include(I18n.t("login.too_many_authenticators"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't count authenticators from other users" do
|
||||||
|
another_user = Fabricate(:user)
|
||||||
|
Fabricate(:user_second_factor_totp, user: another_user)
|
||||||
|
|
||||||
|
stub_const(UserSecondFactor, "MAX_TOTPS_PER_USER", 1) do
|
||||||
|
Fabricate(:user_second_factor_totp, user: user)
|
||||||
|
expect do Fabricate(:user_second_factor_totp, user: user) end.to raise_error(
|
||||||
|
ActiveRecord::RecordInvalid,
|
||||||
|
) do |error|
|
||||||
|
expect(error.message).to include(I18n.t("login.too_many_authenticators"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
58
spec/models/user_security_key_spec.rb
Normal file
58
spec/models/user_security_key_spec.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe UserSecurityKey do
|
||||||
|
fab!(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
describe "name length validation" do
|
||||||
|
it "doesn't allow the name to be longer than the limit" do
|
||||||
|
expect do
|
||||||
|
Fabricate(
|
||||||
|
:user_security_key_with_random_credential,
|
||||||
|
user: user,
|
||||||
|
name: "b" * (described_class::MAX_NAME_LENGTH + 1),
|
||||||
|
)
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid) do |error|
|
||||||
|
expect(error.message).to include(
|
||||||
|
I18n.t("activerecord.errors.messages.too_long", count: described_class::MAX_NAME_LENGTH),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows a name that's under the limit" do
|
||||||
|
expect do
|
||||||
|
Fabricate(
|
||||||
|
:user_security_key_with_random_credential,
|
||||||
|
user: user,
|
||||||
|
name: "b" * described_class::MAX_NAME_LENGTH,
|
||||||
|
)
|
||||||
|
end.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "per-user count validation" do
|
||||||
|
it "doesn't allow a user to have more security keys than the limit allows" do
|
||||||
|
stub_const(UserSecurityKey, "MAX_KEYS_PER_USER", 1) do
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: user)
|
||||||
|
expect do
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: user)
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid) do |error|
|
||||||
|
expect(error.message).to include(I18n.t("login.too_many_security_keys"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't count security keys from other users" do
|
||||||
|
another_user = Fabricate(:user)
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: another_user)
|
||||||
|
|
||||||
|
stub_const(UserSecurityKey, "MAX_KEYS_PER_USER", 1) do
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: user)
|
||||||
|
expect do
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: user)
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid) do |error|
|
||||||
|
expect(error.message).to include(I18n.t("login.too_many_security_keys"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -5475,6 +5475,46 @@ RSpec.describe UsersController do
|
||||||
expect(response.parsed_body["error"]).to eq(I18n.t("login.missing_second_factor_code"))
|
expect(response.parsed_body["error"]).to eq(I18n.t("login.missing_second_factor_code"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't allow creating too many TOTPs" do
|
||||||
|
Fabricate(:user_second_factor_totp, user: user1)
|
||||||
|
|
||||||
|
create_totp
|
||||||
|
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
|
||||||
|
token = ROTP::TOTP.new(staged_totp_key).now
|
||||||
|
|
||||||
|
stub_const(UserSecondFactor, "MAX_TOTPS_PER_USER", 1) do
|
||||||
|
post "/users/enable_second_factor_totp.json",
|
||||||
|
params: {
|
||||||
|
name: "test",
|
||||||
|
second_factor_token: token,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("login.too_many_authenticators"))
|
||||||
|
|
||||||
|
expect(user1.user_second_factors.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow the TOTP name to exceed the limit" do
|
||||||
|
create_totp
|
||||||
|
staged_totp_key = read_secure_session["staged-totp-#{user1.id}"]
|
||||||
|
token = ROTP::TOTP.new(staged_totp_key).now
|
||||||
|
|
||||||
|
post "/users/enable_second_factor_totp.json",
|
||||||
|
params: {
|
||||||
|
name: "a" * (UserSecondFactor::MAX_NAME_LENGTH + 1),
|
||||||
|
second_factor_token: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(response.parsed_body["errors"]).to include(
|
||||||
|
"Name is too long (maximum is 300 characters)",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user1.user_second_factors.count).to eq(0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#update_second_factor" do
|
describe "#update_second_factor" do
|
||||||
|
@ -5650,6 +5690,13 @@ RSpec.describe UsersController do
|
||||||
expect(response_parsed["supported_algorithms"]).to eq(::Webauthn::SUPPORTED_ALGORITHMS)
|
expect(response_parsed["supported_algorithms"]).to eq(::Webauthn::SUPPORTED_ALGORITHMS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't create a challenge if the user has the maximum number allowed of security keys" do
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: user1)
|
||||||
|
stub_const(UserSecurityKey, "MAX_KEYS_PER_USER", 1) { create_second_factor_security_key }
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("login.too_many_security_keys"))
|
||||||
|
end
|
||||||
|
|
||||||
context "if the user has security key credentials already" do
|
context "if the user has security key credentials already" do
|
||||||
fab!(:user_security_key) { Fabricate(:user_security_key_with_random_credential, user: user1) }
|
fab!(:user_security_key) { Fabricate(:user_security_key_with_random_credential, user: user1) }
|
||||||
|
|
||||||
|
@ -5679,6 +5726,43 @@ RSpec.describe UsersController do
|
||||||
)
|
)
|
||||||
expect(user1.security_keys.last.name).to eq(valid_security_key_create_post_data[:name])
|
expect(user1.security_keys.last.name).to eq(valid_security_key_create_post_data[:name])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't allow creating too many security keys" do
|
||||||
|
simulate_localhost_webauthn_challenge
|
||||||
|
create_second_factor_security_key
|
||||||
|
_response_parsed = response.parsed_body
|
||||||
|
|
||||||
|
Fabricate(:user_security_key_with_random_credential, user: user1)
|
||||||
|
|
||||||
|
stub_const(UserSecurityKey, "MAX_KEYS_PER_USER", 1) do
|
||||||
|
post "/u/register_second_factor_security_key.json",
|
||||||
|
params: valid_security_key_create_post_data
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(response.parsed_body["errors"]).to include(I18n.t("login.too_many_security_keys"))
|
||||||
|
|
||||||
|
expect(user1.security_keys.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't allow the security key name to exceed the limit" do
|
||||||
|
simulate_localhost_webauthn_challenge
|
||||||
|
create_second_factor_security_key
|
||||||
|
_response_parsed = response.parsed_body
|
||||||
|
|
||||||
|
post "/u/register_second_factor_security_key.json",
|
||||||
|
params:
|
||||||
|
valid_security_key_create_post_data.merge(
|
||||||
|
name: "a" * (UserSecurityKey::MAX_NAME_LENGTH + 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).to eq(422)
|
||||||
|
expect(response.parsed_body["errors"]).to include(
|
||||||
|
"Name is too long (maximum is 300 characters)",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(user1.security_keys.count).to eq(0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the creation parameters are invalid" do
|
context "when the creation parameters are invalid" do
|
||||||
|
|
Loading…
Reference in New Issue
Block a user