Single Sign On (SSO) với OAuth2
Theo Wikipedia Single Sign On là một thuật ngữ của việc kiểm soát truy cập nhiều hệ thống liên quan. Với việc sử dụng thuật ngữ này cho phép người dùng đăng nhập với một ID và mật khẩu duy nhất để có thể truy cập vào một hệ thống hay nhiều hệ thống kết nối với nhau mà không cần sử dụng nhiều tên ...
Theo Wikipedia Single Sign On là một thuật ngữ của việc kiểm soát truy cập nhiều hệ thống liên quan. Với việc sử dụng thuật ngữ này cho phép người dùng đăng nhập với một ID và mật khẩu duy nhất để có thể truy cập vào một hệ thống hay nhiều hệ thống kết nối với nhau mà không cần sử dụng nhiều tên đăng nhập và mật khẩu khác nhau của từng hệ thống.
Hay nói một cách đơn giản Single Sign On nghĩa là khi người dùng đăng nhập vào một hệ thống, họ sẽ đăng nhập vào tất cả các hệ thống khác liên quan.
Một ví dụ điển hình cho việc ứng dụng thuật ngữ này là Google, Google sử dụng cho những sản phẩm của họ như: Gmail, YouTube, Google Maps... Điều này được thực hiển bởi một "dịch vụ trung tâm" (trong trường hợp Google là https://accounts.google.com).
Khi bạn đăng nhập lần đầu tiên, cookie được khởi tạo ở "dịch vụ trung tâm", sau đó khi bạn truy cập vào hệ thống thứ hai thì trình duyệt sẽ chuyển hướng tới trung tâm nhưng bạn đã có cookie khi đăng nhập từ trước nên điều đó có nghĩa là bạn đã đăng nhập thành công vào các hệ thống còn lại.
Ngược lại, Single Sign Off là thuật ngữ mà theo đó khi người dùng đăng xuất khỏi hệ thống sẽ chấm dứt quyền truy cập vào các hệ thống còn lại.
Luồng chạy chính của hệ thống sử dụng SSO
- Client redirect người dùng tới Provider cho việc xác minh
- Người dùng đăng nhập vào Provider
- Provider redirect người dùng trở lại Client với một token được sinh ra ngẫu nhiên
- Client sử dụng token đó để tạo lời gọi API tới Provider cùng với ID và Secret Key tạo nên Access Token
- Những request sau được xác minh thông qua Access Token
- Đăng xuất xóa bỏ session ở Client cũng như Provider và database
Bây giờ tôi sẽ hướng dẫn bạn demo một hệ thống có sử dụng SSO trong rails.
Tôi giả sử bạn đã có một hệ thống gọi là Provider dùng để xử lý đăng nhập và một hệ thống gọi là Client dùng đăng nhập thông qua Provider. Provider đã có các chức năng để đăng nhập, đăng xuất, lưu thông tin người dùng vào database.
1. Về phía Provider
- Thêm gem omniauth vào Gemfile
gem "omniauth"
- Tạo access_grants dùng để lưu token và thông tin về quyền đăng nhập
$ rails g model AccessGrant code access_token refresh_token access_token_expires_at user:references state:integer
- Tạo một migration thêm 2 trường provider và uid vào bảng Users
$ rails g migration add_provider_and_uid_to_users provider uid
- Trong model AccessGrant ta định nghĩa các hàm như generate_tokens để tạo các token, redirect_uri_for hay authenticate app/model/access_grant.rb
class AccessGrant < ActiveRecord::Base def generate_tokens self.code = SecureRandom.hex 16 self.access_token = SecureRandom.hex 16 self.refresh_token = SecureRandom.hex 16 end def redirect_uri_for redirect_uri if redirect_uri =~ /?/ redirect_uri + "&code=#{code}&response_type=code&state#{state}" else redirect_uri + "?code=#{code}&response_type=code&state=#{state}" end end def start_expiry_period! update_attribute :access_token_expires_at, Time.now + Devise.timeout_in end class << self def authenticate code AccessGrant.find_by code: code end end end
- Tạo auth_controller dùng để tạo access_token và authorize app/controller/auth_controller.rb
class AuthController < ApplicationController def authorize # Hàm này sẽ được gọi khi user đăng nhập vào provider create_hash = { state: params[:state] } access_grant = current_user.access_grants.create create_hash redirect_to access_grant.redirect_uri_for params[:redirect_uri] end def access_token access_grant = AccessGrant.authenticate params[:code] if access_grant.nil? return end access_grant.start_expiry_period! render :json => {:access_token => access_grant.access_token, :refresh_token => access_grant.refresh_token, :expires_in => Devise.timeout_in.to_i} end end
Sau đó ta thêm một hàm để lấy thông tin mà provider sẽ trả về cho client ở trong auth_controlller.rb
def user hash = { provider: "framgia", id: current_user.id.to_s, info: { email: current_user.email, }, extra: { gender: current_user.gender, position: current_user.position, university: current_user.university } } render :json => hash.to_json end
- Thêm các route config/routes.rb
get "/auth/framgia/authorize" => "auth#authorize" post "/auth/framgia/access_token" => "auth#access_token" get "/auth/framgia/user" => "auth#user" post "/oauth/token" => "auth#access_token"
Như thế là xong cho phần Provider, tiếp theo ta sẽ cài đặt cho phần Client để có thể redirect đến Provider đăng nhập là lấy thông tin trả về.
2. Về phía Client
- Tạo bảng Users gồm những thông tin cơ bản
$ rails g model User uid email status:integer
- Ta thêm các route để xử lý khi Provider callback khi đăng nhập thành công, failure hay khi logout
post '/auth/:provider/callback' => 'user_sessions#create' get '/auth/failure' => 'user_sessions#failure' delete '/logout', :to => 'user_sessions#destroy'
- Tạo thư việc Provider xử lý việc lấy thông tin như thế nào khi Provider trả thông tin về lib/framgia.rb
require 'omniauth-oauth2' module OmniAuth module Strategies class Framgia < OmniAuth::Strategies::OAuth2 # Link tới Provider CUSTOM_PROVIDER_URL = 'http://localhost:4000' option :client_options, { :site => CUSTOM_PROVIDER_URL, :authorize_url => "#{CUSTOM_PROVIDER_URL}/auth/sso/authorize", :access_token_url => "#{CUSTOM_PROVIDER_URL}/auth/sso/access_token" } uid do raw_info['id'] end info do { :email => raw_info['info']['email'] } end extra do { :first_name => raw_info['extra']['first_name'], :last_name => raw_info['extra']['last_name'] } end def raw_info @raw_info ||= access_token.get("/auth/framgia/user.json?oauth_token=#{access_token.token}").parsed end end end end
- Vì OmniAuth được xây dựng cho việc xác minh multi-provider, nên nó cung cấp một class OmniAuth::Builder cho phép ta dễ dàng lựa chọn. Dưới đây là một ví dụ config/initializers/omniauth.rb
APP_ID = 'key' APP_SECRET = 'secret' Rails.application.config.middleware.use OmniAuth::Builder do provider :framgia, APP_ID, APP_SECRET end
- Tiếp theo ta xử lý việc khi người dùng login thành công, thất bại hay logout ở sessions_controller
class SessionsController < ApplicationController def create omniauth = env['omniauth.auth'] user = User.find_by_uid(omniauth['uid']) if not user # New user registration user = User.new(:uid => omniauth['uid']) end user.email = omniauth['info']['email'] user.save session[:user_id] = omniauth redirect_to root_path end def failure flash[:notice] = params[:message] end def destroy session[:user_id] = nil redirect_to "#{CUSTOM_PROVIDER_URL}/users/sign_out" end end
- Một điều cũng không kém phần quan trọng là ta phải import provider vào môi trường config/environment.rb
require "framgia"
Vậy là xong phần cài đặt cho Client, ngoài ra bạn có thể thêm khảo thêm project mẫu Provider: https://github.com/joshsoftware/sso-devise-omniauth-provider
Client: https://github.com/joshsoftware/sso-devise-omniauth-client
Single Sign On là một kỹ thuật rất hay, áp dụng khi ta muốn tập trung hóa việc đăng nhập, quản lý các hệ thống lớn hoặc đơn giản là xác minh để lấy API.
Lợi ích của nó thì rất rõ ràng: giảm thiểu rủi ro cho việc truy cập đến trang web của bên thứ ba, giảm thời gian cho người dùng khi phải đăng nhập nhiều lần.