Ở bài trước Setting Up Authentication with Devise, chúng ta cài đặt Devise để thực hiện một số chức năng liên quan đến Authentication.
Trong bài này chúng ta cũng sẽ tìm hiểu về doorkeeper
và doorkeeper-openid_connect
gems để cài đặt server với các chức năng của OAuth2.0 và OpenID Connect.
Trước tiên mình cần thêm các thuộc tính khác vào trong bảng User để phục vụ cho mục đích sign_in
và call APIs
sau này của các Client.
- Name và Phone number: tương ứng với
scope: profile
- UUID: sẽ sử dụng để làm
sub
để trả về cho Client.- Lý do mình sử dụng UUID ở đây mà không dùng ID của của User là vì
id
là một integer tự động tăng và nó khá nhảy cảm nếu để bị lộ ra bên ngoài, attacker hoặc đối thủ có thể biết được số lượng users hiện tại của chúng ta và thực hiện các hành vi tiêu cực. - Đối với
uuid
nó là một dãy ký tự với độ phức tạp cao, không thể trùng lặp và không mang ý nghĩa vì vậy rất phù hợp để làmsub
trong OAuth2.0 và OpenID Connect.
- Lý do mình sử dụng UUID ở đây mà không dùng ID của của User là vì
OK, xem chi tiết các thay đổi trong Pull Request này nhé mọi người.
Note: à phần before_create :set_uuid
, đang bị sai vì trước khi tạo record nó sẽ thực hiện validation và thấy rằng uuid
không tồn tại nên sẽ không thực hiện được. Vì vậy chúng ta cần phải thêm uuid trước khi validation và chỉ đối với hành động create
thôi, nên code đúng sẽ là: before_validation :set_uuid, on: :create
Basic Configuration and Installing Doorkeeper
Link pull request: https://github.com/learnforward2023/stid_web/pull/4
Doorkeeper là gem (Rails engine) giúp bạn dễ dàng giới thiệu chức năng của ID Provider OAuth2.0 & OIDC cho ứng dụng Ruby on Rails.
- Doorkeeper: https://github.com/doorkeeper-gem/doorkeeper
- Doorkeeper OpenID Connect: https://github.com/doorkeeper-gem/doorkeeper-openid_connect
gem 'doorkeeper'
gem 'doorkeeper-openid_connect'
Chạy lệnh bundle install
➜ bundle install Bundle complete! 16 Gemfile dependencies, 106 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Basic configuration
Doorkeeper cung cấp cho chúng ta một số commands để tạo các config cơ bản:
rails g doorkeeper:install
: khởi tạo các config cơ bản cho các chức năng của OAuth2.0.rails g doorkeeper:openid_connect:install
: khởi tạo các config cơ bản cho các chức năng của OpenID Connect.rails db:migrate
: chạy DB migration cho ứng dụng.
Chạy tất cả các lệnh bên trên và bạn sẽ thấy chúng ta có hai file config mới trong phần config/initializers
doorkeeper.rb
doorkeeper_openid_connect.rb
Doorkeeper
Đầu tiên hãy chú ý vào phần config cho OAuth2.0 trong file doorkeeper.rb
trước.
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}" # Put your resource owner authentication logic here. # Example implementation: # User.find_by(id: session[:user_id]) || redirect_to(new_user_session_url)
end
Phần này giúp chúng ta kiểm tra xem liệu resource_owner
đã authenticated (đăng nhập) ở phía ID Provider hay chưa. Bạn có thể thêm các logic để kiểm tra xem user đã đăng nhập chưa trong block này.
- Thông thường nếu user đã đăng nhập thì session sẽ lưu thông tin của người dùng vì vậy việc kiểm tra session ở đây là điều có thể làm. Hoặc bạn có thể sử dụng các helper methods của Devise ở đây cũng được.
Tiếp theo là admin_authenticator
# If you didn't skip applications controller from Doorkeeper routes in your application routes.rb
# file then you need to declare this block in order to restrict access to the web interface for
# adding oauth authorized applications. In other case it will return 403 Forbidden response
# every time somebody will try to access the admin web interface.
#
# admin_authenticator do
# # Put your admin authentication logic here.
# # Example implementation:
#
# if current_user
# head :forbidden unless current_user.admin?
# else
# redirect_to sign_in_url
# end
# end
Phần này giúp chúng ta kiểm tra xem liệu người dùng hiện tại có phải là Admin hay không? Vì chỉ có Admin mới có quyền truy cập vào các trang quản lý oauth_applications
để đăng ký Client.
- Sau này chúng ta sẽ thêm một trường mới trong bảng User là
:admin
để thực hiện chức năng này.
Giờ đến với scopes
# Define access token scopes for your provider
# For more information go to
# https://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes
#
# default_scopes :public
# optional_scopes :write, :update
Chúng ta có hai options là:
default_scopes
: là những scopes mặc định cho các yêu cầu uỷ quyền. Nói cách khác, nếu Client không truyền tham số cho scopes trong authorization_request thì đây là những scopes mà chúng sẽ được chỉ định.optional_scopes
: là những scopes mà ID Provider chấp nhận uỷ quyền. Nói cách khác, đây là danh sách các scopes mà Client có thể lựa chọn khi đăng ký client với ID Provider.
Cuối cùng là skip_authorization
.
# Under some circumstances you might want to have applications auto-approved,
# so that the user skips the authorization step.
# For example if dealing with a trusted application.
#
# skip_authorization do |resource_owner, client|
# client.superapp? or resource_owner.admin?
# end
Đây là phần giúp bạn có thể đưa ra quyết định xem liệu có hiển thị màn hình authorization cho user hay không? Chúng ta có hai loại Client là confidential
và non-confidential
.
- Đối với non-confidential thì chúng ta phải luôn hiển thị màn hình authorization cho user và nhận consent từ người dùng.
- Mặt khác đối với các ứng dụng confidential thì họ có thể quyết định xem có nên hiển thị màn hình authorization cho user hay không, dựa trên chính sách của Client.
Note: màn hình authorization là màn hình khi người dùng xác thực thành công, ID Provider sẽ hiển thị một màn hình nhắc người dùng rằng: "Hêy! Client XXX muốn thay mặt sử dụng tài khoản của bạn, họ sẽ có quyền truy cập vào .... (scopes mà client đăng ký). Bạn có ổn với điều đó không?".
- Chắc bạn sẽ quen thuộc hơn với trường hợp đăng nhập bằng Facebook và Google vào các ứng dụng khác, bạn sẽ nhận được câu hỏi đồng ý để ứng dụng sử dụng thông tin bạn.
Còn rất nhiều thông tin khác trong file doorkeeper.rb
nhưng chúng ta sẽ tìm hiểu thêm sau nếu có cơ hội.
Doorkeeper OpenID Connect
Giờ thì chuyển sự chú ý của chúng ta sang file doorkeeper_openid_connect.rb
để xem chúng ta có những config nào cho OpenID Connect nào!
Đầu tiên là signing_key
, đây sẽ là một private_key
để chúng ta ký vào id_token
và trả về cho Client. Khi nhận được id_token
Client cần lấy các thông tin từ ID Provider để verify.
signing_key <<~KEY -----BEGIN RSA PRIVATE KEY----- .... -----END RSA PRIVATE KEY-----
KEY
Bên dưới một chút là subject_types_supported
, xác định cách mà nhà cung cấp OpenID Connect biểu diễn danh tính của người dùng trong các ID Token.
subject_types_supported [:public]
public
: Giá trị sub (subject) trong ID Token là duy nhất cho tất cả các Client. Điều này có nghĩa là cùng một sub sẽ được sử dụng cho cùng một người dùng trên tất cả các Client. Đây là lý do vì sao mình quyết định sử dụnguuid
để làm sub.pairwise
: Giá trị sub trong ID Token là duy nhất cho mỗi ứng dụng khách. Điều này có nghĩa là cùng một người dùng sẽ có các giá trị sub khác nhau cho các ứng dụng khác nhau. Điều này giúp bảo vệ sự riêng tư của người dùng.ephemeral
: Đây là một loại ít phổ biến hơn, chỉ sử dụng giá trị sub duy nhất trong phiên hiện tại và không được sử dụng lại.
Trong dự án lần này chúng ta sẽ chỉ hỗ trợ subject_type là public
. Giờ thì hãy đi qua nhanh một số block khác.
resource_owner_from_access_token
: được sử dụng để tìm và trả về thông tin của người sở hữu tài nguyên (resource owner) dựa trên access token.auth_time_from_resource_owner
: trả về thời gian xác thực của resource owner bên trong ID Token nếu Client thêm tham sốmax_age
trong authorization request.reauthenticate_resource_owner
: phương thức này được gọi khi cần xác thực lại resource owner.
Cuối cùng hãy đến với subject
.
subject do |resource_owner, application| # Example implementation: # resource_owner.id # or if you need pairwise subject identifier, implement like below: # Digest::SHA256.hexdigest("#{resource_owner.id}#{URI.parse(application.redirect_uri).host}#{'your_secret_salt'}")
end
Đây sẽ là phần triển khai để quyết định xem giá trị sub
trong ID Token trả về cho Client là gì? Như đã đề cập trong các phần trước đó, chúng ta sẽ sử dụng uuid
để làm sub. Nhưng hãy cập nhật nó sau vậy, giờ hãy để yên như thế.
Còn một số phần khác để customize claims cho ID Token bạn có thể đọc thêm trong file.
Make an API for Test in Future
Đầu tiên mình muốn sửa lại một số configuration và UI một chút.
- https://github.com/learnforward2023/stid_web/pull/4/commits/257631a0a35d2f0513cd3d0f70f67945f38836bd
Giờ chúng ta sẽ thêm :admin
column vào bảng User để cho phép Admin truy cập và tạo oauth_application
cho Client.
rails g migration AddAdminToUser admin:boolean
Chúng ta sẽ có một file mới db/migrate/20240725135643_add_admin_to_user.rb
, chỉnh nội dung bên trong như sau.
# frozen_string_literal: true class AddAdminToUser < ActiveRecord::Migration[7.1] def change add_column :users, :admin, :boolean, default: false end
end
Sau đó run rails db:migration
để cập nhật cấu trúc DB.
Chỉnh sử lại config một chút: PR's Commit
- Sửa lại block
admin_authenticator
để cho kiểm tra xem người dùng hiện tại có phải là Admin hay không?
admin_authenticator do current_user.try(:admin) ? current_user : redirect_to(new_user_session_path)
end
- Tạm thời chúng ta hãy bỏ qua màn hình authorization và không hiển thị nó cho resource owner khi họ đăng nhập hoặc uỷ quyền.
skip_authorization do |_resource_owner, _client| true
end
Giờ chúng ta sẽ tạo một API để lấy thông tin của resource_owner
để test trong tương lai gần. Chi tiết xem trong phần commit này.
- Về cơ bản API này sẽ trả về thông tin của người dùng dựa trên
access_token
nhận được.
Customize Doorkeeper UI and Testing
Trong phần này ngoài những thứ như:
- Generate SIGNING_KEY: sử dụng để ký
id_token
trả về cho Client. - Update scopes: chúng ta sẽ có 3 loại scopes:
openid
: cho biết Client đang sử dụng OpenID Connect để xác thực và yêu cầu thông tinsub
từ Identity Provider.email
: thêm thông tinemail
của resource owner bên trongid_token
trả về cho Client.profile
: thêm thông tinname
vàphone_number
bên trongid_token
trả về cho Client.
- Update code to allow skip authorization: thêm
:skip_authorization
vào trong bảng OauthApplication để cho phép Client bỏ qua màn hình authorization sau khi người dùng đăng nhập thành công.
Thì phần còn lại sẽ chủ yếu là sửa lại UI sử dụng Tailwind cho nó đẹp hơn chút, chi tiết chỉnh sửa các bạn có thể xem ở trong Pull Request này nhá.
Khởi động lại server và truy cập http://localhost:3000 chúng ta sẽ được giao diện như sau:
Hơi cùi một chút nhưng hiện tại hãy truy cập rails console
để biến user đầu tiên thành Admin nào, tại chưa có chức năng tạo user cho admin 😂.
➜ stid_web git:(main) rails console
Loading development environment (Rails 7.1.3.4) [1] pry(main)> User.first.update(admin: true) User Load (0.2ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1 TRANSACTION (0.2ms) BEGIN TRANSACTION (0.1ms) COMMIT
=> true
[2] pry(main)>
OK, giờ hãy click Applications
-> chọn New Application
và đăng ký các thông tin như hình dưới đây.
- Name: tên của Client, trong trường hợp này mình sẽ để là
Study Together Client
tuy nhiên bạn có thể đặt bất kỳ tên nào mình muốn. - Redirect URI: đây là đường dẫn để chúng ta nhận được
authorization code
trong OAuth2.0 sau khi người dùng đã ấn nút chấp nhận uỷ quyền sau khi đăng nhập. - Confidential: trong trường hợp này mình sẽ để ứng dụng thuộc dạng
confidential
, tích vào ô checkbox. - Skip authorization: trong trường hợp này mình sẽ chọn
không
, người dùng sẽ thấy màn hình authorization và quyết định xem có uỷ quyền cho ứng dụng hay không sau khi đăng nhập thành công. - Scopes: mình sẽ chỉ định cả 3 scopes mà ID Provider hỗ trợ, đó là
openid email profile
. Mỗi scope cách nhau bởi dấuspace
.
Submit và bạn sẽ được chuyển đến màn hình chi tiết của application để xem thông tin application vừa tạo.
Tiếp theo để test, hãy click vào nút Authorize
như trong ảnh, và bạn sẽ thấy chúng ta được chuyển đến màn hình Authorization.
- Do bạn đang đăng nhập nên sẽ không thấy bước yêu cầu đăng nhập.
- Nếu bạn copy URL hiện tại, sau đó đăng xuất và truy cập lại vào URL vừa copy, bạn sẽ thấy màn hình đăng nhập sau đó mới đến màn hình Authorization.
Ở bước này, nếu bạn đồng ý uỷ quyền thì ID Provider sẽ redirect bạn về redirect_uri
đã đăng ký trước đó kèm với authorization_code
để bạn đổi lấy access_token
trong tương lai (chúng ta sẽ tìm hiểu sâu thêm trong phần thiết lập Client).
- Vì chưa có Client nào để test nên trước đó mình đã để giá trị của
redirect_uri
làhttp://localhost:3000
nên không có gì đặc biệt ở đây cả.
Conclusion
Như vậy là chúng ta đã cài đặt xong một phần của OAuth2.0 và OpenID Connect phía ID Provider bằng cách sử dụng doorkeeper
và doorkeeper-openid_connect
gems. Trong bài tiếp theo mình sẽ cùng các bạn tạo một Client, sau đó sử dụng ID Provider để đăng nhập vào Client nhé.
Link Github: https://github.com/learnforward2023/stid_web
Tài liệu tham khảo:
- The OAuth 2.0 Authorization Framework: RFC 6749
- OpenID Connect Core
- OAuth2 and OpenID Connect: The Professional Guide
Again, nếu bạn thấy bài viết này hữu ích, hãy cho mình một upvote và follow để mình có thêm động lực viết những bài sau tốt và chất lượng hơn! Thank you!