Single Sign On (SSO) với OAuth2

Single Sign On (SSO) là gì?

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

Cách Implement SSO trong ứng dụng Rails

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 provideruid 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

Kết luận

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.