Skip to content

Instantly share code, notes, and snippets.

@lorenadl
Last active October 27, 2023 07:35
Show Gist options
  • Save lorenadl/851e19a75d68182e08aa199b78c0ab43 to your computer and use it in GitHub Desktop.
Save lorenadl/851e19a75d68182e08aa199b78c0ab43 to your computer and use it in GitHub Desktop.
[RoR] Add password expiration feature to Devise

Add password expiration feature to Devise

Assuming you already have a Devise model named User and you want to add following Devise Security Extension to it:

  • Password Expirable
  • Password Archivable
  • Session Limitable

Add gem and run the generator

Add devise-security (https://github.com/devise-security/devise-security) gem to Gemfile:

gem 'devise-security', '~> 0.12.0'

and run

web_app$ bundle install

Note: version 0.12.0 of the gem gives a lot of error in my case. Downgrading gem version fixed the problems:

  • uninstall the gem (ok for RVM):

    bundle exec gem uninstall devise-security

  • change the gemfile:

    gem 'devise-security', '~> 0.11.1'

  • run bundler:

    web_app$ bundle install

Run the generator:

web_app$ rails generate devise_security:install

The generator adds optional configurations to config/initializers/devise-security.rb.

Configuration

Enable the modules you wish to use in the initializer.

For example to enable password expiration set the proper time interval in the config.expire_password_after parameter:

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

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

  # Need 1 char of A-Z, a-z and 0-9
  # config.password_regex = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z])/

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

  # Deny old password (true, false, count)
  # config.deny_old_passwords = true

  # enable email validation for :secure_validatable. (true, false, validation_options)
  # dependency: need an email validator like rails_email_validator
  # config.email_validation = true

  # 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
end

Prepare the model

Now add Devise Security modules on top of Devise modules to any of your Devise models:

devise :password_expirable, :secure_validatable, :password_archivable, :session_limitable, :expirable

for :secure_validatable you need to add:

gem 'rails_email_validator'

Example:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
         :confirmable, :lockable, :timeoutable,
         :password_expirable, :password_archivable, :session_limitable # Devise Security Extensions
  ...

Generate the migrations

https://github.com/devise-security/devise-security#schema

https://gist.github.com/jiggneshhgohel/52bcd562e937ec4dad7b

Generate the migration for the Passowrd Expirable module

web_app$ rails g migration AddDeviseSecurityExtensionPasswordExpirableColumnsToUsers

Open the generated migration and add the following code:

class AddDeviseSecurityExtensionPasswordExpirableColumnsToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :password_changed_at, :datetime
    add_index :users, :password_changed_at    
  end
end

Generate the migration for Session Limitable module

web_app$ rails g migration AddDeviseSecurityExtensionSessionLimitableColumnsToUsers

Open the generated migration and add the following code:

class AddDeviseSecurityExtensionSessionLimitableColumnsToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :unique_session_id, :string, limit: 20
  end
end

Generate the migration for Password Archivable module

web_app$ rails g migration CreateDeviseSecurityExtensionPasswordArchivableOldPasswordsTable

Open the generated migration and add the following code:

class CreateDeviseSecurityExtensionPasswordArchivableOldPasswordsTable < ActiveRecord::Migration[5.0]
  def change
    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
  end
end

Run the migrations

web_app$ rake db:migrate

Create the view

If you need to customize the Password Renewal view, create the file show.html.haml under /views/devise/password_expired/. Example content:

# /views/devise/password_expired/show.html.haml

- content_for :breadcrumbs, t('.password_expired')

%h1= t('.renew_your_passord')
%h2= t('.password_expired')

.row
  .col-md-4.col-md-offset-4
    .cvForm
      = form_for(resource, :as => resource_name, :url => [resource_name, :password_expired], :html => { :method => :put }) do |f|
        %h4= t('.renew_your_password')

        = devise_error_messages!

        .form-group
          = f.label :current_password, t('.current_password')
          = f.password_field :current_password, autofocus: true, class: 'form-control'

        .form-group
          = f.label :password, t('.new_password')
          = f.password_field :password, class: 'form-control'

        .form-group
          = f.label :password_confirmation, t('.confirm_new_password')
          = f.password_field :password_confirmation, class: 'form-control'

        = f.submit t('.change_my_password'), class: 'btn btn-default btn-lg'

.row.top-spaced.center
  = back_button root_path, t(:work_with_us_home)

Translations

The devise_secutiry generator authomatically creates the ready to use language files:

config\locales\devise.security_extension.it.yml
config\locales\devise.security_extension.en.yml
config\locales\devise.security_extension.de.yml

To translate the password expired/renew password view, add the following rows to the devise translation files in /config/locales/:

# /config/locales/devise.it.yml
it:
  devise:
    password_expired:
      show:
        password_expired: Password scaduta
        renew_your_passord: Rinnova la tua password
        current_password: Password corrente
        new_password: Nuova password
        confirm_new_password: Conferma nuova password
        change_my_password: Cambia la mia password
# /config/locales/devise.en.yml
en:
  devise:
    password_expired:
      show:
        password_expired: Password expired
        renew_your_passord: Renew your password
        current_password: Current password
        new_password: New password
        confirm_new_password: Confirm new password
        change_my_password: Change my password

Gem versions

devise-security gem version 0.11.1 requires devise < 5.0, >= 4.2.0. Newer versions of devise require ruby > 2.2.1 and rails > 4.2.5.1.

The following is the combination of changes with the minumun impact on older rails apps:

  • ruby-2.3.1
  • gem 'rails', '5.0.2'
  • gem 'devise', '~> 4.2.1'
  • gem 'devise-security', '~> 0.11.1'

To update the gems, delete Gemfile.lock and run bundle install.

@432i
Copy link

432i commented Oct 5, 2022

thank you very much!!!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment