JavaScript'te Telegram WebApp kimlik doğrulaması

Dec 09 2022
Bugün Telegram WebApp Bot'taki zorlu kimlik doğrulama mekanizmasından bahsedeceğim. Telegram WebApp botu nedir? WebView'ı web sitenizle Telegram içinde çalıştırma yeteneğinden başka bir şey değildir.

Bugün Telegram WebApp Bot'taki zorlu kimlik doğrulama mekanizmasından bahsedeceğim. Telegram WebApp botu nedir? WebView'ı web sitenizle Telegram içinde çalıştırma yeteneğinden başka bir şey değildir. Daha fazlasını buradan okuyabilirsiniz .

Burada neden auth'a ihtiyacımız var?

Web sitenizin yalnızca Telegram WebView'da açılması gerekiyor. Peki ya birisi bunu bir tarayıcıda yaparsa? “Hackerlar” sahte kullanıcı verilerini, kimliklerini vb. kullanabilir. API'mizi Telegram dışındaki kişilerden korumamız gerekiyor.

Nasıl?

Telegram, HMAC (karma tabanlı mesaj kimlik doğrulama kodu) kullanır. Dolayısıyla, web sitenizde Telegram SDK'yı başlatırsanız, bu verileri kullanıcıyı tanımlamak için kullanabileceksiniz. Adım adım auth mekanizması oluşturalım.

1. Adım: verileri istekler aracılığıyla iletmek

Telegram SDK, kullanıcı verilerini uygulamanızın küresel kapsamına ayarlar. Kullanıcılar, akıllı telefonları, renk temaları ve daha fazlası hakkında veriler var. Burada bulabilirsiniz:

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;

2. Adım: Auth ara yazılımı oluşturma

Projemde Nest.js kullanıyorum, ancak ara yazılım oluşturma yöntemi Express.js ve Nest.js'de neredeyse aynı

İlk olarak, birkaç satır kodla ara yazılımı oluşturmalıyız:

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

3. Adım: initData'yı ayrıştırma

İşlemi anlatacağım ve sonra size kodu göstereceğim.

  1. initData dizesini ayrıştırmamız gerekiyor
  2. Bu dizeden karma alanını alın ve gelecek için saklayın
  3. Alanların geri kalanını alfabetik sıraya göre sıralayın
  4. Satır kesiciyi (\n) kullanarak bu alanları birleştirin. Neden? Niye? Sadece çünkü! Telegram istiyor!

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

Bu, yolculuğumuzun son bölümü. Önceki adımdaki işlevi ve biraz kriptografiyi kullanarak initData'yı ayrıştırmamız gerekiyor .

Şu yolu izlemeliyiz:

  1. sh256 algoritması ve bir anahtar kullanarak mesajı kodlamak için bir işlev yazın
  2. Önceki adımdaki işlevi kullanarak dizeyi ayrıştırın
  3. Telegram Bot Simgesini “WebAppData” anahtarıyla kodlayarak gizli bir anahtar oluşturun
  4. Önceki gövdeden dataCheckString'i gizli bir anahtarla kodlayarak bir doğrulama karması oluşturun
  5. Doğrulama karmasını initData'dan gelen hash ile karşılaştırın

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. Önbelleğe alma ekleyebilirsiniz, çünkü kriptografi bir işlemci için oldukça karmaşık bir şeydir, bu nedenle, initData dizesini bir anahtar gibi ve userData JSON'u bir değer olarak tutmak için Redis veya hatta bellek içi önbellek kullanabilirsiniz.
  2. initData'yı doğruladıktan sonra kendi JWT simgenizi bir kez oluşturabilir ve onu tanımlama bilgilerine ayarlayabilirsiniz. Kimlik doğrulama oluşturmanın daha güçlü bir yolu olduğunu düşünüyorum.