Authentification Telegram WebApp sur JavaScript

Dec 09 2022
Aujourd'hui, je vais parler du mécanisme délicat d'authentification dans Telegram WebApp Bot. Qu'est-ce que le bot Telegram WebApp ? C'est juste une possibilité d'exécuter WebView avec votre site Web dans Telegram.

Aujourd'hui, je vais parler du mécanisme délicat d'authentification dans Telegram WebApp Bot. Qu'est-ce que le bot Telegram WebApp ? C'est juste une possibilité d'exécuter WebView avec votre site Web dans Telegram. Vous pouvez en savoir plus ici .

Pourquoi avons-nous besoin d'authentification ici ?

Votre site Web est censé être ouvert uniquement dans Telegram WebView. Alors, que se passerait-il si quelqu'un faisait cela dans un navigateur ? Les "pirates" peuvent utiliser les données, les identifiants, etc. de faux utilisateurs. Nous devons protéger notre API des personnes extérieures à Telegram.

Comment?

Telegram utilise HMAC (code d'authentification de message basé sur le hachage). Ainsi, si vous initialisez Telegram SDK sur votre site Web, vous pourrez utiliser ces données pour identifier l'utilisateur. Allons créer un mécanisme d'authentification étape par étape.

Étape 1 : transmettre des données via des requêtes

Telegram SDK configure les données de l'utilisateur dans la portée globale de votre application. Il existe des données sur les utilisateurs, leurs smartphones, les thèmes de couleur, etc. Vous pouvez le trouver ici:

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;

Étape 2 : création du middleware Auth

J'utilise Nest.js dans mon projet, mais la façon de créer un middleware est presque la même dans Express.js et Nest.js

Premièrement, nous devons créer le middleware avec quelques lignes de code :

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

Étape 3 : analyse de initData

Je vais décrire le processus et ensuite je vais vous montrer le code.

  1. Nous devons analyser la chaîne initData
  2. Prenez le champ de hachage de cette chaîne et conservez-le pour le futur
  3. Trier le reste des champs par ordre alphabétique
  4. Joignez ces champs à l'aide d'un saut de ligne (\n). Pourquoi? Juste parce que ! Telegram le veut !

Regardons le code :

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

C'est le dernier chapitre de notre voyage. Nous devons analyser initData en utilisant la fonction de l'étape précédente et un peu de cryptographie.

Nous devrions suivre ce chemin:

  1. Écrire une fonction pour encoder un message en utilisant l'algorithme sh256 et une clé
  2. Analyser la chaîne à l'aide de la fonction de l'étape précédente
  3. Créez une clé secrète en codant Telegram Bot Token avec la clé "WebAppData"
  4. Créez un hachage de validation en encodant dataCheckString à partir de la racine précédente avec une clé secrète
  5. Comparez le hachage de validation avec le hachage de 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. Vous pouvez ajouter la mise en cache car la cryptographie est une chose assez compliquée pour un processeur, vous pouvez donc utiliser Redis ou même in-memory-cache pour conserver la chaîne initData comme une clé et userData JSON comme valeur par exemple
  2. Vous pouvez générer votre propre jeton JWT une fois après avoir validé initData et vous pouvez le définir dans des cookies. C'est un moyen plus puissant de créer une authentification, je pense.