Members Only Walkthrough
16 May 2014Estimated Time: 3 hours
Course: Ruby on Rails » Forms and Authentication » Project: Authentication
##Objective:
In this project, you’ll be building an exclusive clubhouse where your members can write embarrassing posts about non-members. Inside the clubhouse, members can see who the author of a post is but, outside, they can only see the story and wonder who wrote it.
At the end of the project, you should have a simple application that authenticates users to view authors and create posts.
Sign-in page should flash errors if invalid:
Signed-in members can view authors and create new posts:
##Basic Steps:
From the command line, create a new Rails app:
jamies-air:~ jxberc$ rails new members-only
Change directory to members-only, and generate a User
model:
jamies-air:members-only jxberc$ rails generate model User name:string email:string password_digest:string
Migrate user
model using rake db:migrate
command.
Include bcrypt
gem in your GemFile and run bundle install
command:
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
Add #has_secure_password
to app/models/user.rb
file. Can also add additional validations:
class User < ActiveRecord::Base
has_secure_password
validates :password, length: { minimum: 6 }
end
Load rails console
and create a sample user from command line to verify #has_secure_password
method validates password
and password_confirmation
fields:
2.0.0-p451 :001 > User.all
...
2.0.0-p451 :002 > User.create(name:'Thor', email: 'thor@email.com', password: 'foobar', password_confirmation: 'foobar')
(0.1ms) begin transaction
Binary data inserted for `string` type on column `password_digest`
SQL (5.0ms) INSERT INTO "users" ("created_at", "email", "name", "password_digest", "updated_at") VALUES (?, ?, ?, ?, ?) [["created_at", Fri, 16 May 2014 16:39:42 UTC +00:00], ["email", "thor@email.com"], ["name", "Thor"], ["password_digest", "$2a$10$pAXWAKQsk3oTUdF/YrkGGOROZkDW.qzJElfurP2YsXLyLFUQZqZ/O"], ["updated_at", Fri, 16 May 2014 16:39:42 UTC +00:00]]
(0.8ms) commit transaction
=> #<User id: 1, name: "Thor", email: "thor@email.com", password_digest: "$2a$10$pAXWAKQsk3oTUdF/YrkGGOROZkDW.qzJElfurP2YsXLy...", created_at: "2014-05-16 16:39:42", updated_at: "2014-05-16 16:39:42">
2.0.0-p451 :003 >
In the above example, the password
was validated against password_confirmation
and saved as a hashed password in the password_digest
field.
The#has_secure_password
method provides an #authenticate
command, which can be used to authenticate user by passing the password as an argument:
2.0.0-p451 :003 > u = User.first
...
2.0.0-p451 :004 > u.authenticate('barfoo')
=> false
2.0.0-p451 :005 > u.authenticate('foobar')
=> #<User id: 1, name: "Thor", email: "thor@email.com", password_digest: "$2a$10$pAXWAKQsk3oTUdF/YrkGGOROZkDW.qzJElfurP2YsXLy...", created_at: "2014-05-16 16:39:42", updated_at: "2014-05-16 16:39:42">
2.0.0-p451 :006 >
##Step 2: Sessions and Sign In
Create a sessions_controller.rb
and the corresponding routes.
jamies-air:members-only jxberc$ rails generate controller Sessions
In config/routes.rb
:
MembersOnly::Application.routes.draw do
resources :sessions, only: [:new, :create, :destroy]
match '/signin', to: 'sessions#new', via: 'get'
match '/signout', to: 'sessions#destroy', via: 'delete'
...
In app/controllers/sessions_controller.rb
, fill in the #new
action to create a blank session and send it to the view.
class SessionsController < ApplicationController
def new
end
...
In app/views/sessions
create a new.html.erb
file and then create a simple #form_for
form to sign-in user:
<h1>Sign in</h1>
<%= form_for(:session, url: sessions_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit "Sign in", class: "btn btn-large btn-primary" %>
<% end %>
Now, that users can sign-in, the app should remember when a user is signed-in.
Create a new string column for User table called remember_token
. The token will be stored as a cookie and used later to authenticate users.
jamies-air:members-only jxberc$ rails generate migration add_remember_token_to_users
In the migration file, add the following:
class AddRememberTokenToUsers < ActiveRecord::Migration
def change
add_column :users, :remember_token, :string
add_index :users, :remember_token
end
end
Migrate using rake db:migrate
.
When you create a new user, a new token should be created. Use a #before_create
callback on the User
model to create a new token. Use several helper functions to create random token and then encrypt it. For an explanation of these helper methods, check out Hartl’s Rails tutorial Ch. 8.
class User < ActiveRecord::Base
before_create :create_remember_token
...
def User.new_remember_token
SecureRandom.urlsafe_base64
end
def User.digest(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = User.digest(User.new_remember_token)
end
end
In app/controllers/sessions_controller.rb
, fill in #create
action to create the user’s session.
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
flash[:success] = 'Thank you for signing in!'
sign_in user
redirect_to root_path
else
flash.now[:error] = 'Invalid email/password combination'
render 'new'
end
end
The above #create
action searches for user using submitted e-mail, and then checks if user exists and authenticates using submitted password. If these check out, then it will sign-in user.
The #create
action uses a sign_in
helper method which you can create in the app/helpers/sessions_helper.rb
.
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.digest(remember_token))
self.current_user = user
end
...
The sign_in
method creates a new token and sets cookie equal to new token. Then, the user’s token is updated with hashed token.
(Note: in order to access helper method from controllers, use include
statement in app/controllers/application_controller.rb
)
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
include SessionsHelper
...
Notice the self.current_user = user
line above. This statement uses one of the additional helper methods we will create. Again, for a deeper dive into what these methods do, check Hartl’s Rails Tutorial Ch. 8.
def current_user=(user)
@current_user = user
end
def current_user
remember_token = User.digest(cookies[:remember_token])
@current_user ||= User.find_by(remember_token: remember_token)
end
def signed_in?
!current_user.nil?
end
In app/controllers/sessions_controller.rb
, update destroy
action:
class SessionsController < ApplicationController
...
def destroy
sign_out
redirect_to root_path
end
Most of the functionality is built in the sign_out
helper method. After signing out, redirect to your root directory (homepage).
In app/helpers/sessions_helpers.rb', create
sign_out` method:
def sign_out
current_user.update_attribute(:remember_token,
User.digest(User.new_remember_token))
cookies.delete(:remember_token)
self.current_user = nil
end
##Step 4: Authentication and Posts
Create a Post
model and controller:
jamies-air:members-only jxberc$ rails generate model Post title:string body:text
...
jamies-air:members-only jxberc$ rails generate controller Posts
After you create model, run rake db:migrate
from the command line.
Update routes in config/routes.rb
:
resources :posts, only: [:new, :create, :index]
root 'posts#index'
(Note: Added a root path that directs to list of posts. As an example, the #sign_out
helper method redirects to root path.)
In app/controllers/posts_controller.rb
, add before_action
method to restrict access to #new
and #create
actions to only signed-in users:
class PostsController < ApplicationController
before_action :signed_in_user, only: [:new, :create]
...
# before filter/action
def signed_in_user
unless signed_in?
redirect_to signin_url
end
end
end
Create #new
action in PostsController:
class PostsController < ApplicationController
...
def new
@post = Post.new
end
...
end
Create a simple form and save as app/views/posts/new.html.erb
:
<h1>New Post</h1>
<%= form_for @post do |f| %>
<p>
<%= f.label :title %> <br/>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
Now, create #create
action in PostsController which automatically updates Posts foreign key (user_id
) with the signed-in user. (Note: Need to create a foreign key in the Post model first).
From command line:
jamies-air:members-only jxberc$ rails generate migration AddForeignKeyToPost user:references
Run rake db:migrate
and add associations to respective models:
class Post < ActiveRecord::Base
belongs_to :user
...
class User < ActiveRecord::Base
has_many :posts
Back to the PostsController, fill in the #create
action:
class PostsController < ApplicationController
...
def create
@post = Post.new(post_params)
@post.user_id = current_user.id
@post.save
redirect_to root_path
end
The #create
action creates a new post with help from #post_params
and also updates post’s foreign key (user_id
) with the signed user’s id
. Note the usage of #current_user
helper function.
Create the #index
action to view all posts:
class PostsController < ApplicationController
...
def index
@posts = Post.all
end
...
And finally create the corresponding view app/views/posts/index.html.erb
:
<div class="float-right">
<% if signed_in? %>
<%= link_to "(#{current_user.name}) Sign out", signout_path, method: "delete" %>
<% else %>
<%= link_to 'Sign in', signin_path %>
<% end %>
</div>
<h1>Members Only Posts</h1>
<% @posts.each do |post| %>
<% if signed_in? %>
<p class="float-right">
Posted by:
<%= post.user.name %>
</p>
<% end %>
<h4 class="float-left"><%=post.title %></h4>
<p class="clear"><%= post.body %></p>
<% end %>
<% if signed_in? %>
<%= link_to "Create a Post", new_post_path %>
<% end %>
Using the #signed_in?
helper method only allows signed-in users to view authors of each post.
This project did not require registering new users, so from the command line, create several test users, sign-in, and verify that only signed-in users can view authors and create posts.
Lastly, add some custom CSS styling as practice.
Congratulations, you should have a working authentication system similar to the more comprehensive system built in Michael Hartl’s Rails Tutorial.