Published on
2 min read

Password Policy Implementation with Devise and Devise Security Extension

Authors

One of my recent projects was in the banking domain, so I had to implement password policies. For that, I used the Devise gem.

Password policies:

  1. Enforce password history - 5 passwords should be remembered.
  2. Maximum password age - 30 days.
  3. Minimum password length - 10 letters.
  4. Password complexity - Should be a combination of letters, numbers, and symbols.
  5. Account lockout threshold - 5 failed attempts.
  6. Account lockout duration - 30 minutes.
  7. Email validation - Accept only emails from allowed domains.

Most requirements are achievable with simple configurations in the Devise initializer. To implement password expiration, archiving, and complexity, I used devise_security_extension.

1. Installation

Add the gem to your Gemfile:

gem 'devise_security_extension'

Run the generator:

rails generate devise_security_extension:install

Add the modules to your Devise model (e.g., User):

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable,
         :password_expirable, :password_archivable, :expirable, :lockable
end

2. Enforce password history

Uncomment the configuration in the Devise initializer and create the old_passwords table:

# config/initializers/devise.rb
Devise.setup do |config|
  config.password_archiving_count = 5
  config.deny_old_passwords = true
end

Migration for old passwords:

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

3. Password Age and Length

# config/initializers/devise.rb
config.expire_password_after = 1.months
config.password_length = 10..128

Migration for password tracking:

add_column :users, :password_changed_at, :datetime
add_index :users, :password_changed_at

4. Complexity and Lockout

# Complexity Regex: Need uppercase, lowercase, digit and special character
config.password_regex = /(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[\W])/

# Account Lockout
config.maximum_attempts = 5
config.unlock_in = 30.minutes

5. Email Domain Validation

Add a custom validator to your model:

validate :presence_of_domain_in_email

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