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ử ...
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