Skip to content

Commit

Permalink
Additional password security (#918)
Browse files Browse the repository at this point in the history
* Additional password security

Contain at least 2 uppercase letters
Contain at least 2 lowercase letters
Contain at least 2 numbers
Contain at least 2 special characters

* Make all the specs pass and add pwned gem

* Reduce password complexity message, do not use html

* Include ui specs

* Update password complexity hint

* Leftover code, remove
  • Loading branch information
ebrett authored Nov 7, 2023
1 parent ea80f70 commit e517946
Show file tree
Hide file tree
Showing 18 changed files with 140 additions and 38 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,6 @@ group :ui do
gem 'selenium-webdriver'
gem 'site_prism'
end

gem 'devise-pwned_password', '~> 0.1.10'
gem 'devise-security', '~> 0.18.0'
10 changes: 9 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-pwned_password (0.1.10)
devise (~> 4)
pwned (~> 2.0.0)
devise-security (0.18.0)
devise (>= 4.3.0)
dibber (0.7.0)
diff-lcs (1.5.0)
digest-crc (0.6.5)
Expand Down Expand Up @@ -339,6 +344,7 @@ GEM
public_suffix (5.0.3)
puma (6.4.0)
nio4r (~> 2.0)
pwned (2.0.2)
que (2.2.1)
que-scheduler (4.4.0)
activesupport (>= 5.0)
Expand Down Expand Up @@ -571,6 +577,8 @@ DEPENDENCIES
cssbundling-rails
debug
devise
devise-pwned_password (~> 0.1.10)
devise-security (~> 0.18.0)
dibber
dotenv-rails
dry-core
Expand Down Expand Up @@ -622,4 +630,4 @@ RUBY VERSION
ruby 3.2.2p53

BUNDLED WITH
2.3.26
2.4.13
2 changes: 1 addition & 1 deletion app/helpers/content_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,6 @@ def opt_in_out(type)

# @yield [String]
def password_complexity
t(:password_complexity, length: User.password_length.first)
t('password_complexity', length: User.password_length.first)
end
end
5 changes: 3 additions & 2 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def self.dashboard_headers
attr_accessor :context

devise :database_authenticatable, :registerable, :recoverable,
:validatable, :rememberable, :confirmable, :lockable, :timeoutable
:validatable, :rememberable, :confirmable, :lockable, :timeoutable, :secure_validatable
devise :pwned_password unless Rails.env.test?

has_many :responses
has_many :user_answers
Expand Down Expand Up @@ -362,7 +363,7 @@ def redact!
last_name: 'User',
email: "redacted_user#{id}@example.com",
closed_at: Time.zone.now,
password: 'redacteduser')
password: 'RedactedUser12!@')

notes.destroy_all
end
Expand Down
52 changes: 52 additions & 0 deletions config/initializers/devise_security.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

Devise.setup do |config|
# ==> Security Extension
# Configure security extension for devise

# Should the password expire (e.g 3.months)
# config.expire_password_after = false

# Need 1 char each of: A-Z, a-z, 0-9, and a punctuation mark or symbol
# You may use "digits" in place of "digit" and "symbols" in place of
# "symbol" based on your preference
config.password_complexity = { digit: 2, lower: 2, symbol: 2, upper: 2 }

# How many passwords to keep in archive
# config.password_archiving_count = 5

# Deny old passwords (true, false, number_of_old_passwords_to_check)
# 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
# config.deny_old_passwords = true

# enable email validation for :secure_validatable. (true, false, validation_options)
# dependency: see https://github.com/devise-security/devise-security/blob/master/README.md#e-mail-validation
config.email_validation = false

# captcha integration for recover form
# config.captcha_for_recover = true

# captcha integration for sign up form
# config.captcha_for_sign_up = true

# captcha integration for sign in form
# config.captcha_for_sign_in = true

# captcha integration for unlock form
# config.captcha_for_unlock = true

# captcha integration for confirmation form
# config.captcha_for_confirmation = true

# Time period for account expiry from last_activity_at
# config.expire_after = 90.days

# Allow password to equal the email
# config.allow_passwords_equal_to_email = false

# paranoid_verification will regenerate verification code after failed attempt
# config.paranoid_code_regenerate_after_attempt = 10
end
5 changes: 3 additions & 2 deletions config/locales/devise.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ en:
already_authenticated: "You are already signed in."
inactive: "Your account is not activated yet."
invalid: |
Enter a valid email address and password. Your account will be locked after 5 unsuccessful attempts. We will email you instructions to unlock your account.
Enter a valid email address and password. Your account will be locked after 5 unsuccessful attempts. We will email you instructions to unlock your account.
locked: "For security reasons your account has been locked for %{unlock_in} hours. For faster access we have sent you an email to reset your password."
last_attempt: "You have one more attempt before your account is locked."
not_found_in_database: |
Expand All @@ -27,7 +27,7 @@ en:
unauthenticated: "You need to sign in or sign up before continuing."
unconfirmed: "You have to confirm your email address before continuing."
omniauth_callbacks:
failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
failure: 'Could not authenticate you from %{kind} because "%{reason}".'
success: "Successfully authenticated from %{kind} account."
passwords:
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
Expand Down Expand Up @@ -63,3 +63,4 @@ en:
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
pwned_password: "Password has previously appeared in a data breach and should never be used. Please choose a different password."
42 changes: 42 additions & 0 deletions config/locales/devise.security_extension.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
en:
errors:
messages:
taken_in_past: 'was used previously.'
equal_to_current_password: 'must be different than the current password.'
equal_to_email: 'must be different than the email.'
password_complexity:
digit:
one: must contain at least one digit
other: must contain at least %{count} digits
lower:
one: must contain at least one lower-case letter
other: must contain at least %{count} lower-case letters
symbol:
one: must contain at least one punctuation mark or symbol
other: must contain at least %{count} punctuation marks or symbols
upper:
one: must contain at least one upper-case letter
other: must contain at least %{count} upper-case letters
devise:
invalid_captcha: 'The captcha input was invalid.'
invalid_security_question: 'The security question answer was invalid.'
paranoid_verify:
code_required: 'Please enter the code our support team provided'
paranoid_verification_code:
updated: Verification code accepted
show:
submit_verification_code: Submit verification code
verification_code: Verification code
submit: Submit
password_expired:
updated: 'Your new password is saved.'
change_required: 'Your password is expired. Please renew your password.'
show:
renew_your_password: Renew your password
current_password: Current password
new_password: New password
new_password_confirmation: Confirm new password
change_my_password: Change my password
failure:
session_limited: 'Your login credentials were used in another browser. Please sign in again to continue in this browser.'
expired: 'Your account has expired due to inactivity. Please contact the site administrator.'
13 changes: 4 additions & 9 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ en:
terms_and_conditions_agreed_at:
blank: You must accept the terms and conditions and privacy policy to create an account.




# Form Builder ---------------------------------------------------------------

helpers:
Expand All @@ -96,7 +93,8 @@ en:

phase_banner: This is a new service, your %{link} will help us improve it.

password_complexity: Your password must contain at least %{length} characters.
password_complexity: |
Passwords should be a minimum of %{length} characters long, contain at least two uppercase letters, two lowercase letters, two numbers, and two special characters or non-alphanumeric characters.
pagination:
section: Section %{current} of %{total}
Expand All @@ -120,7 +118,7 @@ en:

# Training Modules -----------------------------------------------------------

date_completed: 'Date completed: %{date}'
date_completed: "Date completed: %{date}"

module_indicator:
completed: completed
Expand Down Expand Up @@ -259,7 +257,6 @@ en:
If you have not received the email after a few minutes, please check your spam folder.
# Pages ----------------------------------------------------------------------

# /my-learning
Expand Down Expand Up @@ -493,7 +490,6 @@ en:
opt_in: Send me early years email updates
opt_out: Do not send me early years email updates


# /
home:
title: Home page
Expand Down Expand Up @@ -524,7 +520,6 @@ en:
Sign in to continue learning, see your progress and download certificates.
# /about-training
about:
title: About training
Expand Down Expand Up @@ -567,7 +562,7 @@ en:
# /settings/cookie-policy
cookie_policy:
title: Cookie policy
legend : Do you want to accept analytics cookies?
legend: Do you want to accept analytics cookies?
flash: You’ve set your cookie preferences. [Go back to Early years child development training](%{path}).
body: |
# Cookies
Expand Down
6 changes: 3 additions & 3 deletions db/seeds/users.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
[email protected]:
password: <%= ENV.fetch('USER_PASSWORD', 'StrongPassword') %>
password: <%= ENV.fetch('USER_PASSWORD', 'StrongPassword12!@') %>
terms_and_conditions_agreed_at: <%= 1.minute.ago %>

[email protected]:
password: <%= ENV.fetch('USER_PASSWORD', 'StrongPassword') %>
password: <%= ENV.fetch('USER_PASSWORD', 'StrongPassword12!@') %>
terms_and_conditions_agreed_at: <%= 1.minute.ago %>
confirmed_at: <%= 1.minute.ago %>

[email protected]:
password: <%= ENV.fetch('USER_PASSWORD', 'StrongPassword') %>
password: <%= ENV.fetch('USER_PASSWORD', 'StrongPassword12!@') %>
terms_and_conditions_agreed_at: <%= 1.minute.ago %>
confirmed_at: <%= 1.minute.ago %>
first_name: Demo
Expand Down
2 changes: 1 addition & 1 deletion lib/tasks/eyfs.rake
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace :eyfs do
unless User.find_by(email: "#{bot_token}@example.com")
User.create!(
email: "#{bot_token}@example.com",
password: ENV.fetch('USER_PASSWORD', 'StrongPassword'),
password: ENV.fetch('USER_PASSWORD', 'StrongPassword12!@'),
confirmed_at: Time.zone.now,
terms_and_conditions_agreed_at: Time.zone.now,
first_name: 'Bot',
Expand Down
12 changes: 6 additions & 6 deletions spec/controllers/user_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
context 'when successful' do
let(:params) do
{
password: 'NewPassword123',
confirm_password: 'NewPassword123',
current_password: 'StrongPassword123',
password: 'NewPassword12!@',
confirm_password: 'NewPassword12!@',
current_password: 'StrongPassword12!@',
}
end

Expand All @@ -70,8 +70,8 @@
context 'when current password is wrong' do
let(:params) do
{
password: 'NewPassword123',
confirm_password: 'NewPassword123',
password: 'NewPassword12!@',
confirm_password: 'NewPassword12!@',
current_password: 'wrongpassword',
}
end
Expand All @@ -90,7 +90,7 @@
{
password: '',
confirm_password: '',
current_password: 'StrongPassword123',
current_password: 'StrongPassword12!@',
}
end

Expand Down
2 changes: 1 addition & 1 deletion spec/factories/users.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :user do
email { Faker::Internet.email }
password { 'StrongPassword123' }
password { 'StrongPassword12!@' }
terms_and_conditions_agreed_at { Date.new(2000, 0o1, 0o1) }

trait :confirmed do
Expand Down
2 changes: 1 addition & 1 deletion spec/models/user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
expect(user.first_name).to eq 'Redacted'
expect(user.last_name).to eq 'User'
expect(user.email).to eq "redacted_user#{user.id}@example.com"
expect(user.valid_password?('redacteduser')).to eq true
expect(user.valid_password?('RedactedUser12!@')).to eq true
expect(user.closed_at).to be_within(30).of(Time.zone.now)
end
end
Expand Down
8 changes: 4 additions & 4 deletions spec/system/forgotten_password_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
RSpec.describe 'User following forgotten password process' do
let(:user) { create :user, :confirmed }
let(:token) { user.send_reset_password_instructions }
let(:password) { 'ABCDE123xyh' }
let(:password) { 'ABCDE123xyh!@' }

context 'when choosing a new password' do
before do
Expand All @@ -12,7 +12,7 @@

# Happy path scenario
context 'and new password meets criteria' do
let(:password) { 'NewPassword123' }
let(:password) { 'NewPassword12!@' }

it 'flash message displays correctly' do
fill_in 'New password', with: password
Expand All @@ -39,8 +39,8 @@
end

context "and password and confirm password don't match" do
let(:password) { 'NewPassword123' }
let(:confirm_password) { 'NewPassword456' }
let(:password) { 'NewPassword12!@' }
let(:confirm_password) { 'NewPassword45!@' }

it 'displays error message' do
fill_in 'New password', with: password
Expand Down
6 changes: 3 additions & 3 deletions spec/system/registered_user/changing_password_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
RSpec.describe 'Registered user changing password', type: :system do
subject(:user) { create :user, :registered, created_at: 1.month.ago }

let(:password) { 'StrongPassword123' }
let(:password) { 'StrongPassword12!@' }

include_context 'with user'

before do
visit '/my-account/edit-password'
fill_in 'Enter your current password', with: 'StrongPassword123'
fill_in 'Enter your current password', with: 'StrongPassword12!@'
fill_in 'Create a new password', with: password
fill_in 'Confirm password', with: password
end
Expand All @@ -23,7 +23,7 @@
end

context 'when successful' do
let(:password) { '1NewPassword' }
let(:password) { '12!@NewPassword' }
let(:today) { Time.zone.today.to_formatted_s(:rfc822) } # 18 May 2022

it 'updates password' do
Expand Down
2 changes: 1 addition & 1 deletion spec/system/registered_user/closing_account_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
expect(user.last_name).to eq 'User'
expect(user.email).to have_text 'redacted_user'
expect(user.notes.count).to eq 0
expect(user.valid_password?('redacteduser')).to eq true
expect(user.valid_password?('RedactedUser12!@')).to eq true
end
end
end
Expand Down
Loading

0 comments on commit e517946

Please sign in to comment.