Skip to content

Instantly share code, notes, and snippets.

@jennli
Last active March 4, 2020 15:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jennli/e7bb6a58062d089d6aa8 to your computer and use it in GitHub Desktop.
Save jennli/e7bb6a58062d089d6aa8 to your computer and use it in GitHub Desktop.

USER AUTHENTICATION

password_digest

  • rails pre/append 'salt' before running through a hash algorithm
Digest::SHA1.digest("abcd1234")
"|\xE05\x9F\x12\x85\x7F*\x90\xC7\xDEF_@\xA9_\x01\xCB]\xA9"
2.2.3 :007 >
  • start by creating a user model
bin/rails g model user first_name last_name email password_digest
rake db:migrate
  • User model
class User < ActiveRecord::Base
  attr_accessor :password
  attr_accessor :password_confirmation
end
  • now we can do something like:
2.2.3 :001 > u = User.new
+----+----------+----------+-------+----------+---------+----------+
| id | first... | last_... | email | passw... | crea... | updat... |
+----+----------+----------+-------+----------+---------+----------+
|    |          |          |       |          |         |          |
+----+----------+----------+-------+----------+---------+----------+
1 row in set
2.2.3 :002 > u.password = "super_secrete"
"super_secret"

BCrypt

  • has_secure_password(options = {})
  • Adds methods to set and authenticate against a BCrypt password.
  • This mechanism requires you to have a password_digest attribute.
  • there are a few validations: password present, length, ..
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
  • Modify the user model
class User < ActiveRecord::Base
  # attr_accessor :password
  # attr_accessor :password_confirmation

  # more info about has_secure_password can be found here:
  # http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html
  has_secure_password
  
  validates :password, length: {minimum: 6}
  validates :first_name, presence: true
  validates :last_name, presence: true
  validates :email, presence: true,
                    uniqueness: true,
                    format: /\A([\w+\-]\.?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
end
  • test out the has_secure_password model
2.2.3 :001 > u = User.new(first_name:"jennifer", last_name:"Li", email:"ldnjennifer@hotmail.com", password:"supersecret", password_confirmation:"supersecret")
+----+-----------+-----------+-----------+------------+-----------+------------+
| id | first_... | last_name | email     | passwor... | create... | updated_at |
+----+-----------+-----------+-----------+------------+-----------+------------+
|    | jennifer  | Li        | ldnjen... | $2a$10$... |           |            |
+----+-----------+-----------+-----------+------------+-----------+------------+
1 row in set
2.2.3 :002 > u.save
   (0.3ms)  BEGIN
  SQL (0.5ms)  INSERT INTO "users" ("first_name", "last_name", "email", "password_digest", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["first_name", "jennifer"], ["last_name", "Li"], ["email", "ldnjennifer@hotmail.com"], ["password_digest", "$2a$10$nsf0CKMrmi0d1D7GKN5kmuZEZRF9fl/8WHPDRiY1fhBdjHxCA2OhO"], ["created_at", "2016-02-11 19:00:00.509358"], ["updated_at", "2016-02-11 19:00:00.509358"]]
   (2.5ms)  COMMIT
true
2.2.3 :003 > u = User.new(first_name:"jennifer", last_name:"Li", email:"ldnjennifer@hotmail.com", password:"supersecret", password_confirmation:"supersecretdd")
+----+-----------+-----------+-----------+------------+-----------+------------+
| id | first_... | last_name | email     | passwor... | create... | updated_at |
+----+-----------+-----------+-----------+------------+-----------+------------+
|    | jennifer  | Li        | ldnjen... | $2a$10$... |           |            |
+----+-----------+-----------+-----------+------------+-----------+------------+
1 row in set
2.2.3 :004 > u.save
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
false

####cookies

  • store session cookies in browser
  def about
    cookies.signed[:abc] = "xyz"
    cookies.signed[:hello] = "good bye"

    # will all be stored in the same session
    session[:foo] = "bar"
    session[:foo1] = "bar1"
    session[:foo2] = "bar2"
  end
  • in the browser, the cookie will have this when about is visited:

screen shot 2016-02-11 at 12 04 47 pm

  • in application_controller
  def user_signed_in?
    session[:user_id].present?
  end

  helper_method :user_signed_in?


  def current_user
    @current_user ||=  User.find(session[:user_id])
  end

  # adding 'helper_method :current_user' makes this method
  # available in all view files as well as a helper method
  helper_method :current_user
  • in application.html.erb
<% if user_signed_in? %>
  Hello <%= current_user.first_name %>
<% else %>
  <%= link_to "Sign Up", new_user_path %>
<% end %>
  • users_controller
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new user_params
    if @user.save
      session[:user_id] = @user.id
      redirect_to root_path, notice: "User created successfully"
    else
      flash[:alert] = "Error creating user"
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation)
  end
end
  • user new view
<h1>Sign Up</h1>
<%= @user.errors.full_messages.join(",") %>

<div>
  <%= form_for @user do |f| %>
  <div>
    <%= f.label :first_name %>
    <%= f.text_field :first_name %>
  </div>

  <div>
    <%= f.label :last_name %>
    <%= f.text_field :last_name %>
  </div>

  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

  <div>
    <%= f.label :password %>
    <%= f.password_field :password, required:true%>
  </div>

  <div>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation, required:true %>
  </div>

  <%= f.submit %>
  <% end %>
</div>

sign in

  • create a new controller called sessions
> rails g controller sessions
  • routes:
    resources :sessions, only: [:new, :create] do
      delete :destroy, on: :collection
    end
  • view
<h1>Sign in</h1>

<%= form_tag sessions_path do %>
<div>

  <%= label_tag :email %>
  <%= email_field_tag :email %>
</div>

<div>
  <%= label_tag :password %>
  <%= password_field_tag :password %>
</div>

<%= submit_tag %>
<% end %>
  • use the authenticate method came with rails
2.2.3 :002 > u = User.find_by_email "lsjf@hot.com"
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = $1 LIMIT 1  [["email", "lsjf@hot.com"]]
+----+------------+-----------+--------------+--------------------------------------------------------------+-------------------------+-------------------------+
| id | first_name | last_name | email        | password_digest                                              | created_at              | updated_at              |
+----+------------+-----------+--------------+--------------------------------------------------------------+-------------------------+-------------------------+
| 4  | Jennifer   | hello     | lsjf@hot.com | $2a$10$eN8M/myrih503r1UA41boe4Aw7rjNgV8FJVJtVBt5GxEKwlGjIFWW | 2016-02-11 19:28:58 UTC | 2016-02-11 19:28:58 UTC |
+----+------------+-----------+--------------+--------------------------------------------------------------+-------------------------+-------------------------+
1 row in set
2.2.3 :003 > u.authenticate("123456")
+----+------------+-----------+--------------+--------------------------------------------------------------+-------------------------+-------------------------+
| id | first_name | last_name | email        | password_digest                                              | created_at              | updated_at              |
+----+------------+-----------+--------------+--------------------------------------------------------------+-------------------------+-------------------------+
| 4  | Jennifer   | hello     | lsjf@hot.com | $2a$10$eN8M/myrih503r1UA41boe4Aw7rjNgV8FJVJtVBt5GxEKwlGjIFWW | 2016-02-11 19:28:58 UTC | 2016-02-11 19:28:58 UTC |
+----+------------+-----------+--------------+--------------------------------------------------------------+-------------------------+-------------------------+
1 row in set
2.2.3 :004 > u.authenticate("12345633")
false
2.2.3 :005 >
  • add a sign_in method in application controller
def sign_in(user)
    session[:user_id] = user.id
end
  • sessions_controller
class SessionsController < ApplicationController
  def new
  end

  def create
    # render json: params
    user = User.find_by_email params[:email]
    if user && user.authenticate(params[:password])
      # the sign_in method is defined in application controller
      sign_in(user)

      redirect_to root_path, notice: "Welcome #{user.first_name}!"
    else
      flash[:alert] = "wrong credentials"
      render :new
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_path, notice: "Sign out!"
  end
end

sign out

  • controller
def destroy
    session[:user_id] = nil
    redirect_to root_path, notice: "Sign out!"
  end
  • application controller
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
  def sign_in(user)
    session[:user_id] = user.id
  end

  def user_signed_in?
    session[:user_id].present?
  end

  helper_method :user_signed_in?


  def current_user
    @current_user ||=  User.find(session[:user_id])
  end

  # adding 'helper_method :current_user' makes this method
  # available in all view files as well as a helper method
  helper_method :current_user
end
  • application.html.erb view
<% if user_signed_in? %>
    Hello, <%= current_user.first_name %>!
    <%= link_to "Log out", sessions_path, method: :delete %>
<% else %>
    <%= link_to "Sign in", new_session_path %>
    <%= link_to "Sign Up", new_user_path %>
<% end %>

authorization

  • start by adding user:references to answer and question
rails g migration add_user_to_answers user:references
  • specify association in the user model
has_many :questions, dependent: :nullify
has_many :answers, dependent: :nullify
  • and answer and question model:
belongs_to :user
  • question/answer controller
  before_action :authenticate_user, except: [:index, :show]
  
    def create
    @question = Question.new question_params
    @question.user = current_user
    if   @question.save
    ...
  • application controller:
def current_user
    @current_user ||=  User.find(session[:user_id])
  end
  
def authenticate_user
    # if session[:user_id] is nil we redirect to the sign in page
    redirect_to new_session_path, notice: "Please sign in." unless user_signed_in?
  end
  • display user name by answer. show.html.erb:
   <ul>
  <% @question.answers.each do |ans| %>
  <li><%= ans.body %> <em>created_by: <%= ans.user_full_name %></em>
  <%= link_to "Delete", question_answer_path(@question, ans),
  method: :delete,
  data: {confirm: "Are you sure?"} %></li>
<% end %>
</ul>
  • in answer controller
# delegate :full_name, to: :user, prefix: true
  def user_full_name
    user.full_name if user
  end
  • in user controller
  def full_name
    "#{first_name} #{last_name}".titleize
  end

only owner has authorization to edit question

  • application controller:
  def current_user
    @current_user ||=  User.find(session[:user_id]) if user_signed_in?
  end

  end
  • question controller
before_action :authorize_user, only: [:edit, :update, :destroy]
  
  ...
 def authorize_user
    if @question.user != current_user
      redirect_to root_path, alert: "access denied!"
    end
  • view
  <% if user_signed_in? && current_user == @question.user %>
  <%= link_to "Edit", edit_question_path(@question) %>

  <%#  method: :delete asks Rails to send a DELETE request instead of GET which
  is accomplished using Javascript/jQuery %>
  <%= link_to "Delete",
  question_path(@question),
  method: :delete,
  data: {confirm: "Are you sure?"} %>
  <% end %>

cancancan

  • centralized location for defining user permission rules
gem 'cancancan'
  • after installing the gem and run bundle, generate an ability.rb file:
bin/rails g cancan:ability
  • the ability.rb is a part of the model
class Ability
  include CanCan::Ability

  def initialize(user)
    # Define abilities for the passed in user here. For example:
    #
      user ||= User.new # guest user (not logged in)
    #   if user.admin?
    #     can :manage, :all
    #   else
    #     can :read, :all
    #   end
    #
  # we defined an ability using the can method that comes from 'cancancan'
  # :manage allows any action on the model (in this case Question)
  # example: :edit, :destroy, :update..etc
  # this doesn't enforce the rule, we just define the rule in here.
  can :manage, Question do |question|
    question.user == user
  end
  ...
end
  • application controller
  def authorize_user
    unless can? :manage
      redirect_to root_path, alert: "access denied!"
    end
  • the view can now be changed to:
  <% if can? :manage %>
  <%= link_to "Edit", edit_question_path(@question) %>

  <%#  method: :delete asks Rails to send a DELETE request instead of GET which
  is accomplished using Javascript/jQuery %>
  <%= link_to "Delete",
  question_path(@question),
  method: :delete,
  data: {confirm: "Are you sure?"} %>
  <% end %>

admin user

  • Add admin user column to users
bin/rails g migration add_admin_to_users admin:boolean
  • the migration file looks like this, add default:false if you want to default admin to false
class AddAdminToUsers < ActiveRecord::Migration
  def change
    add_column :users, :admin, :boolean, default: false
  end
end
  • run rake db:migrate, and add this to ability.rb (if the file has this commented out, uncomment it)
      if user.admin?
        can :manage, :all
      else
        can :read, :all
      end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment