Skip to content

Instantly share code, notes, and snippets.

@lorenadl
Last active May 27, 2025 09:17
Show Gist options
  • Select an option

  • Save lorenadl/851e19a75d68182e08aa199b78c0ab43 to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown

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