- vừa được xem lúc

Implement Relying Party with OpenID Connect & OAuth2.0

0 0 3

Người đăng: Nguyễn Nam Thắng

Theo Viblo Asia

Trong bài Identity Provider with OAuth 2.0 and OpenID Connect chúng ta đã xây dựng thành công một ID Provider với các chức năng cơ bản.

Giờ là lúc chúng ta triển khai một Relying Party (RP) để có thể kết nối và đăng nhập sử dụng IdP mà chúng ta đã xây dựng trước đó.

Mục tiêu của bài này là:

  • Tạo một project Ruby on Rails
  • Kết nối với ID Provider để đăng nhập
    • Sử dụng OpenID Connect
    • Verify id_token được trả về từ ID Provider

Initializing the Project

Đầu tiên chúng ta cần tạo một Rails project mới với command: rails new project_name. Và database mình sử dụng trong dự án lần này sẽ là MySQL vì vậy chúng ta sẽ chạy command sau:

rails new relying_party_web -d mysql # Additional commands to run the project.
rails db:create
rails db:migrate

Sau đó các bạn sử dụng command: rails server để chạy dự án ở môi trường development.

➜ rails server => Booting Puma
=> Rails 7.1.3.4 application starting in development ...
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000

Truy cập http://localhost:3000 chúng ta sẽ được kết quả!

Như này là ổn rồi, giờ mình sẽ xoá một số code không cần thiết và cài đặt Rubocop cho ứng dụng, bạn có thể xem chi tiết trong các commit mình đính kèm bên dưới.

Note: Rubocop là một trình phân tích mã tĩnh Ruby (còn gọi là linter) và trình định dạng mã. Nó kiểu kiểu giống ESLint, giúp bạn đảm bảo chất lượng mã và thống nhất code style.

Refer: Getting Started with Rails

RP Configuration

Trước tiên mình sẽ cài đặt biến môi trường để lưu các thông tin của ID Provider và Authorization Request. Hãy tạo một file .envrc với nội dung như sau:

export STUDY_TOGETHER_CLIENT_ID=slfxSDOnrbc1dEsbFEIuZTw3HxmR2kGpzNk4vffPzKg
export STUDY_TOGETHER_CLIENT_SECRET=ihYuWYySDnbqTzcQtfqRWMAkSams9oCPzadNEmZXJ_I
export STUDY_TOGETHER_REDIRECT_URI=http://localhost:8080/auth/callback
export STUDY_TOGETHER_GRANT_TYPE=authorization_code
export STUDY_TOGETHER_SCOPE=openid email profile export IDP_ISSUER=http://localhost:3000/
export IDP_DISCOVERY_ENDPOINT=http://localhost:3000/.well-known/openid-configuration
export IDP_AUTHORIZE_ENDPOINT=http://localhost:3000/oauth/authorize
export IDP_TOKEN_ENDPOINT=http://localhost:3000/oauth/token
export IDP_USERINFO_ENDPOINT=http://localhost:3000/oauth/userinfo
  • client_id, secret, redirect_uri, grant_typescopes : là những thông tin về Client mà chúng ta đã đăng ký với ID Provider trước đó.
  • issuer : ám chỉ bên phát hành token, vì vậy nó sẽ là địa chỉ của ID Provider.
  • discovery_endpoint : là nơi chúng ta có thể lấy được mọi thông tin về ID Provider để verify id_token.
  • authorization_endpoint : nơi mà chúng ta sẽ gửi yêu cầu uỷ quyền tới.
  • token_endpoint : nơi chúng ta sẽ dùng authorization_code để đổi lấy access_token từ ID Provider.
  • userinfo_endpoint : là điểm cuối để chúng ta lấy thông tin của user, tuy nhiên vì chúng ta sẽ sử dụng scope là openid vì vậy một id_token sẽ được trả về cùng với access_token. Cũng không cần endpoint này lắm nhưng nếu bạn không muốn verify id_token một cách thủ công thì có thể sử dụng điểm cuối này kèm với access_token để lấy thông tin user.
    • Tuy nhiên mình sẽ verify id_token sử dụng discovery_endpoint cho nó chuẩn bài nhé.

Authenticate with ID Provider

Đầu tiên chúng ta sẽ tạo bảng User trước để lưu thông tin người dùng cho ứng dụng RP của chúng ta.

rails generate model User uuid:string email:string

Sau đó chạy rails db:migrate để update cấu trúc DB.

  • uuid : là định danh người dùng, tuy nhiên chúng ta sẽ không tự động generate từ phía RP nữa mà sẽ sử dụng để lưu giá trị sub mà IdP trả về trong id_token.
  • email : cũng sẽ lưu giá trị mà IdP trả về bên trong id_token.

Tiếp theo mình sẽ customize lại root_page để dễ dàng truy cập và authentication hơn về mặt giao diện thôi.

  • Tạo top page sử dụng rails g controller Top index : lệnh này giúp chúng ta tạo view, controllerroute cho top page.
  • Tiếp theo là thêm flash component để hiển thị error/info cho user.

Add sessions_controller

Tạo một sessions_controller.rb có nội dung sau:

# frozen_string_literal: true class SessionsController < ApplicationController def new; end def destroy; end
end
  • new : generate authorization_request và redirect người dùng đến authorize_endpoint.
  • destroy : xử lý đăng xuất cho user.

Và thêm nội dung sau vào routes.rb.

delete '/logout', to: 'sessions#destroy'
get '/auth/stid', to: 'sessions#new'

Add activerecord-session_store gem to manage user's session

Trong bước này chúng ta sẽ sử dụng gem activerecord-session_store để quản lý session của người dùng thông qua ActiveRecord để làm việc với session dễ dàng hơn trong tương lai.

Thêm gem này vào Gemfile và chạy bundle install.

gem 'activerecord-session_store'

Chạy rails generate active_record:session_migration để tạo các file migration tự động. Sau đó tạo một bảng user_session nữa với nội dung sau:

# chạy lệnh rails g migration CreateUserSessions và đổi nội dung file như bên dưới! # frozen_string_literal: true class CreateUserSessions < ActiveRecord::Migration[7.1] def change create_table :user_sessions do |t| t.references :user, null: false, foreign_key: true t.string :session_id t.timestamps end end
end

Run rails db:migrate và thêm nội dung sau:

config.session_store :active_record_store, key: '_relying_party_session'

Và một số thay đổi khác, xem chi tiết commit này. Tóm lại phần này mình chỉ tạo ra các bảng cần thiết để quản lý session, và thêm một số helper methods để lấy thông tin người dùng hiện tại và kiểm tra xem người dùng có đang đăng nhập hay không?

Login with Study Together ID

Trước tiên chúng ta sẽ sửa lại sessions#new để tạo authorization_request và redirect đến authorization_endpoint.

def new redirect_to authorization_url
end ... private def authorization_url client_id = ENV['STUDY_TOGETHER_CLIENT_ID'] redirect_uri = ENV['STUDY_TOGETHER_REDIRECT_URI'] scope = ENV['STUDY_TOGETHER_SCOPE'] state = SecureRandom.hex(16) session[:state] = state "#{ENV['IDP_AUTHORIZE_ENDPOINT']}?response_type=code&client_id=#{client_id}&redirect_uri=#{redirect_uri}&scope=#{scope}&state=#{state}"
end

Khởi động lại server rails s -p 8080 và thử đăng nhập xem như nào?

  • Nhớ khởi động cả ID Provider project ở cổng 3000 nữa nhé mọi người.

Sau khi click vào Login with Study Together ID button, bạn sẽ được chuyển hướng đến ID Provider.

Lúc này chúng ta nhập thông tin đăng nhập và click Sign in button.

Nếu thông tin emailpassword chính xác, bạn sẽ được chuyển hướng đến Authorization Page để uỷ quyền cho ứng dụng. Click Yes, I Authorize bạn sẽ thấy màn hình sau.

Sau khi bạn uỷ quyền cho ứng dụng, IdP sẽ redirect bạn về redirect_uri mà bạn đã đăng ký trước đó với IdP. Do chúng ta chưa cấu hình route cho /auth/callback vì vậy một thông báo lỗi xuất hiện trên màn hình như bạn thấy.

Giờ hãy tạo một controller mới có tên là omniauth_callbacks_controller.rb với nội dung sau:

# frozen_string_literal: true class OmniauthCallbacksController < ApplicationController include HttpRequestable before_action :verify_state, only: :callback def callback token_response = fetch_token(params[:code]) validate_id_token(token_response['id_token']) user = find_or_create_user create_user_session(user) redirect_to root_path, notice: 'Signed in successfully' rescue StandardError => e redirect_to root_path, alert: "Authentication failed: #{e.message}" end def failure redirect_to root_path, alert: 'Authentication failed' end private def verify_state redirect_to root_path, alert: 'State mismatch error' if params[:state] != session[:state] end def fetch_token(code) post_request(ENV['IDP_TOKEN_ENDPOINT'], { grant_type: ENV['STUDY_TOGETHER_GRANT_TYPE'], code:, redirect_uri: ENV['STUDY_TOGETHER_REDIRECT_URI'], client_id: ENV['STUDY_TOGETHER_CLIENT_ID'], client_secret: ENV['STUDY_TOGETHER_CLIENT_SECRET'] }) end def validate_id_token(id_token) discovery_response = get_request(ENV['IDP_DISCOVERY_ENDPOINT']) jwks_uri = discovery_response['jwks_uri'] keys = get_request(jwks_uri)['keys'] decoded_token = JWT.decode(id_token, nil, true, { algorithm: 'RS256', jwks: { keys: }, verify_aud: true, aud: ENV['STUDY_TOGETHER_CLIENT_ID'] }) @payload = decoded_token.first validate_token_timestamps end def validate_token_timestamps raise 'ID token has expired' if Time.zone.at(@payload['exp']) < Time.zone.now raise 'ID token is not valid yet' if Time.zone.at(@payload['iat']) > Time.zone.now end def find_or_create_user User.find_or_create_by(uuid: @payload['sub']) do |user| user.email = @payload['email'] end end def create_user_session(user) UserSession.create(user:, session_id: request.session_options[:id].private_id) session[:uuid] = user.uuid end
end

Ở đây chúng ta sẽ có hai methods chính là callbackfailure.

  • Callback: khi user xác thực thành công và đồng ý uỷ quyền thì chúng ta sẽ nhận được authorization_code ở đây.
  • Failure: trong trường hợp lỗi xác thực hoặc người dùng không đồng ý yêu cầu uỷ quyền.

Giải thích code:

  • Trước khi callback được thực thi chúng ta cần phải verify_state, chi tiết xem phần Authorization Request trong OAuth2.0 RFC 6749.
  • Phần đầu tiên trong callback, chúng ta sẽ phải gọi đến token_endpoint của IdP để đổi authorization_code lấy access_tokenid_token.
  • Vì hiện tại chúng ta không sử dụng đến access_token vì vậy ở đây mình chỉ validate_id_token, bao gồm việc xác minh chữ ký, và một số claims quan trọng (aud, timestamps).
  • Sau khi xác mình thành công thì chúng ta sẽ tạo một người dùng mới nếu họ chưa tồn tại, và lưu session mới cho người dùng.

Chi tiết thay đổi trong phần này xem tại đây!

Giờ hãy đăng xuất và thử đăng nhập lại!

Conclusion

Như vậy là chúng ta đã thành công tạo một sample RP và triển khai đăng nhập với ID Provider thành công, phần triển khai code hơi dài và khó theo dõi nếu đưa hết lên đây, vì vậy các bạn chịu khó xem thông qua Github nhá.

Ở các bài sau mình sẽ thêm các chức năng để hoàn thiện dự án này:

  • Backchannel Logout
  • Withdrawable Check
  • etc.

Link Github: https://github.com/learnforward2023/stid_web

Tài liệu tham khảo:

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!

Bình luận

Bài viết tương tự

- vừa được xem lúc

Sử dụng Misoca API (oauth2) với Python

Với bài viết này giúp chúng ta có thể nắm được. ・Tìm hiểu cách xử lý API misoca bằng Python.

0 0 43

- vừa được xem lúc

OAuth là gì? Cách thức hoạt động của OAuth

. OAuth cho phép các trang web và dịch vụ chia sẻ tài nguyên giữa những người dùng. Nó được sử dụng rộng rãi, nhưng hãy lưu ý về các lỗ hổng của nó.

0 0 49

- vừa được xem lúc

Giải thích về cách thức hoạt động của OAuth 2.0

OAuth2.0 là một giao thức authorization, cho phép truy cập tài nguyên của resource owner bằng cách bật client applications trên các dịch vụ HTTP như Facebook, GitHub, v.

0 0 36

- vừa được xem lúc

OAuth là gì ? Nó hoạt động như thế nào ?

Mở đầu. Khi bạn vào một trang web muốn sử dụng các dịch vụ của một trang web khác — chẳng hạn như đăng nhập vào bằng tài khoản Facebook — thay vì yêu cầu bạn chia sẻ tài khoản Facebook của mình của mì

0 0 48

- vừa được xem lúc

Từ OAuth đến OpenID Connect

Khi sử dụng các ứng dụng như Draw.io hay một số ứng dụng, trang web nào đó, bạn đã bao giờ gặp thông báo yêu cầu cấp quyền truy cập đến Google drive chưa.

0 0 100

- vừa được xem lúc

Tìm hiểu đôi chút về OAuth2

Chắc hẳn một trong số các bạn cũng đã từng nghe qua khái niệm OAuth trước đây. Về cơ bản, OAuth là một phương thức xác thực giúp một ứng dụng bên thứ 3 có thể được ủy quyền bởi người dùng để truy cập

0 0 31