Giới thiệu về Doorkeeper và OAuth 2.0
Bài đăng này đã không được cập nhật trong 7 năm
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
All rights reserved