Authentication with Warden, devise-less

Warden là gì?

Đối với hầu hết chúng ta thì việc sử dụng warden cũng có nghĩa là sử dụng devise. Devise thực sự rất tốt, nhưng nó quá lớn và có một những quy tắc bắt buộc chúng ta cần tuân theo. Vậy warden là gì?

Warden là một Rack-based middleware, nó được thiết kế để cung cấp một cơ chế cho việc authentication trong các ứng dụng web Ruby. Nó là một cơ chế chung phù hợp với Rack Machinery để cung cấp một tùy chọn mạnh mẽ cho việc authentication.

Warden được thiết kế theo kiểu lười biếng. Nó có nghĩa là nếu bạn không sử dụng nó, nó sẽ không làm bất cứ điều gì, nhưng nếu bạn sử dụng nó sẽ tạo ra những hành động và cung cấp một cơ chế cho phép authentication trong bất kì một ứng dụng Rack-based nào.

Tại sao dùng Warden?

Với việc đẩy mạnh theo hướng sử dụng các Rack applications rất nhiều cơ hội được mở ra. Lời hứa về việc có nhiều ứng dụng cùng chạy trong một process, sub-applications và sub-sub-applications có thể được thực hiện.

Hiện nay việc tạo nhiều ứng dụng trong cùng một process đã và đang thu hút rất nhiều người. Vậy một câu hỏi được đặt ra là làm thế nào để quản lý authentication trong trường hợp này? Mỗi ứng dụng có thể yêu cầu một authentication hoặc một user. Nhìn chung, authentication có thể là cái gì đó giống như "user" cho tất cả các application trong cùng mảng lưới của Rack cho phép đăng nhập vào hệ thống.

Warden cho phép tất cả các middlewares và các endpoints chia sẽ chung một cơ chế authentication, đồng thời vẫn cho phép các ứng dụng tổng thể quản lý nó. Mỗi application có thể truy cập authentication user, hoặc là request authentication trong cùng một cách, sử dụng cùng logic trong cùng mạng lưới Rack.

Sử dụng như thế nào?

Warden là tuyệt vời, nhưng mà nó không hề đơn giản để bắt đầu như đối với devise. Mặt khác, toàn bộ quá trình authentication đều do bạn thực hiện, nó có nghĩa là bạn cần kiểm soát nhiều hơn và linh hoạt hơn khi nhu cầu sử dụng của bạn thay đổi.

Warden sử dụng Strategies để authenticate sessions. Tuy nhiên nó lại không cung cấp bất kì strategy nào bạn cần phải tự mình thực thi nó. Nếu sử dụng devise thì bạn đã biết, nó cung cấp một số strategy sau.

Devise đã thêm một số tính năng cũng như công cụ hữu ích, trước khi bạn đi sâu vào warden bạn cũng nên biết thêm một số tính năng sẽ không có khi bạn sử dụng warden như:

  • Helpers giống như current_user, user_signed_in?, etc.
  • Các điều hướng mặc định khi authentication fail hoặc là những request từ khách
  • Tất cả các controllers. Đừng lo lắng, nó nghe có vẻ tệ hơn so với thực tế
  • Những tùy chọn configuration mặc định

Bây giờ chúng ta sẽ cùng nhau implement warden. Ví dụ dưới sẽ thực thi authenticate với user trên web app lẫn API call. Tuy nhiên phần giới thiệu sẽ chỉ thực thi trong Web app, phần call API demo các bạn xem thêm trong phần demo (API chỉ implement cho việc authenticate chưa có phần đăng kí mới người dùng).

Đầu tiên, chúng ta cần phải add warden vào trong Gemfile

gem "warden"

Chúng ta sẽ sử dụng model user để thực hiện việc authenticate và model đó sẽ thực hiện những tính năng

  • Mã hóa password
  • Confirm sau khi người dùng đăng kí thành công => cần một file migration với thuộc tính sau:
rails g model User email:string encrypted_password:string  confirmation_token:string confirmation_sent_at:datetime authentication_token:string

Chúng ta sẽ có 3 controllers cần thực thi, một là sessions để đăng nhập, registration để tạo mới người dùng và confirmation để confirm user. Chúng ta định nghĩ routes theo một cách đơn giản sau (nó phụ thuộc vào cách mà bạn muốn đặt)

resources :users do
    collection do
      resource :registrations, only: [:show, :new, :create]
      resource :sessions, only: [:new, :create, :destroy]
      resource :confirmations, only: [:show]
    end
  end

Và model User như sau:

require 'bcrypt'
class User < ActiveRecord::Base
  before_create :generate_confirmation_token

  validates_presence_of :email
  include BCrypt

  def password
    @password ||= Password.new self.encrypted_password
  end

  def password=(new_password)
    @password = Password.create(new_password)
    self.encrypted_password = @password
  end

  def confirm!
    self.confirmation_token = nil
    self.confirmed_at = Time.now.utc
    self.save!
  end

  private
  def generate_confirmation_token
    token = Digest::SHA1.hexdigest("#{Time.now}-#{self.id}-#{self.updated_at}")
    self.confirmation_token = token
    self.confirmation_sent_at = Time.now.utc
  end
end

Sau đó cần add Warden vào Rack Middleware. Rack là một kĩ thuật cho phép chúng ta kết nối nhiều ứng dụng trong cùng một server, có thể bạn không biết nhưng rails có sử dụng một số rack middleware, mỗi phần có nhiệm vụ, xử lý khác nhau. Để xem danh sách các rack middleware trong project bạn sử dụng ta gõ lệnh

rake middleware

use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001f0dab0>
use Rack::Runtime
use Rack::MethodOverride
......

Một điều quan trọng là thứ tự xuất hiện của các rack middleware rất quan trọng nó ảnh hưởng đến việc các tính năng nào sẽ được sử dụng trong các rack tiếp theo (có thể tìm hiểu thêm về rack middleware tại đây). Vậy chúng ta nên đặt warden ở đâu? Nhìn vào wiki chúng ta có thể thấy rằng Warden must be downstream of some kind of session middleware.Tuy nhiên bạn không nên add chúng sau session vì nếu chỉ add sau session vì flash sẽ không được dùng ở một số nơi và bạn sẽ không dùng được chúng. Vì vậy nên add sau ActionDispatch::Flash.

config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
 end

Bây giờ cùng xem lại danh sách các middleware, bạn sẽ thấy use Warden::Manager ngay sau use ActionDispatch::Flash.

Sau đó chúng ta sẽ định nghĩ một Strategy để authenticate user thông qua password(với web) và thông qua authentication_token(cho API) hoặc một phương thức authentication nào mà bạn định nghĩa. Bạn cũng có thể xem thêm về Strategies tại wiki, nhưng khái quát thì nó sẽ thực thi các function valid!, và authenticate!. Bạn cũng có thể định nghĩa nhiều strategies khác nhau, mỗi strategy sẽ thực thi một authenticate cho mỗi app mà bạn định nghĩa, hoặc là hoàn toàn có thể dùng chung trong một strategy.

#lib/strategies/password_strategy.rb
class PasswordStrategy < ::Warden::Strategies::Base
  def valid?
    email || password || authentication_token
  end

  def authenticate!
    if authentication_token
      user = User.find_by_authentication_token authentication_token
      user.nil? ? fail!("Could not log in") : success!(user)
    else
      user = User.find_by_email email
      if user.nil? || user.confirmed_at.nil? || user.password != password
        fail!("Could not log in")
      else
        success! user
      end
    end
  end

  private
  def email
    params["session"].try :[], "email"
  end

  def password
    params["session"].try :[], "password"
  end

  def authentication_token
    params["authentication_token"]
  end
end

File này mình gộp authenticate cho web và API dùng chung một strategy. Giải thích qua một số tính năng của những function trên:

  • Đầu tiên sẽ chạy qua hàm valid! nếu pass nó sẽ tiếp tục thực thi phần authenticate! Không sẽ tiếp tục check các strategy tiếp theo
  • Trong hàm authenticate!, hàm success! chính là thực thi chức năng login
  • fail! sẽ dừng lại tiến trình không check tiếp bất cứ strategy nào nữa
  • Còn một public function nữa là fail hàm này khác fail! là sẽ không dừng lại tiến trình, mà tiếp tục check các strategy tiếp theo.

Đã implement xong strategy, bây giờ cần phải add chúng vào warden.

# config/application.rb
# Add Warden in the middleware stack
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  manager.default_strategies :password
	# Nếu có thêm strategy thì bạn chỉ việc thêm tên strategy vào sau dòng trên, cách nhau bởi dấu ','
	# ví dụ
	#  manager.default_strategies :password, :basic_auth
end
#config/initializers/warden.rb
require Rails.root.join('lib/strategies/password_strategy')

Warden::Strategies.add(:password_strategy, PasswordStrategy)

Giờ chúng ta sẽ implement một số helper tương tự devise như là current_user, signed_in? và include vào ApplicationController

#app/controllers/concerns/warden_helper.rb
module WardenHelper
  extend ActiveSupport::Concern

  included do
    helper_method :warden, :signed_in?, :current_user

    prepend_before_action :authenticate!
  end

  def signed_in?
    !current_user.nil?
  end

  def current_user
    warden.user
  end

  def warden
    env['warden']
  end

  def authenticate!
    warden.authenticate!
  end
end

Về cơ bản thì quá trình authenticate cho một API là xong, còn đối với Web chúng ta cần implement thêm 2 controller session cho việc login và registration để đăng kí.

#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_filter :authenticate!
  def create
    authenticate!
    redirect_to :root
  end

  def destroy
    warden.logout
    redirect_to :root
  end
end

#app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
  skip_before_action :authenticate!

  def new
    @user = User.new
  end

  def create
    @user = User.new user_params
    if @user.save
      flash[:notice] = "Registrations User Success"
      redirect_to root_path
    end
  end

  private
  def user_params
    params[:user].permit!
  end
end

Về cơ bản thì việc thực thi đăng kí đăng nhập cho Web đã xong. Chắc hẳn bạn sẽ có một thắc mắc khi nhìn vào hai controllers trên là không biết khi mà đăng nhập, hoặc đăng kí failed nó sẽ xử lý như thế nào? Có thể thấy được devise đã làm điều này rất tốt, vì nó đã xây dựng một failure_app cho phép xử lý tất cả các tình huống đăng nhập, đăng kí lỗi thì điều hướng người dùng tới đâu, trả về message gì ..... Thì ở đây chúng ta cũng xây dựng một Failure app tương tự như vậy tuy nhiên sẽ đơn giản hơn rất nhiều. Thứ nhất chúng ta cần nói cho Warden biết chúng ta có một failure_app

# config/application.rb
config.middleware.insert_after ActionDispatch::Flash, Warden::Manager do |manager|
  manager.default_strategies :password
  manager.failure_app = UnauthorizedController
end

Tạo một class UnauthorizedController điều hướng những hoạt động khi mà đăng kí, đăng nhập failed thì sẽ như thế nào. Ví dụ, khi chưa đăng nhập, thì chuyển về trang đăng nhập trước khi người dùng sử dụng ứng dụng.

#app/controllers/unauthorized_controller.rb

class UnauthorizedController < ActionController::Metal
  include ActionController::UrlFor
  include ActionController::Redirecting
  include Rails.application.routes.url_helpers
  include Rails.application.routes.mounted_helpers

  delegate :flash, to: :request

  class << self
    def call env
      @respond ||= action :respond
      @respond.call env
    end
  end

  def respond
    unless request.get?
      message = env['warden.options'].fetch(:message, "unauthorized.user")
      flash.alert = I18n.t(message)
    end
    redirect_to new_sessions_url
  end
end

Trên đây đã hoàn tất quá trình đăng kí, đăng nhập với warden, nó nhẹ nhàng và đơn giản hơn rất nhiều.

Demo

Một demo nhỏ về việc sử dụng warden cho việc authentication với web app và api, một cách authentication cho cả web và api. Các bạn có thể xem nó tại đây

References

http://pothibo.com/2013/07/authentication-with-warden-devise-less/ https://github.com/hassox/warden/wiki/Overview