Padawan coder

Getting from zero to hero...

Rails 3 Authentication for N00bs

| Comments

Progressing my ToDoList app - making it work, making it right

Source: On picking a perfect password by Euroscientist

Since I have a sieve-like memory, I really need to take detailed notes and practice practice in order to remember how to do things. Hence, I am trying to apply what we’ve learned through lectures in my ToDoList ‘practice’ app, especially on areas that I haven’t had a chance to personally code in our current group ‘Hand Raise’ project.

At the end of my previous blog post on my ToDoList app, my next steps were to:
1) Change the navigation links so they are conditional
2) Add authorization for Users to create, edit and delete tasks

But before authorizing users, we first need to authenticate users, and this will be the main body of my post (making it work). The remainder of my post (making it right) is about refactoring, conditionally displaying links and some validation pitfalls that tripped me up.

Making it work - Rails 3 Authentication in 4 parts:
1) Leveraging Rails 3 functionality
Rails 3 includes a has_secure_password class method which simplifies authentication for the beginner and/or the gem-shy.
i) Generate a User model which includes a password_digest string field, which is required to leverage the has_secure_password functionality
ii)Add has_secure_password in the User model
iii) Include (or uncomment) the bcrypt-ruby gem in the Gemfile

2) Creating logins and sessions
The act of logging in is the start of a new user session, which is a virtual model, which means (per my interpretation), that there are specific rules that govern each session, but that there is no need for an actual model, because nothing is persisted into the database.
i) Generate a sessions controller: rails g controller sessions
ii) Create a login form in view. Note that you should use the form_tag helper instead of the form_for helper because there is no sessions model
iii)In the Sessions controller, write a create method, which says: if a user exists and the password can be authenticated (note that .authenticate is a method given by has_secure_password), then set the user’s id as the user_id of the session.

SessionsController
1
2
3
4
5
6
7
8
9
def create
  user = User.find_by_email(params[:email])
  if user && user.authenticate(params[:password])
    session[:user_id] = user.id
    redirect_to root_url, notice:"Logged in!"
  else
    flash.now.alert = "Email or password is invalid"
  end
end

iv) Logging out would simply entail setting the session’s user_id to nil: session[:user_id] = nil
v) Logging out in views is a bit tricky:

1
<%= link_to "Log out", session_path(current user), method: "delete" %>

–> even though the session destroy method does not accept any arguments, the session_path expects an argument, so just put any argument in
–> need to include method: “delete” (note that method: :destroy doesn’t work)

3) Allowing the application remember that a user logged in
Create an instance variable to allow the app to remember who the current user is.
i) In Application Controller (so it is set app-wide), create a private method to store the session’s user_id as the current_user, if session user_id exists.

ApplicationController
1
2
3
4
def current_user
  @current_user ||= User.find_by_id(session[:user_id]) if session[:user_id]
end
helper_method :current_user

–> add a helper_method to allow the method to be accessible to all the Views

4) Allowing the application to test if a user is logged in
i) In ApplicationController, create a private method logged_in? or authorize (or anything that is sensibly-named) that tests if a user is logged in.
Either:

1
true if current_user

Or:

1
redirect_to new_session_path if current_user.nil?

ii) In each relevant controller (in this case, Lists, Tasks, Users), create a before_filter which runs on specific actions with options, e.g.

1
before_filter logged_in?, only: [:edit, :destroy]

or:

1
before_filter logged_in?, except: [:show, :index]

Making it right - refactoring authentication, conditionally displaying links, validation pitfalls
1) Refactoring authentication
i) Prettifying routes
To change routes from users/new, sessions/new etc. to signup and login, we need to change the routes and then update the views.
Adding the following routes to the routes file…

1
2
3
4
get 'signup'=> 'users#new'
get 'login' => 'sessions#new'
post 'login' => 'sessions#create'
get 'logout'=> 'sessions#destroy'

…enables us to refactor our code in Views from…

1
2
3
<%= link_to "Sign up", new_user_path %>
<%= link_to "Log in", new_session_path %>
<%= link_to "Log out", session_path(current_user), method: "delete" %>

to…

1
2
3
<%= link_to "Sign up", signup_path %>
<%= link_to "Log in", login_path %>
<%= link_to "Log out", logout_path %>

ii) Further abstracting methods
We can encapsulate the logic of setting session user_ids in private methods in the ApplicationController:

ApplicationController
1
2
3
4
5
6
7
def login(user)
  session[:user_id] = user.id
end

def logout
  session[:user_id] = nil
end

We can then refactor the SessionsController to use these methods.

2) Conditionally displaying links
I wanted to hide the navigation links for a page if a user is already on that page. The Rails UrlHelper from the ActionView::Helpers were very useful in this regard.

Methods such as link_to_if, link_to_unless and link_to_unless_current provide a lot of options for toggling links on and off.

But to completely hide a link, when a user is on a specific page, I used the current_page? method as follows:

ApplicationController
1
2
3
<% unless current_page?(root_path) %>
  <li><%= link_to "Home", root_path %></li>
<% end %>

3) Validation pitfalls
In this set of iterations, I have also incorporated additional front-end and back-end validations that we learned a couple of weeks ago.

One problem I encountered was with password validation, where I initially applied

user.rb
1
validates :password, :length => {:minimum => 6 }

This broke my ability to assign a user to a task when creating and adding a task to a list. And that took my AGES to debug. Finally, I realized that the user wasn’t saving because the password validation was failing. I got around it by only validating the password on creation:

user.rb
1
validates :password, :length => {:minimum => 6 }, :on => :create

Another issue I grappled with was the case-sensitivity of emails. I came across :case_sensitive => false, but some (admittedly dated) blogs have suggested that it slows down applications, so I have downcased the emails when creating a user, and also downcased the email input when creating a session.

UsersController
1
2
3
4
5
def create
  @user = User.new(params[:user])
  @user.email = @user.email.downcase
  @user.save
end
SessionsController
1
2
3
4
def create
  user = User.find_by_email(params[:email].downcase)
  # other code omitted
end

Resources:
1) ActiveModel at http://api.rubyonrails.org/
2) Authentication from scratch prior to Rails 3: RailsCasts / ASCIIcasts
3) ActionView at http://api.rubyonrails.org/

Comments