I had the same issue. Here's the solution I found.
It digs a bit into the devise internals so I recommend that you test this. I found I broke a lot of stuff while trying to implement this (including not requiring a matching password confirmation, or changing the password!)
Here's a gist showing the snippet of my test suite that checks password resets: https://gist.github.com/samstickland/9744ee29d0028162a7a8
The problem is that devise uses validations to check the passwords match, that it's a valid password (i.e. not too short) and that the password token is valid. This means that once you stop using the valid? method you have to instead look for particular error messages.
Firstly, you have to override the reset_password method in your user model. This new method will write the encrypted password directly (which is created as part of the password= method) if the error messages doesn't contain any 'password' errors.
The original devise implementation is found here: https://github.com/plataformatec/devise/blob/18a8260535e5469d05ace375b3db3bcace6755c1/lib/devise/models/recoverable.rb#L39 (NB: I haven't implemented after_password_reset as it's deprecated)
def reset_password(new_password, new_password_confirmation)
self.password = new_password
self.password_confirmation = new_password_confirmation
if valid?
save
elsif errors.messages.select { |k,v| k[/password/] }.empty?
# No password errors, so we write the password directly to the DB
update_attribute(:encrypted_password, encrypted_password)
true
else
false
end
end
Next you have to implement your own PasswordsController as the update method will also check to see if the errors are empty. Change it to just look for password errors. Here's mine.
I've also had to change to respond_with to a redirect_to to make the redirect work when you reset a password on an invalid model. I don't really understand why this was necessary though.
#
# This is identical to the original devise password controller except
# that it allows resetting of passwords in invalid models (i.e.
# confirmed users without a valid profile
#
class Users::PasswordsController < Devise::PasswordsController
# GET /resource/password/new
# def new
# super
# end
# POST /resource/password
# def create
# super
# end
# GET /resource/password/edit?reset_password_token=abcdef
# def edit
# super
# end
# PUT /resource/password
def update
self.resource = resource_class.reset_password_by_token(resource_params)
yield resource if block_given?
# Was:
# if resource.errors.empty?
# It's been changed to allow resetting passwords of invalid models
if resource.errors.messages.select { |k,v| k[/password/] }.empty?
resource.unlock_access! if unlockable?(resource)
if Devise.sign_in_after_reset_password
flash_message = resource.active_for_authentication? ? :updated : :updated_not_active
set_flash_message(:notice, flash_message) if is_flashing_format?
sign_in(resource_name, resource)
# Was:
# respond_with resource, location: after_resetting_password_path_for(resource)
# but this didn't seem to redirect User's with invalid attributes so I've changed
# it to a redirect
redirect_to after_resetting_password_path_for(resource)
else
set_flash_message(:notice, :updated_not_active) if is_flashing_format?
respond_with resource, location: new_session_path(resource_name)
end
else
respond_with resource
end
end
# protected
# def after_resetting_password_path_for(resource)
# super(resource)
# end
# The path used after sending reset password instructions
# def after_sending_reset_password_instructions_path_for(resource_name)
# super(resource_name)
# end
end