📖 Ruby on Rails Toàn tập - Authentication và Authorization
70 phút

Authentication và Authorization trong Rails

Authentication

Setup với bcrypt

# Gemfile
gem 'bcrypt', '~> 3.1.7'
bundle install
rails g model User name:string email:string password_digest:string
rails db:migrate

User Model

class User < ApplicationRecord
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true, uniqueness: { case_sensitive: false }
  validates :password, length: { minimum: 8 }, if: -> { password.present? }

  before_save { self.email = email.downcase }

  def self.from_omniauth(auth)
    # OAuth integration
  end
end

Sessions Controller

class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:email].downcase)

    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_path, notice: "Logged in successfully"
    else
      flash.now[:alert] = "Invalid email or password"
      render :new, status: :unprocessable_entity
    end
  end

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

Current User Helper

class ApplicationController < ActionController::Base
  helper_method :current_user, :user_signed_in?

  private

  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end

  def user_signed_in?
    current_user.present?
  end

  def require_login
    unless user_signed_in?
      redirect_to login_path, alert: "You must be logged in"
    end
  end

  def require_admin
    unless current_user&.admin?
      redirect_to root_path, alert: "Access denied"
    end
  end
end

Routes

Rails.application.routes.draw do
  get 'login', to: 'sessions#new'
  post 'login', to: 'sessions#create'
  delete 'logout', to: 'sessions#destroy'

  resources :users, only: [:new, :create]
  resources :posts
end

Devise (Alternative)

# Gemfile
gem 'devise'
bundle install
rails g devise:install
rails g devise User
rails db:migrate

Devise Views

rails g devise:views

Devise Configuration

# config/initializers/devise.rb
config.password_length = 8..128
config.timeout_in = 30.minutes
config.confirm_within = 3.days

Protected routes

class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def index
    @posts = current_user.posts
  end
end

Authorization với Pundit

# Gemfile
gem 'pundit'
bundle install
rails g pundit:install

Application Policy

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?; false; end
  def show?; false; end
  def create?; false; end
  def new?; create?; end
  def update?; false; end
  def edit?; update?; end
  def destroy?; false; end

  private

  def admin?
    user&.admin?
  end

  def owner?
    user && record.respond_to?(:user_id) && record.user_id == user.id
  end

  class Scope
    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      raise NotImplementedError
    end

    private
    attr_reader :user, :scope
  end
end

Post Policy

class PostPolicy < ApplicationPolicy
  def index?; true; end
  def show?; record.published? || owner? || admin?; end
  def create?; user.present?; end
  def update?; owner? || admin?; end
  def destroy?; owner? || admin?; end

  class Scope < ApplicationPolicy::Scope
    def resolve
      if user&.admin?
        scope.all
      elsif user
        scope.where(published: true).or(scope.where(user_id: user.id))
      else
        scope.where(published: true)
      end
    end
  end
end

Sử dụng trong Controller

class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]

  def index
    @posts = policy_scope(Post)
  end

  def show
    @post = Post.find(params[:id])
    authorize @post
  end

  def edit
    @post = Post.find(params[:id])
    authorize @post
  end

  def create
    @post = current_user.posts.build(post_params)
    authorize @post

    if @post.save
      redirect_to @post
    else
      render :new
    end
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :published)
  end
end

Roles

Simple enum-based roles

# Migration
add_column :users, :role, :string, default: 'user'
add_index :users, :role

# Model
class User < ApplicationRecord
  ROLES = %w[user moderator admin].freeze

  validates :role, inclusion: { in: ROLES }

  ROLES.each do |r|
    define_method "#{r}?" do
      role == r
    end
  end
end

Sử dụng trong views

<% if current_user&.admin? %>
  <%= link_to "Admin", admin_path %>
<% end %>

<% if policy(@post).edit? %>
  <%= link_to "Edit", edit_post_path(@post) %>
<% end %>

JWT cho API

# Gemfile
gem 'jwt'
# app/services/json_web_token.rb
class JsonWebToken
  SECRET_KEY = Rails.application.secret_key_base

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY).first
    HashWithIndifferentAccess.new(decoded)
  rescue JWT::ExpiredSignature, JWT::DecodeError
    nil
  end
end

# app/controllers/api/v1/auth_controller.rb
module Api
  module V1
    class AuthController < ApplicationController
      skip_before_action :verify_authenticity_token

      def login
        user = User.find_by(email: params[:email]&.downcase)

        if user&.authenticate(params[:password])
          token = JsonWebToken.encode(user_id: user.id)
          render json: { token: token, user: user.as_json(except: :password_digest) }
        else
          render json: { error: "Invalid credentials" }, status: :unauthorized
        end
      end

      def register
        user = User.new(user_params)

        if user.save
          token = JsonWebToken.encode(user_id: user.id)
          render json: { token: token, user: user.as_json(except: :password_digest) },
                 status: :created
        else
          render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
        end
      end

      private

      def user_params
        params.require(:user).permit(:name, :email, :password)
      end
    end
  end
end

# app/controllers/api/v1/base_controller.rb
module Api
  module V1
    class BaseController < ApplicationController
      before_action :authenticate_request
      attr_reader :current_user

      private

      def authenticate_request
        header = request.headers['Authorization']
        token = header.split(' ').last if header

        decoded = JsonWebToken.decode(token)
        @current_user = User.find(decoded[:user_id]) if decoded

        unless @current_user
          render json: { error: 'Unauthorized' }, status: :unauthorized
        end
      end
    end
  end
end

Bài tập thực hành

Hãy implement authentication + authorization!

📝 Bài tập (1)

  1. Build auth với roles và policies