Authentincation với JWT trong Rails 5

Tiếp nối bài trước mình đã nói về tạo api trong rails 5 thì trong bài này mình sẽ tiếp tục chia sẽ thên về authentication bằng JWT và 1 số tính năng khác về api trong rails 5 như API versioning, pagination, và serialization

Giới thiệu về Authentication

Như chúng ta đã biết thì API sẽ cũng cấp cho người dùng có thể có thao tác những hoạt động riêng của họ, quản lý các hoạt động đó của họ.

Đầu tiên chúng ta sẽ tạo ra User, vì tất nhiên phải có người dùng thì chúng ta mới cần tới việc authentication.

rails g model User name:string email:string password_digest:string
rails db:migrate

Nói 1 chút về Molde User này, tại sao chúng ta lại dùng password_disget thay vì dùng password vì chúng ta sẽ sử dụng 1 method là has_secure_password để thực hiện xác thực một bcrypt password, điều đó đòi hỏi chúng ta phải sử dụng password_disget attribute.

Users sẽ thực hiện quản lý nhiều Todo lists của họ, chính vì thế chúng ta sẽ có một mối quan hệ 1 - many ở đây giữa modle User và model Todo (như ở phần trước mình đã trình bày).

class User < ApplicationRecord
  has_secure_password

  has_many :todos, foreign_key: :created_by

  validates_presence_of :name, :email, :password_digest
end

để thực hiên bcrypt password chúng ta cần sử dụng gem bcrypt.

sau khi thưc hiện :

bundle install

để sử dụng gem. Bây giờ chúng ta sẽ tiến hành đi vào authentication, trước tiên cần định nghĩa các class service chúng ta cần sử dụng.

  • JsonWebToken : sử dụng để encode và decode bằng việc sử dung jwt
  • AuthorizeApiRequest: sử dụng để xác thực mỗi request API.
  • AuthenticateUser : sử dụng để xác thực User.
  • AuthenticationController: sử dụng để thực hiện xác thực lúc User login vào hệ thống.

Sử dụng JWT (JSON WEB TOKEN)

Chúng ta sẽ sử dụng gem jwt. Sau khi thực hiện cài và bundle gem jwt, chúng ta sẽ tạo thư mục lib bên trong app, vì sao lại là app/lib, bởi vì tất cả code trong app đều được auto-loaded trong môi trường development và eager-loaded trong môi trường production.

# app/lib/json_web_token.rb
class JsonWebToken
  HMAC_SECRET = Rails.application.secrets.secret_key_base

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

  def self.decode(token)
    body = JWT.decode(token, HMAC_SECRET)[0]
    HashWithIndifferentAccess.new body
  rescue JWT::ExpiredSignature, JWT::VerificationError => e
    raise ExceptionHandler::ExpiredSignature, e.message
  end
end

Trong class trên chúng ta thấy JWT sử dụng 2 method là encode và decode.

  • Method encode sẽ tạo ra token dựa trên payload là user_id và thời gian hết hạn (expiration). lúc đó ta sẽ có token để thực hiện ứng dụng như một key xác thực.
  • Method decode sử dụng để decode token bằng việc sử dụng HMAC_SECRET một đoạn mà để encode trước đó. Trong quá trình decode nếu xảy ra lỗi như hết hạn hoặc validation, JWT sẽ raised những exception, lúc này chúng ra sẽ xử lý bằng module ExceptionHandler.
module ExceptionHandler
  extend ActiveSupport::Concern
  class AuthenticationError < StandardError; end
  class MissingToken < StandardError; end
  class InvalidToken < StandardError; end


  included do
    rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two
    rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request
    rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two
    rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two

    rescue_from ActiveRecord::RecordNotFound do |e|
      json_response({ message: e.message }, :not_found)
    end

    rescue_from ActiveRecord::RecordInvalid do |e|
      json_response({ message: e.message }, :unprocessable_entity)
    end
  end

  private

  def four_twenty_two(e)
    json_response({ message: e.message }, :unprocessable_entity)
  end

  def unauthorized_request(e)
    json_response({ message: e.message }, :unauthorized)
  end
end

Xử lý API request

Ở đây chúng ta sẽ xử lý tất cả các API request, để đảm bảo răng tất cả các request đều được valid token. chúng ta sẽ tạo thưc mục auth trong app.

class AuthorizeApiRequest
  def initialize(headers = {})
    @headers = headers
  end

  def call
    {
      user: user
    }
  end

  private

  attr_reader :headers

  def user
    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
  rescue ActiveRecord::RecordNotFound => e
    raise(
      ExceptionHandler::InvalidToken,
      ("#{Message.invalid_token} #{e.message}")
    )
  end

  def decoded_auth_token
    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
  end

  def http_auth_header
    if headers['Authorization'].present?
      return headers['Authorization'].split(' ').last
    end
      raise(ExceptionHandler::MissingToken, Message.missing_token)
  end
end

Service này sẽ có 1 method là call sẽ trả về một user object khi thực hiện một request thành công. Nó get token từ authorization headers, và thực hiện decode để trả về 1 user object. trong trường hợp xảy ra lỗi thì chúng ta cần care điều đó bằng cách sẽ trả về code lỗi và message thông báo. Vì thế chúng ta sẽ tạo thêm 1 service Message để quản lý Message của app. Chúng ta sẽ vứt nó vào trong app/lib.

class Message
  def self.not_found(record = 'record')
    "Sorry, #{record} not found."
  end

  def self.invalid_credentials
    'Invalid credentials'
  end

  def self.invalid_token
    'Invalid token'
  end

  def self.missing_token
    'Missing token'
  end

  def self.unauthorized
    'Unauthorized request'
  end

  def self.account_created
    'Account created successfully'
  end

  def self.account_not_created
    'Account could not be created'
  end

  def self.expired_token
    'Sorry, your token has expired. Please login to continue.'
  end
end

Xong 2 thằng JsonWebToken : sử dụng để encode và decode bằng việc sử dung jwt , AuthorizeApiRequest: sử dụng để xác thực mỗi request API. Giờ chúng ta sẽ đi tiếp 1 thằng nữa là AuthenticateUser : Class này sẽ thực hiện xác thực User từ việc Login của user bằng email và pasword.

class AuthenticateUser
  def initialize(email, password)
    @email = email
    @password = password
  end

  def call
    JsonWebToken.encode(user_id: user.id) if user
  end

  private

  attr_reader :email, :password

  def user
    user = User.find_by(email: email)
    return user if user && user.authenticate(password)
    raise(ExceptionHandler::AuthenticationError, Message.invalid_credentials)
  end
end

Giải thích 1 chút về class này, nó có 1 method call sẽ thực hiện trả về 1 token khi user được xác thực thành công bằng việc kiểm trả email và pasword của user.

Tiếp theo, chúng ta sẽ thực hiện việc tạo AuthenticationController để thực hiện các service Authentication đã tạo. Controller này cũng sẽ là endpoint của việc login trong app.

class AuthenticationController < ApplicationController
  def authenticate
    auth_token =
      AuthenticateUser.new(auth_params[:email], auth_params[:password]).call
    hash_authen = {
      status: true,
      data: {
          token: auth_token,
          name: "cuongdv"
      }
    }
    json_response(hash_authen)
  end

  private

  def auth_params
    params.permit(:email, :password)
  end
end

trong config/routes.rb chúng ta sẽ config để no sẽ là login.

Rails.application.routes.draw do
  # [...]
  post 'auth/login', to: 'authentication#authenticate'
end

nhưng trước tiên chúng ta cần tạo User thì mới cáo cái để mà Login. =))

class UsersController < ApplicationController
  def create
    user = User.create!(user_params)
    auth_token = AuthenticateUser.new(user.email, user.password).call
    response = { message: Message.account_created, auth_token: auth_token }
    json_response(response, :created)
  end

  private

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

và routes lúc này thì sẽ có thêm thế này :

Rails.application.routes.draw do
  # [...]
  post 'signup', to: 'users#create'
end

UserController sẽ tạo ra user và trả về JSON. chúng ta sử dung create! để khi có 1 error hoặc exception thì sẽ raised để xử lý.

Tới thì có thể đã xong những thứ cần thiêt, bây giờ chúng ta sẽ làm thế nào để mỗi request hay còn gọi là action trong rails, sẽ luôn được check authorize. Rails đã cung cấp before_action callback để giúp chúng ta thực hiện điều này, và chúng ta sẽ làm điều đó ở trong applicationController.

class ApplicationController < ActionController::API
  include Response
  include ExceptionHandler
  include ActionController::Serialization

  before_action :authorize_request
  attr_reader :current_user

  private

  def authorize_request
    @current_user = (AuthorizeApiRequest.new(request.headers).call)[:user]
  end
end

khi mỗi request sẽ được xác thực bởi method authorize_request , nếu request xác thực thành công thì nó sẽ trả về 1 object curent_user để thực hiện ở controller khác. Nhưng ko phải request nào chúng ta cũng thực hiện xác thực, chẳng hạn như sign up, hay login thì chúng ta ko cần đến token. vì vậy chúng ta sẽ bỏ qua nó đối với những request kiểu như thế, nhờ vào callback skip_before_action trong rails.

class AuthenticationController < ApplicationController
  skip_before_action :authorize_request, only: :authenticate
#[...]
end

đối với request sign up của user controller:

class UsersController < ApplicationController
  skip_before_action :authorize_request, only: :create
#[...]
end

Tổng kết

Đến đây thì có lẽ nó đã giúp bạn hiểu 1 phần nào đó về cách authentication bằng gem JWT trong API Rails 5. để tránh bị loãng về kiến thức mình tìm hiểu về JWT muốn chia sẽ, phần tiếp theo mình sẽ nói về các chức năng khác trong API Rails như API versioning, pagination, và serialization.

các bạn có thể tham khảo ở repo này của mình: https://github.com/duongvancuong/API_rails

All Rights Reserved