Building authentication from scratch in Ruby on Rails
To allow creation of new users (registration)
1.) Set the routes in routes.rb
# routes.rb
get '/register', to: 'users#new' # manually mapping user registration to /register instead of /users/new -> therefore not listed under resources
resources :users, only: [:show, :create, :edit, :update] #note we don't want an index route to show all users
2) Create users_controller.rb
# users_controller.rb
...
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
session[:user_id] = @user.id # to automatically login a user that signed up
flash[:notice] = "You are registered."
redirect_to root_path
else
render :new
end
end
def user_params
params.require(:user).permit(:username, :password)
end
3.) Create the views
- Create
users
folder -
Create
users/new.html.erb
+edit.html.erb
+_form.html.erb
- Create the model backed form in
_form.html.erb
<div class="well">
<%= form_for @user do |f| %>
<div class="control-group">
<%= f.label :username %>
<%= f.text_field :username %>
</div>
<div class="control-group">
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<%= f.submit("Register", class: 'btn btn-success' %>
<% end %>
</div>
Update _navigation.html.erb
to show register link, when logged out
<!-- _navigation.html.erb -->
...
<div class="nav_item">
<%= link_to "Register", register_path, class: "btn btn-success btn-small" %>
</div>
4.) Add validations in user.rb
# user.rb
...
validates :username, presence: true, uniqueness: true
validates :password, presence: true, on: :create, length: {minimum: 5} # on: :create means that a password has to be present when creating a new user, but not when updating user details
To build user login/logout
1.) Install the bcrypt gem
gem 'bcrypt', '~> 3.1.7'
Example using Active Record (which automatically includes ActiveModel::SecurePassword):
# Schema: User(name:string, password_digest:string)
class User < ActiveRecord::Base
has_secure_password
end
user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
user.save # => false, password required
user.password = 'mUc3m00RsqyRe'
user.save # => false, confirmation doesn't match
user.password_confirmation = 'mUc3m00RsqyRe'
user.save # => true
user.authenticate('notright') # => false
user.authenticate('mUc3m00RsqyRe') # => user
User.find_by(name: 'david').try(:authenticate, 'notright') # => false
User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
-> http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html
Note that bcrypt
together with has_secure_password
give us a virtual attribute, called “password” and also the “authenticate” method, so in rails console:
$ bob.password = "password"
$ bob.save
$ bob.authenticate('iforgot')
=> "false"
$ bob.authenticate('password')
=> "true"
2.) In models/user.rb
add has_secure_password validations: false
3.) Create the file controllers/sessions_controller.rb
with “new, update and destroy methods”
# sessions_controller.rb
...
def create
user = User.where(username: params[:username]).first
if user && user.authenticate(params[:password])
session[:user_id] = user.id # important to only store userid in cookie(session) and not other information of the user to not have too big a cookie and risk "cookie overflow".
flash[:notice] = "Welcome, you've logged in."
redirect_to root_path
else
flash[:error] = "There is something wrong with your username or password."
redirect_to login_path
end
end
def destroy
session[:user_id] = nil # all that's necessary to end the session (set session user_id to nil)
flash[:notice] = "You've logged out."
redirect_to root_path
end
4.) In routes.rb
add
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create'
get '/logout', to: 'sessions#destroy'
5.) Add a non-model backed form in a new folder and file ‘views/sessions/new.html.erb’
<div class='well'>
<%= form_tag '/login' do %>
<div class='control-group'>
<%= label_tag :username %>
<%= text_field_tag :username %>
</div>
<div class='control-group'%>
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<%= submit_tag 'Login', class: 'btn btn-success' %>
<% end %>
</div>
6.) In application_controller.rb
add
helper_method :current_user, :logged_in? # this makes the helper methods below also available to all our controller actions and all our view templates
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id] # means that this code will return nil if the user has no session and the part after ||= is only executed if @current_user is nil
# ||= is for "memoization" to not hit the database too many times.
end
def logged_in?
!!current_user # !! turns current_user into boolean value
end
7.) Wrap items in views that should only be available to logged in users into if statements (if logged in)
Do this for all items you wan’t to display based on status
<!-- _navigation.html.erb -->
<% if logged_in? %>
<div class='nav_item'>
<%= link_to 'New Post', new_post_path, class: 'btn btn-success btn-small' %>
</div>
<div class='nav_item'>
<%= link_to 'Log out', logout_path, class: 'btn btn-small' %>
</div>
<% else # logged out %>
<div class='nav_item'>
<%= link_to 'Log in', login_path, class: 'btn btn-small' %>
</div>
<% end %>
8.) Shut down routes based on logged in / logged out status
Do this for all actions you want to disable based on status
In posts_controller.rb
add
# posts_controller.rb
before_action :require_user, except: [:show, :index] # means that if you are not logged in you can only use the show or index action
In application_controller.rb
add
# application_controller.rb
def require_user
if !logged_in?
flash[:error] = "Must be logged in to do that."
redirect_to root_path
end
end