Giới thiệu về Doorkeeper và OAuth 2.0

Trong bài viết này, mình sẽ giới thiệu cho các bạn cách tạo một OAuth2 provider và secure API với sự giúp đỡ của Doorkeeper. Chúng ta sẽ làm từ những bước chuẩn bị, integrate Doorkeeper, customize một chút. Ở phần 2 của series chúng ta sẽ cùng thảo luận về những ưu điểm của việc customize views sử dụng refresh tokens, crafting 1 OmniAuth provider và Doorkeeper secure với defualt routes

Tạo App

I am going to use Rails 4.2 for this demo. We’ll create two applications: the actual provider (let’s call it “server”) and an app for testing purposes (“client”). Start with the server: Trong ví dụ này chúng ta sẽ sử dụng Rails 4.2 cho demo này. Chúng ta sẽ tạo ra 2 ứng dụng: 1 ứng dụng cung cấp dịch vụ (mà chúng ta hay gọi là "server") và một app với mục đích test, còn gọi là "client". Đầu tiên chúng ta sẽ bắt đầu với server

$ rails new Keepa -T

Chúng ta sẽ cần một số cách authentication trong app này, với Doorkeeper không bắt buộc phải sử dụng app nào. Trong bài viết này mình sử dụng bcrypt

Gemfile

gem 'bcrypt-ruby'

Cài đặt gem, generate và apply migration

$ bundle install
$ rails g model User email:string:index password_digest:string
$ rake db:migrate

Bây giờ chúng ta sẽ thiết lập một số cài dặt validate cho bcrypt

models/user.rb

has_secure_password
validates :email, presence: true

Tạo một controller để đăng kí

users_controller.rb

class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      session[:user_id] = @user.id
      flash[:success] = "Welcome!"
      redirect_to root_path
    else
      render :new
    end
  end

  private

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

Tiếp theo là view tương ứng

views/users/new.html.erb

<h1>Register</h1>

<%= form_for @user do |f| %>
  <%= render 'shared/errors', object: @user %>
  <div>
    <%= f.label :email %>
    <%= f.email_field :email %>
  </div>

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

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

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

<%= link_to 'Log In', new_session_path %>

views/shared/_errors.html.erb

<% if object.errors.any? %>
  <div>
    <h5>Some errors were found:</h5>

    <ul>
      <% object.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

Cài đặt routes:

config/routes.rb

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

Để kiểm tra user có login hay không, chúng ta sẽ dụng hàn current_user

application_controller.rb

private

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

helper_method :current_user

Chúng ta sẽ chia controller để quả lý user session

sessions_controller.rb

class SessionsController < ApplicationController
  def new
  end

  def create
    @user = User.find_by(email: params[:email])
    if @user && @user.authenticate(params[:password])
      session[:user_id] = @user.id
      flash[:success] = "Welcome back!"
      redirect_to root_path
    else
      flash[:warning] = "You have entered incorrect email and/or password."
      render :new
    end
  end

  def destroy
    session.delete(:user_id)
    redirect_to root_path
  end
end

authenticate là methed được cung cấp bởi bcrypt dùng để kiểm tra password có chính xác hay không

Bây giờ chúng ta sẽ xử lý phần view và routes của sessions_controller

views/sessions/new.html.erb

<h1>Log In</h1>

<%= form_tag sessions_path, method: :post do %>
  <div>
    <%= label_tag :email %>
    <%= email_field_tag :email %>
  </div>

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

  <%= submit_tag 'Log In!' %>
<% end %>

<%= link_to 'Register', new_user_path %>

config/routes.rb

resources :sessions, only: [:new, :create]
delete '/logout', to: 'sessions#destroy', as: :logout

Cuối cùng, Tạo một static pages controller (page tĩnh), root page, và route:

pages_controller.rb

class PagesController < ApplicationController
  def index
  end
end

views/pages/index.html.erb

<% if current_user %>
  You are logged in as <%= current_user.email %><br>
  <%= link_to 'Log Out', logout_path, method: :delete %>
<% else %>
  <%= link_to 'Log In', new_session_path %><br>
  <%= link_to 'Register', new_user_path %>
<% end %>

config/routes.rb

root to: 'pages#index'

Mọi thứ khá cơ bản và đơn giản, như vậy là server app đã sẵn sàng, bước tiếp theo chúng ta sẽ bắt đầu làm việc với Doorkeeper

Tích hợp Doorkeeper

Thêm gem doorkeeper vào Gemfile

Gemfile

gem 'doorkeeper'

Cài đặt và chạy Dookeeper's generator

$ bundle install
$ rails generate doorkeeper:install

Bước generate này sẽ tại ra một intializer file và thêm add_doorkeeper vào routes.rb. Dòng này sẽ cùng cấp đầy đủ Doorkeeper routes (để register OAuth2, request access token v..v.), và mình sẽ đề cập trong phần tới.

Bước tiếp theo là generate migrations, mặc định Doorkeeper sử dụng ActiveRecord, nhưng bạn có thể sử dụng doorkeeper-mongodb cho Mongo

$ rails generate doorkeeper:migration
$ rake db:migrate

Mở File Doorkeeper Initializer và tìm đến dòng resource_owner_authenticator do. Mặc định sẽ có exception, nên thay đổi block conttent bằng

config/initializers/doorkeeper.rb

User.find_by_id(session[:user_id]) || redirect_to(new_session_url)

User model sẽ được giới hạn bở Doorkeeper. Bạn có thể vào server, đăng kí, và chuyển hướng tới . Ở Page này sẽ tạo ra mới một OAuth 2 application, tạo một call back . Tiếp theo chúng ta sẽ tạo Client App ở cổng 3001, vì cổng mặc định 3000 hiện đã sử dụng cho Server App (các bạn có thể sử dụng cổng khác miễn là 2 app chạy đồng thời với nhau và ở 2 port khác nhau).

##Tạo Client App

Chạy Rails generator để tạo một app mới

$ rails new KeepaClient -T

Tạo static page, root page, và route

pages_controller.rb

class PagesController < ApplicationController
  def index
  end
end

views/pages/index.html.erb

<h1>Welcome!</h1>

config/routes.rb

root to: 'pages#index'

Bây giờ chúng ta sẽ tạo file local_env.yml để lưu trữ một số thông itn cấu hình, đặc biệt là Client ID và Secret received từ server app ở bước trước

config/local_env.yml

server_base_url: 'http://localhost:3000'
oauth_token: <CLIENT ID>' 
oauth_secret: '<SECRET>'
oauth_redirect_uri: 'http%3A%2F%2Flocalhost%3A3001%2Foauth%2Fcallback'

Load ở trong ENV

config/application.rb

if Rails.env.development?
  config.before_configuration do
    env_file = File.join(Rails.root, 'config', 'local_env.yml')
    YAML.load(File.open(env_file)).each do |key, value|
      ENV[key.to_s] = value
    end if File.exists?(env_file)
  end
end

Chúng ta nên bỏ file .yml vào trong gitignore

config/local_env.yml

Lấy Access Token

Ok, bây giờ chúng ta đã có thể lấy được access token dùng để đẩy các API requests. Bạn có thể sử dụng oauth2 gem để phục vụ việc này (Tham khảo link này ). Mình sẽ nói rõ hơn để các bạn có thể hiểu được luồng đi của nó.

Chúng ta sẽ sử dụng rest-client(https://github.com/rest-client/rest-client) để send request

Tạo ra new gem and bundle install

Gemfile

gem 'rest-client'

Để lấy được access token , user cần phải vào link "localhost:3000/oauth/authorize" trong khi cung cấp Client ID, redirect URI, và response type. Mình sẽ giới thiệu về helper method để có thể generate ra URL thích hợp

application_helper.rb

def new_oauth_token_path
  "#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code"
end

Gọi nó ra ở màn hình hcinhs ở Client App

views/pages/index.html.erb

<%= link_to 'Authorize via Keepa', new_oauth_token_path %>

Bây giờ, chúng ta sẽ config callback route

config/routes.rb

get '/oauth/callback', to: 'sessions#create'

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&code=#{params[:code]}&grant_type=authorization_code&redirect_uri=#{ENV['oauth_redirect_uri']}"
    response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params)
    session[:access_token] = response['access_token']
    redirect_to root_path
  end
end

Sau khi vào link "localhost:3000/oauth/authorize" user sẽ được dẫn đến callback URL với code parameter. Bên trong create acrtion, generate các chuỗi params (với client_id, client_secret, code````,grant_type, vàredirect_uri``` ) và sau đó thực hiện một POST request tới "localhost:3000/oauth/token". Nếu mọi thứ hoàn thành, sẽ nhận được phản hồi bao gồm JSON với access token kèm với lifespan (mặc định là 2 giờ). Nếu không thì lỗi 401 sẽ xuất hiện.

Giới thiệu về Simple API

Quay trở lại với Server App và tạo mới một controller

controllers/api/users_controller.rb

class Api::UsersController < ApplicationController
  before_action :doorkeeper_authorize!

  def show
    render json: current_resource_owner.as_json
  end

  private

  def current_resource_owner
    User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
  end
end

Proceed thực hiện step-by-step:

before_action :doorkeeper_authorize! cho phép chúng ta bảo vệ các action controller khỏi các non-authorized request, nghĩa là người dùng phải cung cấp access token để thực hiện hành động. Nếu token không được cung cấp, Doorkeeper sẽ block và sẽ trả về lỗi 401 current_resource_owner là method dùng để thông với chủ sở hữu là đã có một token được gửi doorkeeper_token.resource_owner_id trả về id của user thực hiện request đó, bởi vì, chúng ta đã thay đổi resource_owner_authenticator để phù hợp với Doorkeeper initializer. as_json biến đối tượng User người dùng vào JSON. Bạn có thể cung cấp một except để exclude một số trường current_resource_owner.as_json(except: :password_digest) Thêm một route mới

config/routes.rb

namespace :api do
  get 'user', to: 'users#show'
end

Bây giờ, bạn có thể vào trang "localhost:3000/api/user", bạn sẽ thấy một trang trắng. Mở console, và sẽ thấy thông báo lỗi 401, nghĩa là action của bạn được bảo vệ.

application_controller.rb

def doorkeeper_unauthorized_render_options(error: nil)
  { json: { error: "Not authorized" } }
end

Thay đổi ở trang chính của Client App

<% if session[:access_token] %>
  <%= link_to 'Get User', "http://localhost:3000/api/user?access_token=#{session[:access_token]}" %>
<% else %>
  <%= link_to 'Authorize via Keepa', new_oauth_token_path %>
<% end %>

Click vào "Get User" link và quan sát kết quả - bạn sẽ thấy chi tiết của người dùng của bạn!

Làm việc với Scopes

Scope là cách để xác định hành động của client sẽ có thể thực hiện. Có hai loại scope mới:

  • public - cho phép người dùng có thể fetch dữ liệu từ user
  • write - cho phép nguồi dùng thay đổi user profile

Đầu tiên, thay đổi file Doorkeeper intializer và include thêm scope:

config/initializers/doorkeeper.rb

default_scopes :public
optional_scopes :write