Autenticazione Telegram WebApp su JavaScript

Dec 09 2022
Oggi parlerò del complicato meccanismo di autenticazione in Telegram WebApp Bot. Cos'è il bot di Telegram WebApp? È solo la possibilità di eseguire WebView con il tuo sito Web all'interno di Telegram.

Oggi parlerò del complicato meccanismo di autenticazione in Telegram WebApp Bot. Cos'è il bot di Telegram WebApp? È solo la possibilità di eseguire WebView con il tuo sito Web all'interno di Telegram. Puoi leggere di più qui .

Perché abbiamo bisogno dell'autenticazione qui?

Il tuo sito web dovrebbe essere aperto solo in Telegram WebView. Quindi, cosa succederebbe se qualcuno lo facesse in un browser? Gli "hacker" possono utilizzare dati utente falsi, ID e così via. Dobbiamo proteggere la nostra API da persone al di fuori di Telegram.

Come?

Telegram utilizza HMAC (codice di autenticazione dei messaggi basato su hash). Quindi, se inizializzi l' SDK di Telegram sul tuo sito web, sarai in grado di utilizzare quei dati per identificare l'utente. Andiamo a creare un meccanismo di autenticazione passo dopo passo.

Passaggio 1: passaggio dei dati tramite le richieste

L'SDK di Telegram imposta i dati dell'utente nell'ambito globale della tua app. Ci sono dati sugli utenti, i loro smartphone, temi di colore e altro ancora. Potete trovare qui:

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;

Passaggio 2: creazione del middleware di autenticazione

Uso Nest.js nel mio progetto, ma il modo per creare il middleware è quasi lo stesso in Express.js e Nest.js

Innanzitutto, dovremmo creare il middleware con poche righe di codice:

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

Passaggio 3: analisi del file initData

Descriverò il processo e poi ti mostrerò il codice.

  1. Dobbiamo analizzare la stringa initData
  2. Prendi il campo hash da quella stringa e conservalo per il futuro
  3. Ordina il resto dei campi in ordine alfabetico
  4. Unisciti a questi campi utilizzando l'interruttore di riga (\n). Come mai? Solo perché! Telegram lo vuole!

Diamo un'occhiata al codice:

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

Questo è l'ultimo capitolo del nostro viaggio. Dobbiamo analizzare initData utilizzando la funzione del passaggio precedente e un po' di crittografia.

Dovremmo seguire questo percorso:

  1. Scrivi una funzione per codificare il messaggio usando l'algoritmo sh256 e una chiave
  2. Analizza la stringa utilizzando la funzione del passaggio precedente
  3. Crea una chiave segreta codificando Telegram Bot Token con la chiave "WebAppData".
  4. Crea un hash di convalida codificando dataCheckString dalla radice precedente con una chiave segreta
  5. Confronta l'hash di convalida con l'hash di 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. Puoi aggiungere la memorizzazione nella cache perché la crittografia è una cosa piuttosto complicata per un processore, quindi puoi usare Redis o anche in-memory-cache per mantenere la stringa initData come una chiave e userData JSON come valore per esempio
  2. Puoi generare il tuo token JWT una volta dopo aver convalidato initData e puoi impostarlo nei cookie. Penso che sia un modo più potente per creare l'autenticazione.