Rails Kitchen

It's a place to write on stuff I learned recently.

Password Policy Implementation With Devise and Devise Security Extension

| Comments

One of my recent project was in banking domain. So I had to implement password policies, for that I used devise gem.

Password policies :
1 - Enforce password history - 5 password should be remembered
2 - maximum password age - 30 days
3 - minimum password length - 10 letters
4 - password must meet complexity requirements - Should be a combination of letters, numbers and symbols
5 - Account lockout threshold -5
6 - Account lockout duration - 30 minutes
7 - Email validation - Accept only emails of allowed set of domains

Most of the requirents mentioned above are achivable with simple cofigurations in Devise initializer. But to implement Password expirable, Password archivable and password complexity requirements check I used security extension devise_security_extension.

In this post I assume that we already had devise setup in our project. Now we need to add devise_security_extension in to our project.
1
gem 'devise_security_extension'
After you installed Devise Security Extension you need to run the generator:
1
rails generate devise_security_extension:install
The generator will inject the available configuration options into the existing Devise initializer.When you are done, you are ready to add Devise Security Extension modules on top of Devise modules to any of your Devise models.
1
devise :password_expirable, :password_archivable, :expirable, :lockable
I added this in User model
1
2
3
4
5
class User < ActiveRecord::Base
    devise :database_authenticatable, :registerable, :confirmable,
           :recoverable, :rememberable, :trackable, :validatable,
           :password_expirable, :password_archivable, :expirable, :lockable
end
1 - Enforce password history
Uncomment configuration password_archiving_count and deny_old_passwords in Devise initializer and also add a migration creating old_passwords tables.
1
2
3
4
5
6
7
Devise.setup do |config|
  # How often save old passwords in archive
   config.password_archiving_count = 5

  # Deny old password (true, false, count)
   config.deny_old_passwords = true
end
1
2
3
4
5
6
7
8
create_table :old_passwords do |t|
  t.string :encrypted_password, :null => false
  t.string :password_salt
  t.string :password_archivable_type, :null => false
  t.integer :password_archivable_id, :null => false
  t.datetime :created_at
end
add_index :old_passwords, [:password_archivable_type, :password_archivable_id], :name => :index_password_archivable
2 - maximum password age - 30 days
Uncomment configuration expire_password_after and change to 1.months and also add a migration to store .
1
# config.expire_password_after = 1.months
1
2
3
4
5
create_table :the_resources do |t|
  # other devise fields
  t.datetime :password_changed_at
end
add_index :the_resources, :password_changed_at
Replace our devise model with the_resources.
1
2
3
4
5
create_table :users do |t|
  # other devise fields
  t.datetime :password_changed_at
end
add_index :users, :password_changed_at
3 - minimum password length - 10 letters
1
2
# Range for password length.
config.password_length = 10..128
4 - password must meet complexity requirements - Should be a combination of letters, numbers and symbols
Add password_regex with Regular expression which satisfies our needs.
1
2
# Need 1 char of A-Z, a-z and 0-9 and special charactor
config.password_regex = /(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[\W])/
5 - Account lockout threshold -5
1
2
3
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
config.maximum_attempts = 5
6 - Account lockout duration - 30 minutes
1
2
# Time interval to unlock the account if :time is enabled as unlock_strategy.
config.unlock_in = 30.minutes
7 - Email validation - Accept only emails of allowed set of domains
For this, I added a custom validator which will check the domain name of user entered an email.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
validate :presence_of_domain_in_email

def presence_of_domain_in_email
   email_domain = email.split("@").last.downcase
   allowed_domains = User.allowed_domains
   unless allowed_domains[email_domain]
       errors.add :email, "This email domain is not valid. "
   end
end
  
def self.allowed_domains
  {
      'gmail.com' =>'gmail.com',
      'example.com' => 'example.com',
      'example1.in' =>'example1.in'

  }
end

Comments