Merge pull request #5163 from consuldemocracy/enable_password_complexity
ENS: Enable password complexity
This commit is contained in:
@@ -2,16 +2,33 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
App.Managers = {
|
App.Managers = {
|
||||||
generatePassword: function() {
|
generatePassword: function() {
|
||||||
var chars, possible_chars;
|
var pass, possible_chars, possible_digits, possible_symbols, password_complexity;
|
||||||
possible_chars = "aAbcdeEfghiJkmnpqrstuUvwxyz23456789";
|
password_complexity = $(".generate-random-value").data("password-complexity");
|
||||||
chars = Array.apply(null, {
|
possible_chars = "abcdefghijklmnopqrstuvwxyz";
|
||||||
length: 12
|
possible_digits = "123456789";
|
||||||
|
possible_symbols = "-_.,;!?";
|
||||||
|
|
||||||
|
pass = Array.apply(null, {
|
||||||
|
length: 8
|
||||||
}).map(function() {
|
}).map(function() {
|
||||||
var i;
|
var i;
|
||||||
i = Math.floor(Math.random() * possible_chars.length);
|
i = Math.floor(Math.random() * possible_chars.length);
|
||||||
return possible_chars.charAt(i);
|
return possible_chars.charAt(i);
|
||||||
});
|
}).join("");
|
||||||
return chars.join("");
|
|
||||||
|
for (var i = 0; i < password_complexity.upper; i++) {
|
||||||
|
pass += possible_chars.charAt(Math.floor(Math.random() * possible_chars.length)).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < password_complexity.digit; i++) {
|
||||||
|
pass += possible_digits.charAt(Math.floor(Math.random() * possible_digits.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < password_complexity.symbol; i++) {
|
||||||
|
pass += possible_symbols.charAt(Math.floor(Math.random() * possible_symbols.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass;
|
||||||
},
|
},
|
||||||
togglePassword: function(type) {
|
togglePassword: function(type) {
|
||||||
$("#user_password").prop("type", type);
|
$("#user_password").prop("type", type);
|
||||||
|
|||||||
@@ -416,6 +416,14 @@ class User < ApplicationRecord
|
|||||||
update!(subscriptions_token: SecureRandom.base58(32)) if subscriptions_token.blank?
|
update!(subscriptions_token: SecureRandom.base58(32)) if subscriptions_token.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.password_complexity
|
||||||
|
if Tenant.current_secrets.dig(:security, :password_complexity)
|
||||||
|
{ digit: 1, lower: 1, symbol: 0, upper: 1 }
|
||||||
|
else
|
||||||
|
{ digit: 0, lower: 0, symbol: 0, upper: 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def clean_document_number
|
def clean_document_number
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= link_to t("management.account.edit.password.random"), "#", class: "generate-random-value float-right" %>
|
<%= link_to t("management.account.edit.password.random"),
|
||||||
|
"#",
|
||||||
|
class: "generate-random-value float-right",
|
||||||
|
data: { "password-complexity": User.password_complexity } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= f.submit t("management.account.edit.password.save"), class: "button success" %>
|
<%= f.submit t("management.account.edit.password.save"), class: "button success" %>
|
||||||
|
|||||||
@@ -3,21 +3,25 @@ Devise.setup do |config|
|
|||||||
# Configure security extension for devise
|
# Configure security extension for devise
|
||||||
|
|
||||||
# Should the password expire (e.g 3.months)
|
# Should the password expire (e.g 3.months)
|
||||||
# config.expire_password_after = false
|
|
||||||
config.expire_password_after = 1.year
|
config.expire_password_after = 1.year
|
||||||
|
|
||||||
# Need 1 char of A-Z, a-z and 0-9
|
# Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol
|
||||||
# config.password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/
|
# You may use "digits" in place of "digit" and "symbols" in place of
|
||||||
|
# "symbol" based on your preference
|
||||||
|
config.password_complexity = { digit: 0, lower: 0, symbol: 0, upper: 0 } # Overwritten in User model
|
||||||
|
|
||||||
# How many passwords to keep in archive
|
# How many passwords to keep in archive
|
||||||
# config.password_archiving_count = 5
|
# config.password_archiving_count = 5
|
||||||
|
|
||||||
# Deny old password (true, false, count)
|
# Deny old passwords (true, false, number_of_old_passwords_to_check)
|
||||||
# config.deny_old_passwords = true
|
# Examples:
|
||||||
|
# config.deny_old_passwords = false # allow old passwords
|
||||||
|
# config.deny_old_passwords = true # will deny all the old passwords
|
||||||
|
# config.deny_old_passwords = 3 # will deny new passwords that matches with the last 3 passwords
|
||||||
|
|
||||||
# enable email validation for :secure_validatable. (true, false, validation_options)
|
# enable email validation for :secure_validatable. (true, false, validation_options)
|
||||||
# dependency: need an email validator like rails_email_validator
|
# dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
|
||||||
# config.email_validation = true
|
config.email_validation = false
|
||||||
|
|
||||||
# captcha integration for recover form
|
# captcha integration for recover form
|
||||||
# config.captcha_for_recover = true
|
# config.captcha_for_recover = true
|
||||||
@@ -36,6 +40,9 @@ Devise.setup do |config|
|
|||||||
|
|
||||||
# Time period for account expiry from last_activity_at
|
# Time period for account expiry from last_activity_at
|
||||||
# config.expire_after = 90.days
|
# config.expire_after = 90.days
|
||||||
|
|
||||||
|
# Allow password to equal the email
|
||||||
|
config.allow_passwords_equal_to_email = true
|
||||||
end
|
end
|
||||||
|
|
||||||
module Devise
|
module Devise
|
||||||
@@ -51,14 +58,6 @@ module Devise
|
|||||||
end
|
end
|
||||||
|
|
||||||
module SecureValidatable
|
module SecureValidatable
|
||||||
def self.included(base)
|
|
||||||
base.extend ClassMethods
|
|
||||||
assert_secure_validations_api!(base)
|
|
||||||
base.class_eval do
|
|
||||||
validate :current_equal_password_validation
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_equal_password_validation
|
def current_equal_password_validation
|
||||||
if !new_record? && !encrypted_password_change.nil? && !erased?
|
if !new_record? && !encrypted_password_change.nil? && !erased?
|
||||||
dummy = self.class.new
|
dummy = self.class.new
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ development:
|
|||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
|
password_complexity: false
|
||||||
secret_key_base: 56792feef405a59b18ea7db57b4777e855103882b926413d4afdfb8c0ea8aa86ea6649da4e729c5f5ae324c0ab9338f789174cf48c544173bc18fdc3b14262e4
|
secret_key_base: 56792feef405a59b18ea7db57b4777e855103882b926413d4afdfb8c0ea8aa86ea6649da4e729c5f5ae324c0ab9338f789174cf48c544173bc18fdc3b14262e4
|
||||||
<<: *maps
|
<<: *maps
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ staging:
|
|||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
|
password_complexity: false
|
||||||
tenants:
|
tenants:
|
||||||
# If you've enabled multitenancy, you can overwrite secrets for a
|
# If you've enabled multitenancy, you can overwrite secrets for a
|
||||||
# specific tenant with:
|
# specific tenant with:
|
||||||
@@ -92,6 +94,7 @@ preproduction:
|
|||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
|
password_complexity: false
|
||||||
tenants:
|
tenants:
|
||||||
# If you've enabled multitenancy, you can overwrite secrets for a
|
# If you've enabled multitenancy, you can overwrite secrets for a
|
||||||
# specific tenant with:
|
# specific tenant with:
|
||||||
@@ -135,6 +138,7 @@ production:
|
|||||||
multitenancy: false
|
multitenancy: false
|
||||||
security:
|
security:
|
||||||
last_sign_in: false
|
last_sign_in: false
|
||||||
|
password_complexity: false
|
||||||
tenants:
|
tenants:
|
||||||
# If you've enabled multitenancy, you can overwrite secrets for a
|
# If you've enabled multitenancy, you can overwrite secrets for a
|
||||||
# specific tenant with:
|
# specific tenant with:
|
||||||
|
|||||||
@@ -849,4 +849,37 @@ describe User do
|
|||||||
expect(user.subscriptions_token).to eq "already_set"
|
expect(user.subscriptions_token).to eq "already_set"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe ".password_complexity" do
|
||||||
|
it "returns no complexity when the secrets aren't configured" do
|
||||||
|
expect(User.password_complexity).to eq({ digit: 0, lower: 0, symbol: 0, upper: 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when secrets are configured" do
|
||||||
|
before do
|
||||||
|
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
|
||||||
|
security: {
|
||||||
|
password_complexity: true
|
||||||
|
},
|
||||||
|
tenants: {
|
||||||
|
tolerant: {
|
||||||
|
security: {
|
||||||
|
password_complexity: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses the general secrets for the main tenant" do
|
||||||
|
expect(User.password_complexity).to eq({ digit: 1, lower: 1, symbol: 0, upper: 1 })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses the tenant secrets for a tenant" do
|
||||||
|
allow(Tenant).to receive(:current_schema).and_return("tolerant")
|
||||||
|
|
||||||
|
expect(User.password_complexity).to eq({ digit: 0, lower: 0, symbol: 0, upper: 0 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -755,4 +755,51 @@ describe "Users" do
|
|||||||
|
|
||||||
expect(page).to have_content "must be different than the current password."
|
expect(page).to have_content "must be different than the current password."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "Regular authentication with password complexity enabled" do
|
||||||
|
before do
|
||||||
|
allow(Rails.application).to receive(:secrets).and_return(ActiveSupport::OrderedOptions.new.merge(
|
||||||
|
security: { password_complexity: true }
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
context "Sign up" do
|
||||||
|
scenario "Success with password" do
|
||||||
|
message = "You have been sent a message containing a verification link. Please click on this" \
|
||||||
|
" link to activate your account."
|
||||||
|
visit "/"
|
||||||
|
click_link "Register"
|
||||||
|
|
||||||
|
fill_in "Username", with: "Manuela Carmena"
|
||||||
|
fill_in "Email", with: "manuela@consul.dev"
|
||||||
|
fill_in "Password", with: "ValidPassword1234"
|
||||||
|
fill_in "Confirm password", with: "ValidPassword1234"
|
||||||
|
check "user_terms_of_service"
|
||||||
|
|
||||||
|
click_button "Register"
|
||||||
|
|
||||||
|
expect(page).to have_content message
|
||||||
|
|
||||||
|
confirm_email
|
||||||
|
|
||||||
|
expect(page).to have_content "Your account has been confirmed."
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario "Errors on sign up" do
|
||||||
|
visit "/"
|
||||||
|
click_link "Register"
|
||||||
|
|
||||||
|
fill_in "Username", with: "Manuela Carmena"
|
||||||
|
fill_in "Email", with: "manuela@consul.dev"
|
||||||
|
fill_in "Password", with: "invalid_password"
|
||||||
|
fill_in "Confirm password", with: "invalid_password"
|
||||||
|
check "user_terms_of_service"
|
||||||
|
|
||||||
|
click_button "Register"
|
||||||
|
|
||||||
|
expect(page).to have_content "must contain at least one digit, must contain at least one" \
|
||||||
|
" upper-case letter"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user