Article summary
This week I’ve started adding real authentication to our application. We’re running the latest and greatest Rails, and we’re using ActiveRecord, so Devise looks like an excellent choice for authentication.
Except I’m running into a problem getting the basics wired up. In fact, mass assignment rules are getting in my way. Am I missing something?
Context
Instead of getting into unnecessary details about my application, I’m going to use Daniel Kehoe’s handy rails3-devise-rspec-cucumber Rails application as a starting point.
With the application setup and running, view the Login page and click the “Sign in” button with an empty form. You’ll get an “Invalid email or password” error. Cool. But now open up app/models/user.rb
and remove password
and password_confirmation
fields from the attr_accessible
list. Now submit the empty for again. Uh oh! Now we get an application error about how password is not mass assignable.
My first reaction to this was “why the hell is Devise trying to mass assign the password, especially on login?!?” I did some digging and found that there’s an interaction going on between Devise’s Devise::SessionsController#create
and Devise::SessionsController#new
. The create
action is rejecting the login, as it should, and then running the new
action. The new
action then creates a new User
object from the parameters. Now the first time you hit the new
action, there are no parameters, so all is well. But when new
is run in the context of create
‘s parameters, we get a mass assignment error since password
is in the parameters.
The Workaround
Assuming I want to keep password
off the attr_accessible
list, the best I could think of was to create my own controller that derives from Devise::SessionsController
and configure Devise to use it for my session routes. Then, based on an idea Patrick gave me, I created a before_filter
for the new
action that strips the user
parameter. Here’s the code.
class SessionsController < Devise::SessionsController
before_filter only: :new do
params.delete(:user)
end
end
Pretty small. The downside here is that the other form fields, like Email and Remember me don’t get re-rendered. That’s a little obnoxious, but not the end of the world. At a later date I can write some fancier code to preserve those values.
A Question
So now my question is: am I missing something? It seems like password
is an excellent candidate for a field that should not be mass assignable1. But perhaps in this context it’s ok? Or am I misusing Devise’s built in controllers?
Any insight here is appreciated.
1 I know little about the details, but GitHub’s recent mass assignment exploit has my mind particularly attuned to mass assignment protection, especially since I happen to be implementing login this week.
It’s been a wee while since I looked at Devise in detail but are you running Rails 3.2? Maybe this didn’t show up pre-3.2 because invalid mass-assignment used to just display a warning. So, it never caused a problem. Now, mass-assignment is illegal, you are getting an error.
DHH posted this gist which shows how you they are handling mass-assignment.
https://gist.github.com/1975644
I agree with you, Devise should not be using mass assignment.
If you use Devise with Mongoid, the generated model will not include the attr_accessible declaration because Mongoid does not have a whitelist_attributes-like functionality enabled by default the way that Rails’ ActiveRecord does. If you subsequently enable it, you will have problems with Devise unless you add attr_accessible to your model.
Perhaps Devise is using mass assignment in order to allow developers to add arbitrary fields to their Devise models? If they were not using mass assignment, the custom fields would get lost. I have to imagine there’s a better way to support that though.