JSON Web Tokens (JWT) là gì ?
Tags: jwt, statelesstoken, authentication, authorization
Published: 2019-10-31 19:11:05

 Chúng ta đều biết thủ tục chung để xác thực user trong ứng dụng của mình. Đó là phương pháp đăng ký user với thông tin cơ bản như email, password. Tại thời điểm user đăng nhập ứng dụng, hệ thống sẽ kiểm tra trùng khớp email, password đã cho với dữ liệu được lưu trước đó. Nếu phù hợp thì cho user truy cập, nếu không thì chặn user lại.

 Chúng ta cũng biết thủ tục phân quyền user trong một ứng dụng. Đó là user bình thường chỉ được truy cập tính năng cơ bản (user xài free), những chức năng nâng cao chỉ giành cho những premium user (user chấp nhận thực hiện thanh toán tiền), admin user có quyền truy cập quản lý dashboard, sale admin có thể truy cập để xem thống kê và xuất ra file excel report theo tháng/ năm, ...

 Luồng cơ bản của xác thực dựa trên token (Stateful Token):
* User nhập email/password trong form đăng nhập và gởi lên server.
* Server xác minh email/password này là chính xác sẽ tạo ra 1 unique token đồng thời lưu vào database. Sau đó server sẽ trả về cho user 1 token có chứa một số thông tin dưới dạng json như user_id, role, permission.
* Token này sẽ được lưu trữ phía user (cookie). 
* Các request APIs tiếp theo đến server sẽ phải đính kèm token này vào header hoặc body của request.Server sẽ query database để kiểm tra nếu token này hợp lệ sẽ tiếp tục xử lý request.

# Client side :
```javascript
$.ajax({
    type: 'POST',
    url: url,
    headers: {
        "Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
    }
  
}).done(function(data) { 
    //
});
```

# Server side
```php
use Closure;

class CheckUserRolePermissions
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string  $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role, $permission)
    {
        $token = $request>headers['Authorization'];
        $currentUser = User::findByToken($token);
        if($currentUser){
          // query database lấy ra những role của user này 
          if (! $currentUser->hasRole($role)) {
            // Redirect...
          }
          // query database lấy ra những permissions của user này 
          if (! $currentUser->hasPermissions($permission)) {
            // Redirect...
          }
          return $next($request);
        }
         // Redirect...
        
    }

}
```

* Khi user logout, token bị hủy ở phía user và server sẽ xoá token này ra khỏi database.
 
 Nhưng thời gian luôn trôi qua nhanh cùng với rất phương thức xác thực và phân quyền khác cũng được ra đời. Ứng dụng liên tục mở rộng thêm chức năng mới và ngày càng phình to trở thành siêu ứng dụng. Đồng thời số lượng user cũng tăng lên đột biến. Để đảm bảo trải nghiệm tốt cho user thì hệ thống buộc phải scale up theo chiều ngang (horizontal scale) hết cỡ có thể. Từ một hệ thống duy nhất có kiến trúc đơn khối (Monolithic) nên được chia ra thành những hệ thống con (vật lý) đơn nhiệm (chỉ thực hiện đúng 1 loại nhiệm vụ chức năng). Kiến trúc hệ thống cũ chuyển sang kiến trúc hướng dịch vụ (Microservices): 
 * Mỗi service (đơn nhiệm) phải tự xác thực và phân quyền cho chính mình. Nếu clone source code xác thực và phân quyền từ hệ thống cũ sang từng service để tái sử dụng lại sẽ khiến việc maintain khi có bất cứ thay đổi gì trở nên tốn thời gian và phức tạp.
 * Mỗi service (đơn nhiệm) nên chỉ thực hiện đúng 1 loại nhiệm vụ chức năng của cuộc đời nó, không nên làm mất tập trung sang những việc không phải nhiệm vụ chức năng của nó là xác thực và phân quyền.
 * Kiến trúc Microservices khiến các service cần phải communicate với nhau, các request tới các service cũng tăng lên để lấy đủ dữ liệu từ nhiều service (ví dụ : sau khi add product vào cart thì khi tạo order sẽ cần request sang customer service lấy thông tin credit của customer có còn đủ để place order hay không, có thể request sang inventory service để kiểm tra còn hàng hay không, ... ). Nếu tiếp tục sử dụng cách xác thực, phân quyền theo Stateful Token sẽ làm performance thấp đi đáng kể, low latency (do query database để kiểm tra token quá nhiều lần).
 
 JSON Web Tokens (JWT) là 1 loại Stateless token, có những ưu điểm vượt trội hơn Stateful token và thích hợp hơn khi áp dụng cho xác thực và phân quyền kiến trúc Microserives :
 * Token không lưu vào database. 
 * Không cần query database kiểm tra chi đỡ tốn thời gian và resource.
 * JWT đảm bảo xác thực, phân quyền tốt cho cả users-to-services, services-to-services.

 JWT được bảo vệ và đảm bảo tính toàn vẹn dữ liệu bằng chữ ký số (Digitally Signed). Chữ ký số (Digitally Signed) được mã hoá bằng hệ mã hoá đối xứng HMAC hoặc bất đối xứng RSA.
 * HMAC là hệ mã hoá đối xứng (Symmetric cryptography) : ví dụ 1 ổ khoá luôn có ít nhất 2 chìa khoá để backup lại phòng khi mất. Sau khi khoá cửa căn nhà lại thì ai có chìa khoá đều có thể mở cửa căn nhà ra lại. Hệ mã hoá và giải mã dùng chung 1 private key. Private key bị share ra cả client/server nên dễ bị lộ dẫn đến kém bảo mật.
 * RSA là hệ mã hoá bất đối xứng (Asymmetric cryptography): ví dụ Nhân gởi 1 bức thư tay thông qua đường bưu điện tới nhà Phong. Nhân viên bưu điện sẽ mang bức thư tay tới tận địa chỉ nhà được ghi trên thư để xác nhận và giao cho đúng người nhận là Phong. Hệ mã hoá bất đối xứng cần public key (public cho mọi người đều biết) để mã hoá, người nhận cần private key (giữ bí mật không nên để lộ) để giải mã. Hệ mã hoá bất đối xứng dùng để kiểm tra tính toàn vẹn của dữ liệu không bị thay đổi/giả mạo. Địa chỉ Phong có thể được xem là public key, và để xác nhận là Phong thì giấy chứng minh nhân dân có thể được xem là private key. Private key được lưu trên server dùng để giải mã. Bảo mật cao hơn. RSA là hệ mã hoá thích hợp dùng cho chữ ký số (Digitally Signed) do vấn đề tính bảo mật tốt hơn.

  Chữ ký số (Digitally Signed): vai trò private key và public key trong RSA bị đảo ngược. Để tạo chữ ký số thì sẽ dùng private key, người nhận sẽ dùng public key để giải mã chữ ký số đó và xác thực xem chữ ký số có toàn vẹn không bị thay đổi/ giả mạo. Làm bằng cách nào ?
  * Trong chữ chữ ký số đã mã hoá có chuỗi hash đính kèm (encrypted hash)
  * Người nhận dùng public key để giải mã, sau khi có thông tin đã giải mã thì người nhận tiến hành dùng hàm hash để tính toán lại chuỗi hash gốc (original hash), sau đó so sánh chuỗi với (encrypted hash) xem có trùng khớp nhau không để biết là dữ liệu vẫn đảm bảo tính toàn vẹn hoặc đã bị thay đổi/ giả mạo.

  3 thành phần của JWT:
 * JWT có format : xxxxx.yyyyy.zzzzz
 * Header : các thông tin về thuật toán signing, type của payload (JWT).
 * Payload : các dữ liệu thực tế trong JSON format (user info, roles, permissions)
 * Signature : chữ kí

 Xem thêm về [3 thành phần của JWT ][https://jwt.io/introduction/]

 Luồng cơ bản của xác thực dựa trên JWT (Stateless Token):
* User nhập email/password trong form đăng nhập và gởi lên server.
* Server xác minh email/password này là chính xác sẽ tạo ra 1 JWT không  lưu lại vào bất cứ đâu. Trong Payload của JWT có chưa JSON format (user info, roles, permissions). Server trả JWT này về cho user.
* Token này sẽ được lưu trữ phía user (cookie). 
* Các request APIs tiếp theo đến các services sẽ phải đính kèm token này vào header hoặc body của request. Services tiếp nhận request sẽ dùng public key (khoá công khai ) để giải mã token , kiểm tra token này hợp lệ sẽ tiếp tục xử lý request.

# Client side :
```javascript
$.ajax({
    type: 'POST',
    url: url,
    headers: {
        "Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ5b3VyLWFwaS1rZXkiLCJqdGkiOiIwLjQ3MzYyOTQ0NjIzNDU1NDA1IiwiaWF0IjoxNDQ3MjczMDk2LCJleHAiOjE0NDcyNzMxNTZ9.fQGPSV85QPhbNmuu86CIgZiluKBvZKd-NmzM6vo11DM"
    }
  
}).done(function(data) { 
    //
});
```
# Server side
```php
use Closure;

class CheckUserRolePermissions
{
    /**
     * Handle the incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string  $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role, $permission)
    {
        $token = $request>headers['Authorization'];
        $userRolePermission = JWT::decode($token); 
        // kiểm tra tính toàn vẹn dữ liệu bằng cách dùng khoá công khai giả mã, dùng hàm hash tạo ra chuổi hash từ JWT vừa nhận đồng thời kiểm tra chuổi hash sau khi giải mã có giống với chuổi hash trong JWT vừa nhận.
        // $userRolePermission : {user : object, userRoles: object[], userPermissions: object[]}
        $currentUser =  $userRolePermission->user;
        $userRoles =  $userRolePermission->userRoles;
        $userPermissions =  $userRolePermission->userPermissions;
        if($currentUser){
          // check user roles và permissions
          return $next($request);
        }
         // Redirect...
        
    }

}
```

* Khi user logout, token bị hủy ở phía user