1. Code
  2. Ruby
  3. Ruby on Rails

How to Build an Unobtrusive Login System in Rails

Scroll to top
15 min read

An unobtrusive login system is one that gets out of the user’s way. It will make your application nicer and more polished. This article will guide you through the process of setting up user logins, then ajaxifying the process by moving the form into a modal box that communicates with the server. Additionally, this article will show you how to create the setup using jQuery and Prototype so you can choose your favorite library. This article assumes that you have experience with Rails and jQuery or Prototype.


You can use Adman65/nettuts for a successful login. Be sure to use bad credentials so you can see how everything works.

What We're Making

Getting Started

We’re going to start by creating a dummy application that has a public and private page. The root url is the public page. There’s a login link on the public page. If the user logs in successfully, they’re redirected to the private page. If not, they’re redirected back to the login form. The private page shows the user name. We’ll use this as the starting point for ajaxifying the site.

The first step is using the rails command to generate a new application, then install and setup up authlogic.

1
$ cd into-a-directory
2
$ rails unobtrusive-login

Add authlogic.

1
# /config/environment.rb

2
config.gem 'authlogic'

Now install the gems.

1
$ sudo gem install gemcutter
2
$ sudo gem tumble
3
$ sudo rake gems:install

Next create a user model and add the required authlogic columns to the migration.

1
$ ./script/generate model User
2
exists  app/models/
3
exists  test/unit/
4
exists  test/fixtures/
5
create  app/models/user.rb
6
create  test/unit/user_test.rb
7
create  test/fixtures/users.yml
8
create  db/migrate
9
create  db/migrate/20100102082657_create_users.rb

Now, add the columns to the new migration.

1
class CreateUsers < ActiveRecord::Migration
2
  def self.up
3
    create_table :users do |t|
4
      t.string    :login,               :null => false
5
      t.string    :crypted_password,    :null => false
6
      t.string    :password_salt,       :null => false
7
      t.string    :persistence_token,   :null => false
8
      t.timestamps
9
    end
10
  end
11
12
  def self.down
13
    drop_table :users
14
  end
15
end

Migrate the database.

1
$ rake db:migrate

Include authlogic in the user model.

1
# /app/models/user.rb

2
class User < ActiveRecord::Base
3
  acts_as_authentic
4
end

Now we can create a user. Since this is a demo app, web based functionality for signing up isn’t required. So open up the console and create a user:

1
$ ./script/console
2
>> me = User.create(:login => 'Adman65', :password => 'nettuts', :password_confirmation => 'nettuts')

Now we have a user in the system, but we have no way to login or logout. We need to create the models, controllers, and views for this. Authlogic has its own class for tracking logins. We can use the generator for that:

1
# create the user session

2
$ ./script/generate UserSession

Next we need to generate the controller that will login/logout users. You can create sessions just like any other resource in Rails.

1
# create the session controller

2
$ ./script/generate controller UserSessions

Now set its contents to:

1
# /app/controllers/user_sessions_controller.rb

2
class UserSessionsController < ApplicationController  
3
  def new
4
    @user_session = UserSession.new
5
  end
6
7
  def create
8
    @user_session = UserSession.new(params[:user_session])
9
    if @user_session.save
10
      flash[:notice] = "Login successful!"
11
      redirect_back_or_default user_path
12
    else
13
      render :action => :new
14
    end
15
  end
16
end

It looks exactly the same as a controller that was generated via scaffolding. Now create the users controller which has public and private content. Generate a users controller. Inside the controller we’ll use a before filter to limit access to the private areas. The index action is public and show is private.

1
# create the users controller

2
$ ./script/generate controller users

Update its contents:

1
# /app/controllers/users_controller.rb

2
class UsersController < ApplicationController
3
  before_filter :login_required, :only => :show
4
5
  def index
6
  end
7
8
  def show
9
    @user = current_user
10
  end
11
12
  private
13
  def login_required
14
    unless current_user
15
      flash[:error] = 'You must be logged in to view this page.'
16
      redirect_to new_user_session_path
17
    end
18
  end
19
end

You should notice that current_user is an undefined method at this point. Define these methods in ApplicationController. Open up application_controller.rb and update its contents:

1
# application controller

2
class ApplicationController < ActionController::Base
3
  helper :all # include all helpers, all the time

4
  protect_from_forgery # See ActionController::RequestForgeryProtection for details

5
6
  # From authlogic

7
  filter_parameter_logging :password, :password_confirmation
8
  helper_method :current_user_session, :current_user
9
10
  private
11
  def current_user_session
12
    @current_user_session ||= UserSession.find
13
  end
14
15
  def current_user        
16
    @current_user ||= current_user_session && current_user_session.user
17
  end
18
end

At this point the models and controllers are complete, but views aren’t. We need to create views for a login form and the public and private content. We’ll use the nifty-generators gem to create a basic layout.

1
$ sudo gem install nifty-generators
2
$ ./script/generate nifty_layout

Time to create the login form. We’re going to use a partial for this because in the future we’ll use the partial to render just the login form in the modal box. Here’s the code to create the login form. It’s exactly the same as if you were creating a blog post or any other model.

1
# create the login views

2
# /app/views/user_sessions/_form.html.erb

3
<% form_for(@user_session, :url => user_session_path) do |form| %>

4
  <%= form.error_messages %>
5
6
  <p>
7
    <%= form.label :login %>

8
    <%= form.text_field :login %>
9
  </p>

10


11
  <p>

12
    <%= form.label :password %>

13
    <%= form.password_field :password %>

14
  </p>
15
16
  <%= form.submit 'Login' %>

17
<% end %>

Render the partial in the new view:

1
# /app/views/user_sessions/new.html.erb

2
<% title 'Login Please' %>
3
<%= render :partial => 'form' %>

Create some basic pages for the public and private content. The index action shows public content and show displays private content.

1
# create the dummy public page

2
# /app/views/users/index.html.erb

3
<% title 'Unobtrusive Login' %>
4
5
<p>Public Facing Content</p>

6


7
<%= link_to 'Login', new_user_session_path %>

And for the private page:

1
# create the dummy private page

2
# /app/views/users/show.html.erb

3
<% title 'Welcome' %>
4
<h2>Hello <%=h @user.login %></h2>

5


6
<%= link_to 'Logout', user_session_path, :method => :delete %>

Delete the file /public/index.html and start the server. You can now log in and logout of the application.

1
$ ./script/server

Here are some screenshots of the demo application. The first one is the public page.

Public PagePublic PagePublic Page

Now the login form

Login PageLogin PageLogin Page

And the private page

Private PagePrivate PagePrivate Page

And finally, access denied when you try to visit http://localhost:3000/user

Access DeniedAccess DeniedAccess Denied

The AJAX Login Process

Before continuing, we need to understand how the server and browser are going to work together to complete this process. We know that we’ll need to use some JavaScript for the modal box and the server to validate logins. Let’s be clear on how this is going to work. The user clicks the login link, then a modal box appears with the login form. The user fills in the form and is either redirected to the private page, or the modal box is refreshed with a new login form. The next question is how do you refresh the modal box or tell the browser what to do after the user submits the form? Rails has respond_to blocks. With respond_to, you can tell the controller to render different content if the user requested XML, HTML, JavaScript, YAML etc. So when the user submits the form, the server can return some JavaScript to execute in the browser. We’ll use this render a new form or a redirect. Before diving any deeper, let’s go over the process in order.

  1. User goes to the public page
  2. User clicks the login link
  3. Modal box appears
  4. User fills in the form
  5. Form is submitted to the server
  6. Server returns JavaScript for execution
  7. Browser executes the JavaScript which either redirects or updates the modal box.

That’s the high level. Here’s the low level implementation.

  1. User visits the public page
  2. The public page has some JavaScript that runs when the DOM is ready that attaches JavaScript to the login link. That javscript does an XMLHTTPRequest (XHR from now on) to the server for some JavaScript. The JavaScript sets the modal box’s content to the form HTML. The JavaScript also does something very important. It binds the form’s submit action to an XHR with POST data to the form’s action. This allows the user to keep filling the login form in inside the modal box.
  3. Modal box now has the form and required JavaScript
  4. User clicks ‘Login’
  5. The submit() function is called which does a POST XHR to the form’s action with its data.
  6. Server either generates the JavaScript for the form or the redirect
  7. Browser receives the JavaScript and executes it. The browser will either update the modal box, or redirect the user through window.location.

Taking a Peak at the AJAX Ready Controller

Let’s take a look at the new structure for the UserSessions controller.

1
class UserSessionsController < ApplicationController
2
  layout :choose_layout
3
4
  def new
5
    @user_session = UserSession.new
6
  end
7
8
  def create
9
    @user_session = UserSession.new(params[:user_session])
10
11
    if @user_session.save
12
      respond_to do |wants|
13
        wants.html { redirect_to user_path(@user_session.user) }
14
        wants.js { render :action => :redirect } # JavaScript to do the redirect

15
      end
16
    else
17
      respond_to do |wants|
18
        wants.html { render :new }
19
        wants.js # defaults to create.js.erb

20
      end
21
    end
22
  end
23
24
  private
25
  def choose_layout
26
    (request.xhr?) ? nil : 'application'
27
  end
28
end

As you can see the structure is different. Inside the if save, else conditional, respond_to is used to render the correct content. want.xx where xx is a content type. By default Prototype and jQuery request text/JavaScript. This corresponds to wants.js. We’re about ready to get started on the AJAX part. We won’t use any plugins except ones for modal boxes. We’ll use Facebox for jQuery and ModalBox for Prototype.

Prototype

Rails has built in support for Prototype. The Rail’s JavaScript helpers are Ruby functions that generate JavaScript that use Prototype. This technique is known as RJS (Ruby JavaScript). One example is remote_form_for which works like the standard for_for adds some JS bound to onsubmit that submits to the form to its action using its method with its data. I won’t use RJS in this article since I want to demonstrate vanilla JS. I think by using pure JS and eliminating the JS helpers the article will be more approachable by less experienced developers. That being said, you could easily accomplish these steps using RJS/Prototype helpers if you choose.

Adding Prototype to the application is very easy. When you use the rails command, it creates the Prototype and scriptaculous files in /public/JavaScripts. Including them is easy. Open up /app/views/layouts/application.erb and add this line inside the head tag:

1
<%= JavaScript_include_tag :defaults %>

JavaScript_include_tag creates script tags for default files in /public/JavaScripts, most importantly prototype.js, effects.js, and application.js. effects.js is scriptaculous. application.js is a file you can use to keep application specific JS. Now we need a modal box plugin. We’re going to use this. Its a very nice modal box plugin inspired by OSX. The source is hosted on GitHub, so you’ll have to clone and move the files in your project directory. For example:

1
$ cd code
2
$ git clone git://github.com/okonet/modalbox.git
3
$ cd modalbox
4
# move the files in the correct directories.
5
# move modalbox.css into /public/stylesheets
6
# move modalbox.js into /public/JavaScripts
7
# move spinner.gif into /public/images

Now include the stylesheets and JavaScript in your application.

1
2
  <%= stylesheet_link_tag ‘application’ %>

3
  <%= stylesheet_link_tag modalbox %> 
4
  <%= JavaScript_include_tag :defaults %>

5
  <%= JavaScript_include_tag modalbox%>

Now let’s get our login link to open a modalbox. In order to do this we need to add some JavaScript that runs when the DOM is ready that attaches the modalbox to our link. When the user clicks the login link, the browser will do a GET to /user_sessions/new which contains the login form. The login link uses the #login-link selector. Update the login link to use the new id in /app/views/users/index.html.erb. Modify the link_to function like this:

1
<%= link_to 'Login', new_user_session_path, :id => 'login-link' %>

That gives us a#login-link. Now for the JavaScript to attach a modalbox. Add this JS in /public/JavaScripts/application.js

1
document.observe('dom:loaded', function() {
2
    $('login-link').observe('click', function(event) {
3
        event.stop();
4
        Modalbox.show(this.href,
5
            {title: 'Login', 
6
            width: 500}
7
        );
8
    });
9
})

There’s some simple JS for when the user clicks the link a modal box opens up with the link’s href. Refer to the modalbox documentation if you’d like more customization. Here’s a screenshot:

Initial ModalboxInitial ModalboxInitial Modalbox

Notice that inside the modal box looks very similar to our standard page. Rails is using our application layout for all HTML responses. Since our XHR’s want HTML fragments, it make sense to render without layouts. Refer back to the example controller. I introduced a method for determining the layout. Add that to UserSessionsController to disable layout for XHR’s.

1
class UserSessionsController < ApplicationController  
2
  layout :choose_layout
3
4
  def new
5
    @user_session = UserSession.new
6
  end
7
8
  def create
9
    @user_session = UserSession.new(params[:user_session])
10
    if @user_session.save
11
      flash[:notice] = "Login successful!"
12
      redirect_to user_path
13
    else
14
      render :action => :new
15
    end
16
  end
17
18
  def destroy
19
    current_user_session.destroy
20
    flash[:notice] = "Logout successful!"
21
    redirect_to root_path
22
  end
23
24
  private
25
  def choose_layout
26
    (request.xhr?) ? nil : 'application'
27
  end
28
end

Refresh the page and click the link you should get something like this:

Without LayoutWithout LayoutWithout Layout

Fill in the form and see what happens. If you fill in the from with bad info, you’re redirected outside the modal box. If you login correctly you’re redirected normally. According the requirements the user should be able to fill out the form over and over again inside the modal box until they login correctly. How can we accomplish this? As described before we need to use AJAX to submit data to the server, then use JavaScript to update the modal box with the form or do a redirection. We know that the modalbox does a GET for HTML. After displaying the initial modalbox, we need to write JS that makes the form submits itself AJAX style. This allows the form to submit itself inside the modal box. Simply adding this code after the modal box is called won’t work because the XHR might not have finished. We need to use Modalbox’s afterLoad callback. Here’s the new code:

1
document.observe('dom:loaded', function() {
2
    $('login-link').observe('click', function(event) {
3
        event.stop();
4
        Modalbox.show(this.href,
5
            {title: 'Login', 
6
            width: 500,
7
            afterLoad: function() {
8
                $('new_user_session').observe('submit', function(event) {
9
                    event.stop();
10
                    this.request();
11
                })
12
            }}
13
        );      
14
    });
15
})

Form#request is a convenience method for serializing and submitting the form via an Ajax.Request to the URL of the form’s action attribute—which is exactly what we want. Now you can fill in the form inside the modal without it closing. The client side is now complete. What about the server side? The client is submitting a POST wanting JS back. The server needs to decide to either return JavaScript to update the form or render a redirect. In the UserSessionsController we’ll use respond_to to handle the JS request and a conditional to return the correct JS. Let’s begin by handling the failed login case. The server needs to return JS that updates the form, and tells the new form to submit over ajax. We’ll place this template in /app/views/users_sessions/create.js.erb. Here’s the structure for the new create action:

1
def create
2
  @user_session = UserSession.new(params[:user_session])
3
  if @user_session.save
4
    flash[:notice] = "Login successful!"
5
    redirect_to user_path
6
  else
7
    respond_to do |wants|
8
      wants.html { render :new }
9
      wants.js # create.js.erb

10
    end
11
  end
12
end

Now let’s fill in create.js.erb:

1
$('MB_content').update("<%= escape_JavaScript(render :partial => 'form') %>");
2
Modalbox.resizeToContent();
3
$('new_user_session').observe('submit', function(event) {
4
    event.stop();
5
    this.request();
6
});

First we update the content to include the new form. Then we resize the modal box. Next we ajaxify the form just as before. Voilla, you can fill in the form as many times as you want.

Bad InfoBad InfoBad Info
Updated FormUpdated FormUpdated Form

Next we need to handle the redirection case. Create a new file in /app/views/users_sessions/redirect.js.erb:

1
2
  window.location=<%= user_path %>;

Now, update the create action to handle the redirection process:

1
def create
2
  @user_session = UserSession.new(params[:user_session])
3
  if @user_session.save
4
    respond_to do |wants|
5
      wants.html do 
6
        flash[:notice] = "Login successful!"
7
        redirect_to user_path
8
      end
9
10
      wants.js { render :redirect }
11
    end
12
  else
13
    respond_to do |wants|
14
      wants.html { render :new }
15
      wants.js # create.js.erb

16
    end
17
  end
18
end

And that’s it! Now try login with correct credentials and you’re redirected to the private page. For further learning, try to add a spinner and notification telling the user the form is submitting or they’re being redirect. The application still works if the user has JavaScript disabled too.

jQuery

Since I’ve already covered the Prototype process, so I won’t go into the same detail as before. Instead, I will move quickly describing the alternate JavaScript to add to the application. The jQuery vesion will have the exact same structure as the Prototype version. All we need to change is what’s in application.js, create.js.erb, and the JavaScript/css includes.

First thing we need to do is download jQuery and Facebox. Move jQuery into /public/JavaScripts as jquery.js. For facebox move the images into /public/images/, stylesheets into /public/stylesheets, and finally the JS into /public/JavaScripts. Now update /app/views/layouts/application.html.erb to reflect the changes:

1
<head>
2
  <title><%= h(yield(:title) || "Untitled") %></title>

3
  <%= stylesheet_link_tag 'facebox' %>
4
  <%= stylesheet_link_tag 'application' %>    

5
  <%= JavaScript_include_tag 'jquery' %>
6
  <%= JavaScript_include_tag 'facebox' %>

7
  <%= JavaScript_include_tag 'application' %>
8
</head>

Facebox comes with a default stylesheet which assumes you have your images in /facebox. You’ll need to update these selectors in facebox.css like so:

1
#facebox .b {
2
  background:url(/images/b.png);
3
}
4
5
#facebox .tl {
6
  background:url(/images/tl.png);
7
}
8
9
#facebox .tr {
10
  background:url(/images/tr.png);
11
}
12
13
#facebox .bl {
14
  background:url(/images/bl.png);
15
}
16
17
#facebox .br {
18
  background:url(/images/br.png);
19
}

Now we attach facebox to the login link. Open up /public/JavaScripts/application.js and use this:

1
$(document).ready(function() {
2
    $('#login-link').facebox({
3
        loadingImage : '/images/loading.gif',
4
    closeImage   : '/images/closelabel.gif',
5
    });
6
});

I override the default settings for the images to reflect the new image path. Start the sever and head over to the index page. You should see a nice facebox with the login form:

FaceboxFaceboxFacebox

Next thing we have to do is set the form to submit itself via AJAX. Just like before, we’ll have to use callbacks to execute code after the modal box is ready. We’ll use jQuery’s post method for the XHR request. Facebox has an after reveal hook we can use. application.js:

1
$(document).ready(function() {
2
    $('#login-link').facebox({
3
        loadingImage : '/images/loading.gif',
4
        closeImage   : '/images/closelabel.gif',
5
    });
6
7
    $(document).bind('reveal.facebox', function() {
8
        $('#new_user_session').submit(function() {
9
            $.post(this.action, $(this).serialize(), null, "script");
10
            return false;
11
        });
12
    });
13
});

Updating create.js.erb should be easy enough. We have to update the facebox’s contents and re-ajaxify the form. Here’s the code:

1
$('#facebox .content').html("<%= escape_JavaScript(render :partial => 'form') %>");
2
$('#new_user_session').submit(function() {
3
    $.post(this.action, $(this).serialize(), null, "script");
4
    return false;
5
});

And that’s it! Here’s the final product:

Logging InLogging InLogging In
Bad LoginBad LoginBad Login
RedirectedRedirectedRedirected

Downloading the Code

You can get the code here. There are branches for each library so you can check out the Prototype or jQuery versions. Any questions, comments, concerns? Thanks again for reading!

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.