Building authentication for applications is a run of the mill task we have all encountered at some point or another. In the past, most developers would have reached into their tool belt and pulled out restful authentication. Lately, a new kid on the block has been stealing a lot of thunder where authentication is concerned, and with good reason.
Devise provides a complete authentication solution. Views, mailers and a host of common authentication helpers such as registration, confirm via email, and the ability to lock accounts come as standard features out the box.
Rolling your own authentication system can be a good exercise and, in most cases, is not too big a chore. Having something stable, feature rich and modular like Devise can be a big time saver.
Today, we are going to look at implementing Devise in a real world scenario. While the base features of Devise are well-known, its flexibility is somewhat overlooked. We will look at customizing how users log in.
A common idiom in multi-tenant applications these days is to have subdomains defined as the users account, for example rubysource.basecamp.com
could be a fictional Basecamp login for Rubysource. Let’s do this with Devise.
The Application
We will start with the essence of our application first. It will be a Rails 3.1-based note taking application. At this stage, we will keep it really simple and only have three models in our domain, Account
, Note
and User
. Let’s crank up some generators for Account
and Note
and leave User
for Devise.
rails g model Account subdomain:string
rails g scaffold Note title:string content:text user_id:integer account_id:integer
In our Account
model we will create the associations,
class Account < ActiveRecord::Base
has_many :users
has_many :notes, :through => :users
end
Similarly for Note
we build the associations as follows.
class Note > ActiveRecord::Base
belongs_to :user
belongs_to :account
end
Nothing special here, just a couple of ActiveRecord
models with associations. The only point of interest is the subdomain
attribute on the Account
model. This is essentially the slug for the account in the request URL (“rubysource” in our previous example).
Introducing Devise
Devise is a gem, so very straight forward to install. Add Devise to your Gemfile, gem devise
then run bundle install
.
Now on the command line run rails g
and check out the generators Devise adds to our application.
Devise:
devise
devise:form_for
devise:install
devise:shared_views
devise:simple_form_for
devise:views
We will use the obvious rails g devise:install
to get Devise running inside our application.
After running the generator we should get a couple of instructions telling us to set the root of the application to map somewhere, have a flash area in our layout and setting the default mailer in our development and test environments. Just follow those to the letter. I have set the root of the application to point to notes#index
.
We need a user model for Devise to authenticate. As it turns out, we can use devise to generate that for us rails g devise User
. This generates the model,corresponding migration and the specialized devise_for
route in our config/routes.rb
file.
Before running the migrations we need to modify the User
model to associate the Account
. We also associate Note
as a has_many
.
I should point out this is the best time to decide what modules Devise will use such as adding :confirmable
to the migration, but for now we will just use the default given to us by the generator.
With Devise installed, the easiest way to protect our content is with the blanket before_filter :authenticate_user!
placed in the ApplicationController
. Starting up the server and navigating to localhost:3000
, the generic Devise login form is displayed.
Knowing it Works
Now we have Devise in place, it is not doing quite what we want just yet. We need to authenticate against the subdomain. There are a couple of options in terms of testing. True, we do not really want to test Devise itself. It’s a third party, well- used piece of software, so it’s fair to say it has been well tested. However, we still want to ensure our customizations behave the way we expect. We could do some work starting up a local server and using the brilliant lvh.me
domain so we do not have to keep adding subdomains to our hosts file.
But aren’t we are better than that? We want reproducible, automated tests. Let’s use our integration tests and the awesomeness that is Capybara. Capybara, by default, runs our suite under the domain www.example.com
. However by simply using the app_host
helper we can explicitly specify the domain the test runs under. Here is a basic test of the behavior we are looking for using RSpec requests.
describe "LoginToAccounts" do
before do
other_account = Factory.create(:account)
@invalid_user = Factory.create(:user, account: other_account)
@account = Factory.create(:account, subdomain: "test-account")
@user = Factory.create(:user, account: @account)
Capybara.app_host = "http://test-account.example.com"
visit '/'
end
describe "log in to a valid account" do
before do
fill_in 'Email', with: @user.email
fill_in 'Password', with: @user.password
click_button 'Sign in'
end
it "will notify me that I have logged in successfully" do
page.should have_content "Signed in successfully"
end
end
describe "fail login for valid user wrong account" do
before do
fill_in 'Email', with: @invalid_user.email
fill_in 'Password', with: @invalid_user.password
click_button 'Sign in'
end
it "will not notify me that I have logged in successfully" do
page.should_not have_content "Signed in successfully"
end
end
end
Devise Flexibility
Now that we have a test, it’s time to implement the functionality of capturing the subdomain and making sure the user is authenticating into the correct account.
When we installed Devise, it generated an initializer, aptly named devise.rb
in the config/initializers
directory. This file is brilliantly commented and should be the first point of call when looking to add functionality to the vanilla install of Devise.
You will see on line 33 of this file (based on installing version 1.4.7 of Devise) that we can add request keys for authentication. Simply uncomment the line and add :subdomain
to the array of keys. This will basically push this request parameter to Devises authentication method to test against.
Even though that was simple enough, we are not quite there yet. Remember that Devise authenticates on User
and the subdomain attribute is with the Account
. Now, we could simply add a subdomain attribute to user and be done, but that just seems plain wrong to me.
Instead, we will override the find_for_authentication
method used by Devise. This method is our dropping off point before Devise takes over. To find the Account we are looking for, we will grab the subdomain from the request and pass it up the chain to be authenticated. Using Rails 3+ makes this really easy. In the User
model simply add
def self.find_for_authentication(conditions={})
conditions[:account_id] = Account.find_by_subdomain(conditions.delete(:subdomain)).id
super(conditions)
end
Here we are popping out the subdomain
parameter and doing a find by on the Account
and pass it up to the find_for_authentication
method.
With all this in place we can run the tests and make sure we have a passing suite. The demo code can be found on Github.
Where to Next?
Our naive little application has authentication and no unauthorized users can access our all-important notes. We still have some work to do regarding what notes are displayed for what login. That is solved with the common multi-tenant idiom of making all our data in the controller extract down from the “current tenant”, like this on by DHH.
That is not really what we were looking at here today. Normally, I would avoid testing third party code, but the behavior of our application and the changes we made require that we at least tip our hat to Devise. Wrapping this as an integration test is the best place to do so and using a tool like Capybara makes it easy, so it would be criminal not to be thorough.
Authentication is a pattern we will do over and over again when it comes to Rails applications, and something as feature rich as Devise can be intimidating. Hence, why some look to simpler gems such as Restful Authentication, Sorcery or just roll our own. I recommend you invest some time in Devise. With a good understanding you can almost write off the authentication element out of your application and use Devise as a drop in replacement. It’s design is so great and personally I have yet to find myself in the situation where I am fighting against Devise. It’s usually a case of hooking into it to create exactly what I need.
Frequently Asked Questions (FAQs) about Basecamp-like Subdomains with Devise
How can I set up user authentication with Devise in a Rails application?
Setting up user authentication with Devise in a Rails application involves several steps. First, you need to add the Devise gem to your Gemfile and run the bundle install command. Next, you need to run the Devise generator, which will create several files in your application, including the Devise initializer. You can then generate a Devise model, which will be used for authentication. After setting up the model, you can add authentication to any controller by using the authenticate_user! before_action. Finally, you can customize the views and controllers generated by Devise to suit your application’s needs.
How can I create Basecamp-like subdomains with Devise?
Creating Basecamp-like subdomains with Devise involves setting up a custom domain constraint in your routes file. This constraint will match any subdomain and route it to the appropriate controller. You can then use the request.subdomain method in your controllers to access the current subdomain. Additionally, you can set up a before_action in your ApplicationController to redirect users to their subdomain if they are logged in.
How can I log in a user with Devise?
Logging in a user with Devise is straightforward. You can use the sign_in method provided by Devise, passing in the user you want to log in. This method will set the user in the session, making them the current user for the duration of the session. You can then use the current_user method provided by Devise to access the currently logged in user.
How can I customize the Devise views?
Devise provides a generator that you can use to copy its views into your application for customization. You can run this generator with the command rails generate devise:views. This will copy all the Devise views into your app/views/devise directory, where you can customize them to suit your application’s needs.
How can I add additional fields to the Devise registration form?
To add additional fields to the Devise registration form, you need to customize the Devise views and the Devise controllers. First, you can add the fields to the registration form view. Then, you need to permit these additional parameters in the Devise controller by overriding the sign_up_params and account_update_params methods.
How can I handle subdomain routing in Rails?
Handling subdomain routing in Rails involves setting up a custom domain constraint in your routes file. This constraint will match any subdomain and route it to the appropriate controller. You can then use the request.subdomain method in your controllers to access the current subdomain.
How can I redirect users to their subdomain after login with Devise?
To redirect users to their subdomain after login with Devise, you can set up a before_action in your ApplicationController. This before_action can check if the user is logged in and if their current subdomain matches their user subdomain. If not, it can redirect them to their subdomain.
How can I handle authentication errors with Devise?
Devise provides several methods for handling authentication errors. The most common method is to use the flash messages provided by Devise. These flash messages are automatically displayed when an authentication error occurs. You can customize these messages by overriding the devise_error_messages! method in your application.
How can I log out a user with Devise?
Logging out a user with Devise is as simple as calling the sign_out method, passing in the user you want to log out. This method will remove the user from the session, effectively logging them out of the application.
How can I reset a user’s password with Devise?
Devise provides a built-in mechanism for resetting a user’s password. This involves sending the user an email with a link to a password reset form. The user can then enter their new password, which will be saved and used for future logins. You can customize the password reset views and controllers to suit your application’s needs.
Dave is a web application developer residing in sunny Glasgow, Scotland. He works daily with Ruby but has been known to wear PHP and C++ hats. In his spare time he snowboards on plastic slopes, only reads geek books and listens to music that is certainly not suitable for his age.