Search code examples
javascriptnestjstelegramtelegram-botnest

How to properly verify data in Telegram's Web App?


I'm trying to implement data validation for a Telegram Web App in my NestJS application. The keys don't match for me at all. I can't figure out if it's due to the use of a work VPN (I can't check without it. The form's functionality that opens is tied to the VPN) or because of an error in my code. I'd appreciate any advice! Below is the code on how I perform the check.

async authorizationTest(@Body() authorizationData: AuthorizeUserDto) {
    console.log("Body received from Telegram", authorizationData);

    const botToken = this.configService.get("TELEGRAM_BOT_TOKEN");
    const data = querystring.parse(authorizationData.telegramId);
    console.log("Data before hash", data);
    const hash = data.hash as string;
    delete data.hash; // Remove hash from object to build the verification string

    // 2. Building the verification string.
    const dataCheckString = Object.keys(data)
      .sort()
      .map((key) => `${key}=${data[key]}`)
      .join("\n");

    console.log("dataCheckString", dataCheckString);

    // 3. Calculating the secret key.
    const secretKey = crypto
      .createHmac("sha256", botToken)
      .update("WebAppData")
      .digest();

    console.log("secretKey", secretKey);

    // 4. Computing HMAC for the verification string.
    const computedHash = crypto
      .createHmac("sha256", secretKey)
      .update(dataCheckString)
      .digest("hex");

    console.log("computedHash", computedHash);
    // 5. Comparing the computed HMAC with hash.
    if (computedHash !== hash) {
      console.log("Verification failed!");
    } else {
      console.log("Hooray! Verification succeeded!");
      // 6. Checking the auth_date for its validity.
      const CURRENT_UNIX_TIME = Math.floor(Date.now() / 1000);
      const TIMEOUT_SECONDS = 3600; // Approximately 1 hour
      if (CURRENT_UNIX_TIME - Number(data.auth_date) > TIMEOUT_SECONDS) {
        console.log("Verification failed due to timeout");
      }

      // If all checks are successful, return true.
      console.log("Verification successful");
    }
}

Solution

  • According to the docs, the secret key is a HMAC-SHA-256 signature of the bot token with the literal "WebAppData" being the key, but you are doing exactly the opposite and calculating the secret key having the bot token as the key and the literal "WebAppData" as the payload.

    This means everything should be fine if you calculate the secret key as follows:

    const secretKey = crypto
      .createHmac("sha256", "WebAppData")
      .update(botToken)
      .digest();
    

    You can also use the @twa.js collection of libraries that have already implemented this and other Web App logics for you.