Otentikasi Telegram WebApp pada JavaScript

Dec 09 2022
Hari ini saya akan menceritakan tentang mekanisme otentikasi yang rumit di Telegram WebApp Bot. Apa itu bot Telegram WebApp? Itu hanya kemampuan untuk menjalankan WebView dengan situs web Anda di dalam Telegram.

Hari ini saya akan menceritakan tentang mekanisme otentikasi yang rumit di Telegram WebApp Bot. Apa itu bot Telegram WebApp? Itu hanya kemampuan untuk menjalankan WebView dengan situs web Anda di dalam Telegram. Anda dapat membaca lebih lanjut di sini .

Mengapa kita membutuhkan autentikasi di sini?

Situs web Anda seharusnya dibuka hanya di Telegram WebView. Jadi, bagaimana jika seseorang melakukan ini di browser? “Peretas” dapat menggunakan data pengguna palsu, id, dan sebagainya. Kami perlu melindungi API kami dari orang di luar Telegram.

Bagaimana?

Telegram menggunakan HMAC (kode otentikasi pesan berbasis hash). Jadi, jika Anda menginisialisasi Telegram SDK di situs web Anda, Anda akan dapat menggunakan data tersebut untuk mengidentifikasi pengguna. Mari kita buat mekanisme autentikasi langkah demi langkah.

Langkah 1: meneruskan data melalui permintaan

Telegram SDK menyiapkan data pengguna ke dalam lingkup global aplikasi Anda. Ada data tentang pengguna, ponsel cerdas mereka, tema warna, dan lainnya. Anda dapat menemukannya di sini:

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;

Langkah 2: membuat middleware Auth

Saya menggunakan Nest.js di proyek saya, tetapi cara membuat middleware hampir sama di Express.js dan Nest.js

Pertama, kita harus membuat middleware dengan beberapa baris kode:

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();
    }
}

Langkah 3: mem-parsing file initData

Saya akan menjelaskan prosesnya dan kemudian saya akan menunjukkan kodenya.

  1. Kita perlu mengurai string initData
  2. Ambil bidang hash dari string itu dan simpan untuk masa depan
  3. Urutkan bidang lainnya berdasarkan urutan abjad
  4. Gabungkan bidang ini dengan menggunakan pemecah baris (\n). Mengapa? Hanya karena! Telegram menginginkannya!

Mari kita lihat kodenya:

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'),
    },
  };
}

Ini adalah bab terakhir dari perjalanan kita. Kita perlu mengurai initData menggunakan fungsi dari langkah sebelumnya dan sedikit kriptografi.

Kita harus mengikuti jalan ini:

  1. Tulis fungsi untuk menyandikan pesan menggunakan algoritma sh256 dan kunci
  2. Parsing string menggunakan fungsi dari langkah sebelumnya
  3. Buat kunci rahasia dengan menyandikan Telegram Bot Token dengan kunci “WebAppData”.
  4. Buat hash validasi dengan menyandikan dataCheckString dari batang sebelumnya dengan kunci rahasia
  5. Bandingkan hash validasi dengan hash dari 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. Anda dapat menambahkan caching karena kriptografi adalah hal yang cukup rumit untuk sebuah prosesor, jadi, Anda dapat menggunakan Redis atau bahkan cache dalam memori untuk menyimpan string initData seperti kunci dan userData JSON sebagai nilai misalnya
  2. Anda dapat membuat token JWT Anda sendiri setelah memvalidasi initData dan Anda dapat mengaturnya menjadi cookie. Ini cara yang lebih ampuh untuk membuat autentikasi menurut saya.