TẢN MẠN VỀ OAUTH VÀ ỨNG DỤNG SCOPE VỚI MỘT BÀI TOÁN ĐƠN GIẢN

Anh Hoang
SotaTek
Published in
12 min readDec 13, 2018
  1. SAO LẠI LÀ OAuth

Tôi sẽ bắt đầu câu chuyện bằng một ví dụ điển hình: Bạn đang muốn tải một tài liệu trên tailieu.vn, hệ thống yêu cầu đăng nhập để tải văn bản bạn yêu cầu. Và tin tôi đi, với những văn bản phù hợp với chất văn của bạn, nó đều mất phí cả thôi. Không muốn trả một khoản tiền (dù vài nghìn đồng) cho một văn bản xài 1 lần cho mục đích hoàn thành bài đồ án, bạn tìm trên các kênh văn bản khác và một chuỗi hành động lặp đi lặp lại như chính cái cách bạn làm trên tailieu.vn vậy. Sau cùng, bạn nhận ra top đầu trong hòm thư của mình là hàng loạt các Email yêu cầu kích hoạt tài khoản. Và bạn cũng nhận ra mình đã tiêu hàng tiếng đồng hồ để lặp đi lặp lại cái hành động: đăng ký - kích hoạt - đăng nhập nhàm chán này.

May mắn cho các bạn, các ông lớn như Twitter, Facebook hay Google, với lòng trắc ẩn của mình đã ngồi lại với nhau và quyết định làm điều có ích cho xã hội mang tên - OAuth.

2. GIỚI THIỆU

OAuth là một phương thức chứng thực các ứng dụng có thể chia sẻ tài nguyên với nhau mà không cần chia sẻ các thông tin username và password. Nó là từ ghép của O - Open, và Auth - tượng trưng cho 2 nghĩa:

  • Authentication: Xác thực người dùng
  • Authorization: Người dùng ủy quyền cho ứng dụng truy cập tài nguyên của họ

Quay lại ví dụ trên với ông hoàng Facebook. Thời đại này người người, nhà nhà ai cũng có ít nhất một tài khoản Facebook, và chúng ta có thể dùng tài khoản này để đăng nhập ở hàng triệu ứng dụng (miễn là họ hỗ trợ tính năng này) mà không phải nhìn thấy vài cái ô nhập thông tin nhàm chán và già cỗi.

Là một người dùng thông thái, có thể bạn sẽ cân nhắc rằng: lỡ có ông hacker nào cao tay lấy được thông tin người dùng của mình với phương thức này thì sao. Trong trường hợp này, bạn không cần quá lo lắng khi cơ chế của Facebook (hay hàng loạt ông lớn khác) khi cấp cho ứng dụng một mã xác thực, cũng đã phân quyền hạn nhất định cho mã đó, và tất nhiên, họ sẽ không cho phép mã đó truy cập vào thông tin username và password tài khoản Facebook của bạn rồi.

Ngược dòng lịch sử phát triển, ta có thể điểm qua vài nét chính như sau

  • Năm 2006, Twitter phát triển hệ thống OpenID phục vụ cho đăng nhập các ứng dụng trong hệ thống của Twitter - được coi là chuẩn OAuth đầu tiên, yêu cầu người dùng phải cung cấp thông tin cá nhân - cũng chính là điểm yếu của nó. Các ông lớn về mạng xã hội thấy vậy đã ngồi lại với nhau và phát triển hệ thống lên một tầng cao mới.
  • Năm 2010, IETF - Tổ chức quản lý tiêu chuẩn mạng Internet - phát hành phiên bản chính thức đầu tiên của OAuth 1.0 (RFC 5849). Sau đó, một lỗi bảo mật nghiêm trọng với cái tên Session Fixation xảy ra trên OAuth 1.0, cho phép các Hacker lừa ứng dụng bên thứ 3 trao quyền truy cập vào tài khoản và dữ liệu người dùng
  • Năm 2012 OAuth2 ra đời, tuy vẫn còn các lỗi bảo mật nhưng hiện vẫn đang được sử dụng rất rộng rãi.

OAuth2 không đơn thuần chỉ là giao thức kết nối, nó là một “Framework - nền tảng” mà chúng ta triển khai ở cả 2 phía: Client và Server. Chúng ta sẽ cùng tìm hiểu các thuật ngữ cần biết của OAuth2, gồm:

  • Resource Owner: chủ sở hữu các dữ liệu ta muốn chia sẻ. Chẳng hạn, bạn muốn chia sẻ thông tin Email - Username Facebook cho 1 bên có chức năng đăng nhập bằng Facebook, thì ở đây, thông tin Email - Username này là tài nguyên cần chia sẻ (resource), còn ta chính là Resource Owner.
  • Resource Server: nơi chứa thông tin dữ liệu cần chia sẻ. Server này phải có khả năng nhận và trả lời các yêu cầu (request) truy xuất dữ liệu. Trong ví dụ trên, Resource Server là Facebook.
  • Client: là những chương trình, ứng dụng có nhu cầu muốn sử dụng tài nguyên được chia sẻ.
  • Authorization Server: đối tượng quyết định việc cấp quyền truy cập vào dữ liệu cho Client. Như trong ví dụ trên, đấy chính là Authorization Server của Facebook. Đôi khi Resource Server và Authorization Server có thể là một, nhưng về mặt chức năng mà nói, đây là 2 chức năng hoàn toàn riêng biệt.

3. OAuth HOẠT ĐỘNG NHƯ THẾ NÀO

Với OAuth 1.0, chúng ta cần 3 bước để xác thực:

  • Client gọi lên Authorization Server để xin xác nhận tạm thời (Temporary Credentials)
  • Mở một Webpage với thông tin xác nhận tạm thời, để Resource Owner đăng nhập vào Server và trao quyền truy cập cho phía Client. Sau đó Redirect về Client, dựa theo thông tin Callback gắn kèm trong Request đầu tiên, gửi theo thông tin Token tạm thời.
  • Gọi lên Authorization Server một lần nữa, sử dụng cả xác nhận tạm thời. Server lúc này sẽ trả về cho Client một Access Token. Sử dụng Token này, Client từ đó về sau có thể truy cập thông tin dữ liệu của Resource Owner, cho tới khi nào Resource Owner rút lại quyền truy cập của Access Token.

Chúng ta có thể dễ dàng chỉ ra các điểm yếu của OAuth 1.0 rằng:

  • Cách thức triển khai khá phức tạp (lại còn Token chính với tạm), việc thực thi đôi khi không thuận lợi.
  • Access Token không có Time Life, và phần lớn người dùng không mấy khi quyết định rằng khi nào ta sẽ vào trình quản lý và hủy việc cấp quyền này đi cả.
  • Không có phân quyền chi tiết cho mỗi Token (ví dụ Token 1 chỉ được thực hiện hành động A trên tài nguyên X, Token 2 thì có quyền B trên tài nguyên Y…)

Để giải quyết bài toán đó, OAuth 2.0 ra đời. Khác với phiên bản đầu là một giao thức (Protocol), OAuth 2.0 là một nền tảng (Framework). Trong chuẩn 2.0, Client không chỉ được quyền truy xuất dữ liệu nhân danh Resource Owner, mà Client có thể nhân danh chính mình. Qua đó, việc phân quyền cụ thể cho từng Client cũng có thể thực hiện được. Chính vì thế, OAuth 2.0 không tương thích với phiên bản đầu tiên của nó. Về luồng thực hiện:

Sơ đồ luồng hoạt động của OAuth2 (nguồn: Viblo)

Ta có thể tóm gọn bằng 3 luồng chính:

  • Client gửi yêu cầu quyền truy cập tới User, nếu đồng ý, User chấp nhận cấp quyền.
  • Client gửi Authorization Grant do User cung cấp lên cho Authorization Server, sau khi xác nhận, Server sẽ gửi về Access Token cho Client.
  • Client sử dụng Access Token trong quá trình làm việc với Resource Server. Resource Server sẽ kiểm tra tình hợp lệ của Token mỗi lần nó được gửi lên, nếu hợp lệ, dữ liệu sẽ được trả về cho Client.

Tất nhiên, luồng hoạt động thực tế có thể sẽ khác nhau tùy thuộc vào việc ứng dụng sử dụng loại ủy quyền (Grant Type) nào, trên đây chỉ là một ý tưởng chung nhất của OAuth2.

OAuth định nghĩa ra 4 loại ủy quyền tùy thuộc vào phương thức mà Application sử dụng để yêu cầu ủy quyền, gồm:

  • Authorization Code (mã xác thực): loại ủy quyền phổ biến nhất, sử dụng với các Server-side Application
  • Implicit (ủy quyền ngầm): giống với Authorization Code. Tuy nhiên, token sẽ được phản hồi về phía client mà không phải trao đổi Authorization Code. Loại ủy quyền này phổ biến với phần lớn các ứng dụng JavaScript và Mobile, kho mà các chứng thực phía Client không thể lưu trữ một cách an toàn.
  • Password Grant Token (ủy quyền thông tin người dùng): loại ủy quyền cho phép các ứng dụng bên thứ 3, ví dụ như các ứng dụng Mobile, có thể nhận token bằng cách sử dụng thông tin tài khoản người dùng. Nó cho phép người dùng nhận token mà không phải thông qua các bước redirect theo luồng chính thống của OAuth2.
  • Client Credentials (ủy quyền bằng thông tin ứng dụng): phù hợp với giao tiếp machine-to-machine. Ví dụ, chúng ta có thể sử dụng dạng xác thực này để chạy một nhiệm vụ định kỳ để kiểm tra hiệu năng thông qua API.

Dù ta có sử dụng loại định danh nào, thông tin cần gửi đi luôn cần 1 trong 2 thông số tối quan trọng như sau:

  • Client Identifier (Client ID): chuỗi ký tự được sử dụng bởi Service API để định danh ứng dụng, đồng thời cũng được dùng để xây dựng ‘Authorization URL’ hiển thị phía User.
  • Client Secret: là một chuỗi ký tự dùng cho việc xác thực định danh ứng dụng khi ứng dụng yêu cầu truy cập thông tin tài khoản User. Chuỗi này được giữ bí mật giữa Client và Authorization Server.

4. MỘT BÀI TOÁN ĐƠN GIẢN VỚI LARAVEL VÀ ỨNG DỤNG TÍNH NĂNG SCOPE

Lý thuyết luôn khô khan nhưng phải có nó mới đi được tới thực hành, giờ ta sẽ đi vào ví dụ cho nó trực quan sinh động. Dựa trên yêu cầu từ chính dự án thực tế từ SotaTek, mình xin được đưa ra một bài toán đơn giản rằng: phân quyền truy cập giữa 2 tài khoản người dùng là User và Customer Support, với Customer Support có thể truy cập vào một số API được chỉ định cho chức năng chăm sóc khách hàng. Bắt đầu nào!

Mình xin được mặc định rằng các bạn đã biết về Laravel và một package xử lý OAuth chính chủ là Passport. Quá trình cài đặt Laravel mình sẽ không đi vào chi tiết. Nhưng với Passport, cùng điểm qua vài bước nào.

Passport sẽ quản lý Access Token và Client bằng Database. Tất nhiên chúng ta sẽ không tốn công tự tạo Migration làm gì, Service Provider của Package đã phụ ta một tay. Công sức bỏ ra chỉ là một dòng nhập lệnh migrate là xong

php artisan migrate

Lựa chọn Grant Type phù hợp là điều cần thiết khi xây dựng ứng dụng. Với yêu cầu bài toán là phân định người dùng, Password Grant Token là giải pháp hợp lý, tận dụng việc đăng nhập hệ thống của người dùng. Chúng ta sẽ tiến hành chạy lệnh đầu tiên để thiết lập Passport vào hệ thống Laravel

php artisan passport:install --password

Hệ thống sẽ sinh ra một Client IDClient Secret, nhanh tay lưu nó lại. Và trong trường hợp bạn quên nó, các thông tin có thể được tìm thấy tại bảng oauth_clients.

Chúng ta cần tiến hành thêm Trait Laravel\Passport\HasApiTokens vào model bạn sử dụng cho Guard của mình (mình mặc định nó là App\User). Trait này sẽ cung cấp một số phương thức cho việc xác thực Token và Scopes của người dùng.

<?phpnamespace App;

use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}

Tiếp đến với AuthServiceProvider, chúng ta cần gọi phương thức Passport::routes() trong hàm boot(). Nó sẽ đăng ký danh sách các Route phục vụ cho quá trình phân phối và thu hồi Token và Client được đăng ký. Chúng ta có thể xem danh sách Route được cập nhật bằng lệnh artisan route:list

<?php

namespace App\Providers;

use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];

/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();

Passport::routes();
}
}

Bước cài đặt cuối cùng - config/auth.php bằng cách đổi driver của Guard API sang passport - một cách “dạy dỗ” ứng dụng sẽ sử dụng Passport’s TokenGuard khi xác thực request gửi về phía API

'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],

'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],

Nói qua về Scope, giống như việc bạn làm chức năng phân quyền cho tài khoản Admin bằng cách truyền thống vậy. Scope sẽ thay ta định danh xem người dùng sẽ được truy cập vào API nào chứa tên Scope được lưu trong Token được cấp phép. Rất tiện lợi và bám sát tính năng OAuth2 dùng để khắc phục điểm yếu của phiên bản đầu tiên. Bắt đầu với việc liệt kê danh sách Scope được phân phối trên hệ thống bằng cách quay lại với hàm boot() trong AuthServiceProvider và thêm phương thức tokensCan với mảng truyền vào định danh tên Scope và mô tả của Scope đó

/**
AuthServiceProvider
*/
use Laravel\Passport\Passport;public function boot()
{
Passport::tokensCan([
'place-orders' => 'Place orders',
'check-status' => 'Check order status',
]);
}

Passport cung cấp 2 middleware được sử dụng để xác thực các request truyền lên khi đi kèm với các token đã được xác thực với danh sách Scope chứa bên trong nó, chỉ bằng cách thêm chúng vào $routeMiddleware trong file app\Http\Kernel.php:

'scopes' => \Laravel\Passport\Http\Middleware\CheckScopes::class,
'scope' => \Laravel\Passport\Http\Middleware\CheckForAnyScope::class

Trong đó:

  • scopes: đảm bảo các request phải có đủ tất cả các Scope mà nó chỉ định
Route::get('/orders', function () {
// Access token has both "check-status" and "place-orders" scopes
})->middleware('scopes:check-status,place-orders');
  • scope: đảm bảo các request có ít nhất một Scope mà nó chỉ định
Route::get('/orders', function () {
// Access token has either "check-status" or "place-orders" scope
})->middleware('scope:check-status,place-orders');

Để tiến hành xin mã Token, ta sẽ gửi một request với phương thức POST lên đường dẫn mặc định oauth/token, với các tham số:

  • grant_type: phương thức xác thực, ở đây là password
  • client_id: mã định danh ứng dụng được cấp ở bước thiết lập
  • client_secret: chuỗi ký tự bí mật định danh ứng dụng được cấp ở bước thiết lập
  • username: mặc định Passport hiểu tham số này là Email (do sử dụng model User mặc định) đăng nhập của người dùng
  • password: mật khẩu của tài khoản Email bên trên
  • scope: nó đây, danh sách các scope được cấp cho token người dùng được ngăn cách bằng “space” (điền * nếu ta yêu cầu toàn bộ scope và để trống nếu không yêu cầu bất kỳ scope nào)

Một ví dụ một request khi ta sử dụng Package GuzzleHttp\Client của Laravel:

$http = new GuzzleHttp\Client;

$response = $http->post('http://your-app.com/oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => '',
],
]);

return json_decode((string) $response->getBody(), true);

Như vậy, với yêu cầu của bài toán chúng ta, khi người dùng đăng nhập bằng Form được cung cấp trên ứng dụng của chúng ta, chúng ta sẽ tiến hành gửi kèm Request trên để lấy ra Access Token với Scope mà chúng ta muốn chỉ định cho tài khoản để phân định ai là User với Customer Support (chúng ta có thể xử lý đơn giản bằng cách truyền đi một tham số phụ hoặc kiểm tra bằng 1 trường Role trong Database và xử lý lại logic bên phía Server… thoải mái biến hóa)

Access Token sẽ trả về nếu Request của chúng ta và thông tin tài khoản người dùng nhập thật chính xác, lưu nó vào Local Storage, Session, Cookie tùy vào nhu cầu sử dụng của bạn. Và từ nay về sau, mỗi lần ta truyền một Request API, ta cần thêm tham số Authorization đi kèm với Access Token được cấp dưới dạng Bearer:

"Authorization: Bearer ACCESS_TOKEN"

Nếu bạn là người quản trị, bạn có thể xem cấu trúc Access Token được lưu trong bảng oauth_access_tokens với các tham số user_idscope để biết được quá trình tạo Access Token trong quá trình phát triển ứng dụng có đúng không nhé.

5. KẾT LUẬN

  • Bài viết này mang tính chất giúp các bạn biết và hiểu về OAuth2: từ khái niệm, đối tượng tham gia, luồng hoạt động. Nội dung còn nặng về lý thuyết bởi trong quá trình triển khai, chúng ta sẽ còn gặp rất nhiều vấn đề khác cần phải xử lý
  • Ví dụ về Scope cho các bạn có một cách tiếp cận khác trong việc phân quyền sử dụng cho người dùng. Theo mình, trong quá trình phát triển thì vấn đề đau đầu nhất là về mặt bảo mật và cách thức đảm bảo tính đúng đắn của Access Token bên phía Client. Với Laravel cũng như Passport nói riêng, thư viện được xây dựng với concept cố định nên với các yêu cầu đặc thù đòi hỏi ta phải xử dụng nhiều Trick và cái đầu lạnh từ phía người lập trình :D

--

--