Rails #5: Security

In my previous articles, I have showed you how to create a simple blog application with articles, comments, rss feeds and formatting. However, as it is currently written, the application allows for anyone to create or edit an article. This is a serious security issue, and we better fix it.

In this tutorial, I will show you how to make sure that only logged in users can create articles, and that nobody else can edit an article that you created.

The task at hand

This is basically what I want to say:

In plain English: Verify that the user is logged in before any other actions than index and show are performed. This boils down to the method current_user returning a non-nil result. Use the current_user as starting point when creating or updating articles.

Now we just need to create the login functionality. This means some way of creating users and making these users log in. There is actually a plugin called restful_authentication that does this for you, but it’s not that hard, so I though we could do it on our own.

The users model

The key to this business is obviously the verify_logged_in method. Let’s try this:

What is this current_user thing?

Here, we use a session variable to store the user_id, and look up the a User model using this session variable. You should never store ActiveRecord objects in session variables.

By now, it is becoming blindingly obvious that we need a User model. Let’s use the generator:

ruby script/generate model User username:string password:string

We also want to associate the articles with users. Add has_many :articles in app/models/user.rb, and belongs_to :user in app/models/article.rb. Also add the following to your newly generated migration in db/migrate/003_create_users.rb:

You can now execute rake db:migrate to update the database structure.

Logging in

Next order of business: verify_logged_in contained the line redirect_to new_login_session_path. If you guessed that we are going to have a resource called “login_session”, and that this is just like when we said “new_comment_path”, you guessed right! This means we have to add the following to config/routes.rb: map.resource :login_session.

A user should only be aware of one login_session, so we say map.resource (singular) instead of map.resources plural. A typo here can be very confusing.

Now, create the LoginSessionsController by executing ruby script/generate controller LoginSessions new create show destroy on the command line. We can now update our new app/controllers/login_sessions_controller.rb. Notice how we use the conventions from other restful controllers of standardized action names for new, create, show and destroy, even though there is no login_session model:

The form for logging in is located in app/views/login_sessions/new.html.erb and looks like this:

I have also made it possible to log out from the show-view by having a logout “form” in app/views/login_sessions/show.html.erb:

(I’m not totally happy with this – suggestions for improvements are welcome!)

Everything now works – trust me!

Testing the code

Okay, don’t trust me. God knows I wouldn’t trust me. You should obviously test your code. Normally, you would test this sort of logic using test classes, but I haven’t covered this yet, so we’ll have to be satisfied with manual testing for now.

In article 4 in this series, I introduced fixtures. Fixtures are located under test/fixtures, and when you used the generator to create the user model, a fixture was generated for you as well in test/fixtures/users.yml. Update this to contain two test users with passwords. I use the following:

Also, make sure that the article fixtures in test/fixtures/articles.yml contain reference to the users. Here is one of my articles:

You can now reload the fixtures with rake db:fixtures:load. Then test as follows:

  1. Go to an article and press the “edit” link. You should now be redirected to the login page
  2. Enter the username and password to your user who own the article from test/fixtures/users.yml and press “Log in”. You should now be taken to a page that shows your login status
  3. Navigate back to the article edit page. You should now be able to edit the article.
  4. Navigate back to the login status page (http://localhost:3000/login_session) and press “Log out”. You should now be returned to the login form.
  5. Log in as a user who does not own the article.
  6. Navigate back to the article edit page. You should now be presented with an error message stating that there is no article with this id and user_id.

Our rudimentary security now works correctly, and we can rest soundly. Or we can polish it to make it nicer. Which one is it going to be? You know me by now: Let’s get out the belt sander.

Making it nice

There are many things we need to do to make this actually usable. We have our work cut out for us. Let’s get started:

  1. Show current login status on all pages. In app/views/layouts/articles.html.erb, add <% if current_user %><p>Logged in as <%= link_to current_user.username, :controller => :login_sessions, :action => :show %></p><% end %>, and copy current_user from the articles_controller to app/helpers/login_sessions_helper.rb
  2. Most importantly: Let’s remove the “edit” link when the user can’t edit the current article. This avoid confusion, and means that we don’t have to fix the ugly error message for people trying to edit other people’s articles. Here’s how: In app/views/articles/show.html.erb, put the following around the edit link: <% if current_user_can_edit @article %> ... <% end %>. Define the current_user_can_edit method in app/helpers/login_sessions_helper.rb as follows: article.user == current_user
  3. Give a friendly message when we redirect someone to log in. We do this using the flash. The flash is a special session which is cleared after a the next request. We use it when we’re redirecting, and normally would put something in “@” instance variables, as instance variables don’t survive a redirect. In app/controllers/articles_controller.rb, add the following in verify_logged_in before redirecting: flash[:notice] = "You must login in to do this". In app/views/login_sessions/new.html.erb, add the following to the top of the page: <p style="color: green"><%= flash[:notice] %></p>
  4. Add a message if login failed. In app/controllers/login_sessions_controller.rb, add the following to the else block of create: flash[:error] = "Login failed". In app/views/login_sessions/new.html.erb, add the following to the top of the page: <p style="color: red"><%= flash[:error] %></p>. As an extra exercise: Can you create a generic construct to list all items in the flash (use flash.each_pair) and display them with proper formatting (update public/stylesheet/scaffold.css)?
  5. If a user was diverted to the login page, redirect the user back to the page he wanted to go to after a successful login. In app/controllers/articles_controller.rb, add the following in verify_logged_in before the redirect: session[:redirect_after_login] = request.request_uri. In app/controllers/login_sessions_controller.rb, change the redirect in create to the following: redirect_to(session[:redirect_after_login] || { :action => 'show'})
  6. Allow for http authentication to allow web service clients to access restricted operations without having to simulate a login dialog. If you have Cygwin, or you’re working on a UNIX platform, you can test this by using the command line tool “curl”: curl http://localhost:3000/articles/id/edit will redirect, but curl --user username:password http://localhost:3000/articles/id/edit will work if we make the following change: In app/controllers/articles_controller.rb, add the following to the start of current_user: if !session[:user_id] && user = authenticate_with_http_basic { |u, p| User.authenticate(u,p) }; session[:user_id] = user.id; end
  7. Never store clear text passwords in the database. We can hash the passwords first. In app/models/user.rb, make the following changes:

    Also update the migration by replacing the password column with hashed_password, and rerun the last migration with rake db:migrate:redo. Finally update the test/fixture/users.yml and replace the password lines with the following: hashed_password: <%= User.hash_string "password" %>. Reload the fixtures with rake db:fixtures:load. Everything will behave as before, but the passwords are no longer stored in clear text.
  8. Fix the tests: All the tests in test/functional/articles_controller_test.rb of methods that require login will now fail. The get, put, post and delete methods that the tests use are all designed with this problem in mind. They all take up to three parameters: The action, the arguments, and the current session. So, for example, in test_should_create_article the line post :create, :article => { } should be replaced by post :create, { :article => { } }, { :user_id => users(:user1).id }, and the first line in test_should_update_article should be put :update, { :id => articles(:one).id, :article => { } }, { :user_id => articles(:one).user_id }. You can find more information about testing controllers in A Guide to Testing the Rails, by Steve Kellock and Chris Carter.
  9. Allow for the creation of new users. Whoops! Currently, only the users already in the database are available. That won’t really work, will it? First, let’s add a few details to the user model: ruby script/generate migration AddDetailsToUser email:string full_name:string bio:text and rake db:migrate. Then, create the controller and views for the user scaffold: ruby script/generate scaffold --skip-migration user username:string password:string email:string full_name:string bio:text. You will need to make a few simple changes: Remove password from show.html.erb and index.html.erb, and replace text_field for password in edit.html.erb and new.html.erb with password_field. You will probably also want to have a before_filter :verify_logged_in, :only => [ :edit, :update, :destroy ] in app/controllers/users_controller.rb. Move verify_logged_in and current_user to app/controllers/application.rb to reuse these methods between the different controllers.
  10. There are still many things left to do with the users to make them secure and well-functioning. But that’s going to be the subject of a future post.

Don’t mess with my articles!

We have implemented functionality for logging in, and for restricting access to an article. It turns out that using the relationships between the user object and articles is a good way to ensure that a user is only allowed to edit his own articles. Using ActiveRecord to execute find on an association instead of a class minimizes the risk of leaving security holes open by accident. There’s still a lot to learn about security however. In my next post, I will show you how you can let users grant access for other users to edit their posts. I might also touch on a few odds and ends regarding validation. Stay turned.

Copyright © 2008 Johannes Brodwall. All Rights Reserved.

About Johannes Brodwall

Johannes is Principal Software Engineer in SopraSteria. In his spare time he likes to coach teams and developers on better coding, collaboration, planning and product understanding.
This entry was posted in Ruby-on-Rails. Bookmark the permalink.

Comments are closed.