JWT to authenticate Servers API’s

Hôm nay mình muốn chia sẻ với các bạn về cấu trúc của 1 JWT token và cách sử dụng nó để xác thực các APIs phía Server.

Có ba phần chính trong JWTs như thể hiện trong hình trên.

Phần 1 HEADER

{
 "alg": "HS256",
 "typ": "JWT"
}

alg: Có hai thuật toán chính (HS256 / RS256) để tạo chữ ký vào phần 3 của JWT token. typ: Định nghĩa kiểu của token.

Khi base64UrlEncode dữ liệu (data) chúng ta sẽ có được phần đầu tiên của JWT token là đoạn chuỗi mã hóa như sau: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Phần 2 PAYLOAD

Đây là phần chủ yếu chứa dữ liệu tùy chỉnh (custom data) và một số yêu cầu khác. Chúng ta có thể sử dụng các xác nhận quyền sở hữu để xác định nhiều thứ như: thời gian tồn tại của token, thời gian tạo ra mã token, ....

{
 "email": "[email protected]",
 "xyz": "abc"
}

và chúng ta cũng sử dụng base64UrlEncode để mã hóa dữ liệu payload tạo ra phần thứ 2 của JWT token: eyJuYW1lIjoiSm9obiBEb2UiLCJhbW91bnQiOjUwMCwieHl6IjoiYWJjIn0 Bạn có thể tham khảo 1 số yêu cầu tiêu chuẩn ở đây Wikipedia

Phần 3 SIGNATURE

Nó được mã hóa bằng base64url của headerpayload và nối chúng lại với nhau. Sau đó mã hóa bằng HMAC-SHA256 cùng với 1 khóa bí mật (secret key).

key           = 'secretkey';
unsignedToken = encodeBase64Url(header) + '.' + encodeBase64Url(payload);
signature     = HMAC-SHA256(key, unsignedToken) 

Bây giờ chúng ta cùng xem cách xác thực Server APIs qua JWT token:

Theo mô hình ở trên, ta có 2 bên tham gia: 1 bên cung cấp dịch vụ (Producer) và 1 bên sử dụng dịch vụ (Consumer)

  • Producer: sẽ là nhà cung cấp các APIs sử dụng JWT token
  • Consumer: là các khách hàng (Server/Web App/Mobile App/Client) phải cung cấp được mã JWT token hợp lệ để sử dụng được APIs.

Trong xác thực server 2 server cả hai bên cần chia sẻ hợp đồng tùy chỉnh cho API cụ thể hoặc cho tất cả các API. Hợp đồng này có thể bao gồm bất kỳ điều khoản tùy chỉnh nào bạn muốn giới thiệu.

Ví dụ:

  1. Chia sẻ mã bí mật (SECRET): secret này sẽ được yêu cầu để xác minh token phía Producersecret giống nhau sẽ được sử dụng để tạo ra token ở phía Consumer tương ứng.
  2. PAYLOAD: Consumer tiến hành mã hóa tất cả dữ liệu (body, query, params,...) trong payload của mã JWT token. Producer sẽ xác minh dữ liệu là như nhau trong payload nhận được thông qua API.
  3. Get TOKEN: token co trong HEADER
Consumer
import axios from 'axios'; // npm install axios
const jwt = require('jsonwebtoken'); // npm install jsonwebtoken
const PRODUCER_URL = 'https://<BASE_URL>/v1/api/getdetails';
/************KEEP IT SAFE ******************/
// Keep it in ENV and get it like process.env.(secret|clientName) ;
const secret = 'hello-reader';
const clientName = 'consumer-1-erx97812'
/**************END****************/
const getUserDetails = (email) => {
  const params = {
     email
   };
  // Sign everything here for be body(POST, PUT etc) or params(GET, POST, etc).
  /***************TOKEN CREATION ***************/
   const token = jwt.sign({...params}, secret);
   // BODY as well: ex: jwt.sign({...params, ...body }, secret);
  /****************END**************/
  const options = {
   params,
   headers: {
     'jwt-token': token, // Setting token in header
     'jwt-consumer': clientName, // Consumer identity
   },
  };
  // Anyway you like to call external API. I prefer axios.
  const response =  axios.get(PRODUCER_URL, options);
  return response.data;
}
Producer
import _ from 'lodash'; // npm install lodash
const router = express.Router();
const jwt = require('jsonwebtoken'); // npm install jsonwebtoken
export const API_HEADERS = {
  JWT_TOKEN: 'jwt-token',
  JWT_CONSUMER: ' jwt-consumer'
};
const MESSAGES = {
 NOT_FOUND: 'Valid headers not present ',
 TOKEN_EXPIRES: 'Download token expired',
 USER_NOT_FOUND: 'User with given email doesn\'t exist',
 NOT_VALID_CLIENT: 'Not a valid client',
};
/***************SECRETS***************/
// I would prefer to keep in database if it is more than > 5. Else // keep it in environment.
const SECRETS = {
  'consumer-1-erx97812': 'secret1',
  'consumer-2-i32eecx2': 'secret2',
}
/***************END***************/
// Middleware for JWT Verifier
export const JWTVerifier = async (req, res, next) => {
  const jwtToken = req.headers[API_HEADERS.JWT_TOKEN];
  const jwtConsumer = req.headers[API_HEADERS.JWT_CONSUMER];
  const payload = {};
  if (!jwtToken || !jwtConsumer) {
    return res.status(400).json({ message: MESSAGES.NOT_FOUND });
  }
try {
    const secret = SECRETS[jwtConsumer];
    if (!secret) {
      return res.status(403).json({ message: MESSAGES.NOT_VALID_CLIENT });
    }
    _.merge(payload, req.query, req.body);
    try {
      jwt.verify(bopClientToken, secret);// Verify only token not data.
      const decoded = jwt.decode(jwtToken, { complete: true });
      // Verifying the data sent inside the token should be same as payload.
      if (!_.isEqual(decoded.payload, payload)) { 
        return res.status(403).json({ message: MESSAGES.NOT_VALID_PAYLOAD });
      }
      return next();
    } catch (err) {
      return res.status(403).json({ message: MESSAGES.NOT_FOUND });
    }
  } catch (error) {
    return next(error);
  }
};
router.get('/v1/api/getdetails', JWTVerifier, (req, res, next) {
  var customData = {
    '[email protected]': {
      name: 'tester1',
      dob: '26/07/1993'
    },
    '[email protected]': {
      name: 'tester2',
      dob: '09/09/1996'
    }
  }
  const user = customData[req.query.email];
  if(user) res.json({ user });
  res.json({ message: MESSAGES.USER_NOT_FOUND})
})

Bây giờ chúng ta hiểu JWT là gì và làm thế nào chúng ta có thể sử dụng nó để xác thực giao tiếp Server-Server của chúng ta.