Xác thực Telegram WebApp trên JavaScript

Dec 09 2022
Hôm nay tôi sẽ nói về cơ chế xác thực phức tạp trong Telegram WebApp Bot. Bot Telegram WebApp là gì? Nó chỉ là khả năng chạy WebView với trang web của bạn bên trong Telegram.

Hôm nay tôi sẽ nói về cơ chế xác thực phức tạp trong Telegram WebApp Bot. Bot Telegram WebApp là gì? Nó chỉ là khả năng chạy WebView với trang web của bạn bên trong Telegram. Bạn có thể đọc thêm ở đây .

Tại sao chúng ta cần auth ở đây?

Trang web của bạn được cho là chỉ được mở trong Telegram WebView. Vì vậy, điều gì sẽ xảy ra nếu ai đó làm điều này trong trình duyệt? “Tin tặc” có thể sử dụng dữ liệu, id của người dùng giả mạo, v.v. Chúng tôi cần bảo vệ API của mình khỏi những người bên ngoài Telegram.

Làm sao?

Telegram sử dụng HMAC (mã xác thực tin nhắn dựa trên hàm băm). Vì vậy, nếu bạn khởi chạy Telegram SDK trên trang web của mình, bạn sẽ có thể sử dụng dữ liệu đó để nhận dạng người dùng. Hãy từng bước tạo cơ chế xác thực.

Bước 1: truyền dữ liệu qua các yêu cầu

Telegram SDK thiết lập dữ liệu của người dùng vào phạm vi toàn cầu của ứng dụng của bạn. Có dữ liệu về người dùng, điện thoại thông minh của họ, chủ đề màu sắc, v.v. Bạn có thể tìm thấy nó ở đây:

window.Telegram.WebApp

const { initData } = window.Telegram.WebApp

auth_date=<auth_date>&query_id=<query_id>&user=<user>&hash=<hash>

axios.defaults.headers.common['Telegram-Data'] = window?.Telegram?.WebApp?.initData;

Bước 2: tạo phần mềm trung gian Auth

Tôi sử dụng Nest.js trong dự án của mình, nhưng cách tạo phần mềm trung gian gần như giống nhau trong Express.js và Nest.js

Đầu tiên, chúng ta nên tạo phần mềm trung gian với một vài dòng mã:

export function telegramAuthMiddleware(req, res, next) {
    // take initData from headers
    const iniData = req.headers[
      'telegram-data'
      ];
    // use our helpers (see bellow) to validate string
    // and get user from it
    const user = checkAuthorization(iniData);

    // add uses to  the request "context" for the future
    if (user) {
      req.user = user;
      next();
    // or if the validation is failed response 401
    } else {
      res.writeHead(401, { 'content-type': 'application/json' });
      res.write('unauthorized');
      res.end();
    }
}

Bước 3: phân tích cú pháp initData

Tôi sẽ mô tả quy trình và sau đó tôi sẽ cho bạn xem mã.

  1. Chúng ta cần phân tích chuỗi initData
  2. Lấy trường băm từ chuỗi đó và giữ nó cho tương lai
  3. Sắp xếp các trường còn lại theo thứ tự bảng chữ cái
  4. Nối các trường này bằng cách sử dụng ngắt dòng (\n). Tại sao? Chỉ vì! Telegram muốn nó!

Hãy nhìn vào mã:

function parseAuthString(iniData) {
  // parse string to get params
  const searchParams = new URLSearchParams(iniData);
  
  // take the hash and remove it from params list
  const hash = searchParams.get('hash');
  searchParams.delete('hash');

  // sort params
  const restKeys = Array.from(searchParams.entries());
  restKeys.sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey));

  // and join it with \n
  const dataCheckString = restKeys.map(([n, v]) => `${n}=${v}`).join('\n');

  
  return {
    dataCheckString,
    hash,
    // get metaData from params
    metaData: {
      user: JSON.parse(searchParams.get('user')),
      auth_date: searchParams.get('auth_date'),
      query_id: searchParams.get('query_id'),
    },
  };
}

Đây là chương cuối cùng của cuộc hành trình của chúng tôi. Chúng ta cần phân tích cú pháp initData bằng hàm từ bước trước và một chút mật mã.

Chúng ta nên đi theo con đường này:

  1. Viết hàm mã hóa tin nhắn bằng thuật toán sh256 và một khóa
  2. Phân tích chuỗi bằng hàm từ bước trước
  3. Tạo khóa bí mật bằng cách mã hóa Telegram Bot Token bằng khóa “WebAppData”
  4. Tạo hàm băm xác thực bằng cách mã hóa dataCheckString từ gốc trước bằng khóa bí mật
  5. So sánh hàm băm xác thực với hàm băm từ initData

const crypto = require('crypto')

const WEB_APP_DATA_CONST = "WebAppData"
const TELEGRAM_BOT_TOKEN = "so secret token!!"

// encoding message with key
// we need two types of representation here: Buffer and Hex 
function encodeHmac(message, key, repr=undefined) {
  return crypto.createHmac('sha256', key).update(message).digest(repr);
}

function checkAuthorization(iniData){
  // parsing the iniData sting
  const authTelegramData = parseAuthString(iniData);

  // creating the secret key and keep it as a Buffer (important!)
  const secretKey = encodeHmac(
    TELEGRAM_BOT_TOKEN,
    WEB_APP_DATA_CONST,
  );

  // creating the validation key (and transform it to HEX)
  const validationKey = encodeHmac(
    authTelegramData.dataCheckString,
    secretKey,
    'hex',
  );

  // the final step - comparing and returning
  if (validationKey === authTelegramData.hash) {
    return authTelegramData.metaData.user;
  }

  return null;
}

const crypto = require('crypto')
const WEB_APP_DATA_CONST = "WebAppData"
const TELEGRAM_BOT_TOKEN = "so secret token!!"

export function telegramAuthMiddleware(req, res, next) {
  // take initData from headers
  const iniData = req.headers[
    'telegram-data'
    ];
  // use our helpers (see bellow) to validate string
  // and get user from it
  const user = checkAuthorization(iniData);

  // add uses to  the request "context" for the future
  if (user) {
    req.user = user;
    next();
    // or if the validation is failed response 401
  } else {
    res.writeHead(401, { 'content-type': 'application/json' });
    res.write('unauthorized');
    res.end();
  }
}

function parseAuthString(iniData) {
  // parse string to get params
  const searchParams = new URLSearchParams(iniData);

  // take the hash and remove it from params list
  const hash = searchParams.get('hash');
  searchParams.delete('hash');

  // sort params
  const restKeys = Array.from(searchParams.entries());
  restKeys.sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey));

  // and join it with \n
  const dataCheckString = restKeys.map(([n, v]) => `${n}=${v}`).join('\n');


  return {
    dataCheckString,
    hash,
    // get metaData from params
    metaData: {
      user: JSON.parse(searchParams.get('user')),
      auth_date: searchParams.get('auth_date'),
      query_id: searchParams.get('query_id'),
    },
  };
}


// encoding message with key
// we need two types of representation here: Buffer and Hex
function encodeHmac(message, key, repr=undefined) {
  return crypto.createHmac('sha256', key).update(message).digest(repr);
}

function checkAuthorization(iniData){
  // parsing the iniData sting
  const authTelegramData = parseAuthString(iniData);

  // creating the secret key and keep it as a Buffer (important!)
  const secretKey = encodeHmac(
    TELEGRAM_BOT_TOKEN,
    WEB_APP_DATA_CONST,
  );

  // creating the validation key (and transform it to HEX)
  const validationKey = encodeHmac(
    authTelegramData.dataCheckString,
    secretKey,
    'hex',
  );

  // the final step - comparing and returning
  if (validationKey === authTelegramData.hash) {
    return authTelegramData.metaData.user;
  }

  return null;
}

  1. Bạn có thể thêm bộ nhớ đệm vì mật mã là một thứ khá phức tạp đối với bộ xử lý, vì vậy, bạn có thể sử dụng Redis hoặc thậm chí trong bộ nhớ đệm để giữ chuỗi initData như một khóa và userData JSON làm giá trị chẳng hạn
  2. Bạn có thể tạo mã thông báo JWT của riêng mình một lần sau khi xác thực initData và bạn có thể đặt mã đó thành cookie. Tôi nghĩ đó là một cách mạnh mẽ hơn để tạo xác thực.