4 min read

How to Add Password Validation to Your Devise Models

Devise does do some password validation on its own, but that’s mostly front-end or controller-based, not on the model itself. If you want to add additional password validation to your Devise models you have to be careful to add it in a way that accommodates how Devise stores and processes passwords. To wit: Devise never actually stores the raw password on the model, even though there’s a field for it. That’s because as soon as your model is saved with something in the password field Devise takes it out, encrypts it, and then stores it in the encrypted_password field.

Therefore, you cannot do something like:

class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable,
:token_authenticatable
 

before_save :ensure_authentication_token

validates :username, :email, :password, presence: true
validates :username, :email, uniqueness: true
validates :password, length: { minimum: 8 }
 
end

This would run the validations every time the model is saved, so on updates you’d need to pass in the raw password again or the validations would fail. You’d never be able to do an automated update of the model without first disabling the validations! To get around these, you need to make the validations for the password field be conditional.

class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable,
:token_authenticatable
 
validates :username, :email, presence: true
validates :username, :email, uniqueness: true
validates :password, length: { minimum: 8 }, unless: "password.nil?"
validates :password, presence: true, if: "id.nil?"
 
end

When you add an ‘unless’ condition to a validation, you can specify it as a string that will be executed as ruby code within the context of the model instance being validated. In this case, we’ll validate the password’s presence ONLY if the model is new (and therefore has no ID) and we’ll validate the password’s length only if it’s set to something. This still requires the password to be set in cases where it’s actually required (creation) but not when it isn’t (updates where you’re not updating the password).

HOWEVER, this does bring up one other important concern: Depending on how you’ve structured your account update forms, you may need to add some additional logic to your update controller. If your account update controller has the fields for updating your password in it, you need to be aware that the form will submit itself with an empty string for the contents of those fields. That means your updates will still trigger the password length validation because it’s not nil. However, you can’t just change the validation to:

# DON'T DO THIS
validates :password, length: { minimum: 8 }, unless: "password.blank?"

Because that would allow an empty string as a password. You have two options to work around this: separate your password update process into a different form, or add conditional logic to your update controllers. In the first scenario you don’t have to change anything in your controller, as the fields simply won’t exist unless you’re actually changing the password. However, if the design requires everything be in the same form you’ll need to do the following in your RegistrationsController:

class RegistrationsController < Devise::RegistrationsController
def account_update_params
if params[@devise_mapping.name][:password_confirmation].blank?
params[@devise_mapping.name].delete(:password)
params[@devise_mapping.name].delete(:password_confirmation)
end
 
super
end
end

This overrides the method that Devise uses to get authorized params in Rails4+ apps. You’re essentially checking to see if password_confirmation is blank (nil, empty string, etc.) and if it is you can assume it’s not a case where you want to interact with the password field at all and can forcibly remove the password fields from the params hash. Note that Hash#delete does NOT throw an exception if the key is not found in the hash so you can safely try to delete anything you might not want in a given scenario.