Single Sign On (SSO) với OAuth2
Bài đăng này đã không được cập nhật trong 8 năm
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ênAccess 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ưutoken
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 authenticateapp/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à authorizeapp/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.
All rights reserved