This is a follow on from my last post, Writing a Ruby on Rails application to fetch data from Basecamp. At the end of that post we had an app which fetched a project list from Basecamp, but we had hard coded our Basecamp API token into the application. In this post we are going to create a login form. The form will prompt for a Basecamp username and password and use them to fetch and store the API token which can then used in all further Basecamp API calls in that session.
Before we start however, I'll talk a bit about what I mean by a "session" in a web based application.
Browsers, Cookies and Sessions
When web browsers were first developed all requests from the browser to fetch web pages from the server were stateless, that is each request was independent of any other and there was no way of linking one request to any other requests.
For example, if Harry and Jane are both browsing the same web site, the web server has no way of knowing which requests for web pages are from Harry and which are from Jane.
This was all fine when web pages were purely static. It didn't matter if the request came from Harry or Jane, they'd get the same page back anyway. It does however become a problem once we start looking at web applications. A banking web site, for example, has to show only Harry's bank account details to Harry and only Jane's back account details to Jane. This is where cookies come into the picture.
A cookie is simply a piece of information that a web server can store in a browser when the browser fetches a web page. When the browser requests another page from the server it will send the cookie back to the server. The cookie is something that links together the two requests, so the requests are no longer stateless.
Cookies can have an expiration date set. Cookies can also be set to expire when the user quits from the browser. These cookies are called session cookies. Session cookies are what we'll be using in this example.
Sessions in Rails
In Rails, a special hash called session is used to reference session cookies. It's very simple to use, any element you put into the session hash will be available from then on in all requests from the same browser, until the use quits the browser.
For example, if you want to store the variable userid in a session you would do:
session[:userid] = userid
then in later requests you can get the userid from session[:userid].
In our case we'll be using the session hash to store our Basecamp API token.
Creating a Login Form
The steps our login code will be doing are:
- Every time the browser requests a page that needs to access the Basecamp API, check if we already have a Basecamp API token.
- If we don't have an API token, redirect to the page with the login form.
- When the user submits the form, use the given username and password to log in to Basecamp and get the user's API token.
- Redirect back to the original page.
To implement step 1 on our list we are going to use something called a before filter. A before filter is some code that runs before the action in the controller is called.
We will set up our before filter in the file app/controllers/application_controller.rb. This file will already exist in your rails directory. If you look at it, you will see that the first line (apart from some comments) is:
class ApplicationController < ActionController::Base
Now have a look at the first line in app/controllers/projects_controller.rb (which we created in the last blog post):
class ProjectsController < ApplicationController
What we can see is that the ProjectsController class (and in fact all controller classes) inherits from ApplicationController, which in turns inherits from ActionController::Base.
ActionController::Base is the class that implements controllers in Rails, but having that intermediate ApplicationController class gives us a place to put stuff that we want to apply to all our controllers. That's where we are going to put the before filter.
So now add the lines in bold below to app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base helper :all # include all helpers, all the time protect_from_forgery# See ActionController::RequestForgeryProtection for details # Scrub sensitive parameters from your log filter_parameter_logging :password before_filter :check_token protected def check_token unless session[:token] session[:original_url] = request.url redirect_to :controller => :logins, :action => :new end end end
The line before_filter :check_token says that our before filter is in the method check_token.
Rails 3 Update: The filter_parameter_logging option has now moved to config/application.rb and the syntax has changed.
We are going to store our Basecamp API token in session[:token], so the first thing we do in check_token is check if we already have a token or not. If we don't have a token then we will redirect to the new action of the logins controller, which is where we'll do the login form.
Before we do the redirect however, we save away our current URL (in session[:original_url]) so we know where to return to once we have logged in.
The Login Form
Here is the view template for our login form, from app/views/logins/new.html.erb
<% form_tag :action => :create do %> <%= label_tag :user, 'Username: ' %><br> <%= text_field_tag :user %><br> <%= label_tag :password, 'Password: ' %><br> <%= password_field_tag :password %><br> <%= submit_tag 'Login' %> <% end %>
In this there are the <%=...%> tags that we saw in the last blog post, but there are also some <%...%> tags that we haven't seen before. Both tags are for running Ruby code, but the difference is that ones without the equals sign will not have the output of the code written to the web page.
Rails 3 Update: form_tag now needs to be inside <%=...%> rather than <%...%>.
The form_tag :action => :create will generate HTML for a form. When the form is submitted it will call the create action to process the form. Everything between the do and the end is part of the form, i.e. the labels, fields, submit button, and any other HTML in the form.
The code for the create action to process the form will be in app/controllers/logins_controller.rb:
class LoginsController < ApplicationController skip_before_filter :check_token def create Basecamp.establish_connection!('myhost.basecamphq.com', params[:user], params[:password], true) session[:token] = Basecamp.get_token redirect_to session[:original_url] end end
First of all we have to tell LoginsController not to call the check_token before filter, otherwise we'd just end up in an endless loop.
The Basecamp.establish_connection! line is logging in to Basescamp with the user and password we gave in the form. Ruby will put the parameters from the form in the hash params when calling the controller action, so params[:user] and params[:password] will contain the user and password fields from the form.
Once we've logged in, we get the API token for the user and store it as session[:token] so we can use it from then on the rest of our app. Finally we redirect back to the page we originally tried to access, which we had stored in session[:original_url].
Now that we have our API token, there's one more thing we have to do, which is to modify the projects controller to use the stored token, i.e.:
class ProjectsController < ApplicationController def index Basecamp.establish_connection!('myhost.basecamphq.com', session[:token], 'X', true) @projects = Basecamp::Project.find(:all) end end
Is Anything Missing?
Going back and looking at the logins controller and view, it looks like new has a view template but no controller action, and create has a controller action but no view template. Is that right?
Well taking create first, at the end of the controller it does a redirect back to the saved URL, so it never actually has to display a page of its own.
On the other hand, new doesn't need to do anything other than to just display the form. As a controller action renders the corresponding template by default, we don't need to specify anything in the controller. We could have an action in the controller that just looks like this:
def new end
but Rails actually lets us leave even that out. This is another case of convention over configuration.
Trying it Out
Rails 3 Update: You will also need to add resources :logins to config/routes.rb.
If you now navigate in your browser to http://localhost:3000/projects (don't forget to run script/server if it isn't already running), you should get redirected to the login form. You can now log in an see your projects.
What happens though if you get your username and password wrong. Try it and see what happens...
That wasn't very pretty was it, we just get some cryptic error messages and a stack trace. What we would really like to see is a friendly error message, and to go back to the login form to try again. Well that's going to be the subject of my next post.