Skip to content

Instantly share code, notes, and snippets.

@konstantin24121
Last active February 24, 2025 04:05
Show Gist options
  • Save konstantin24121/49da5d8023532d66cc4db1136435a885 to your computer and use it in GitHub Desktop.
Save konstantin24121/49da5d8023532d66cc4db1136435a885 to your computer and use it in GitHub Desktop.
Telegram Bot 6.0 Validating data received via the Web App node implementation
const TELEGRAM_BOT_TOKEN = '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw'; // https://core.telegram.org/bots#creating-a-new-bot
export const verifyTelegramWebAppData = async (telegramInitData: string): boolean => {
// The data is a query string, which is composed of a series of field-value pairs.
const encoded = decodeURIComponent(telegramInitData);
// HMAC-SHA-256 signature of the bot's token with the constant string WebAppData used as a key.
const secret = crypto
.createHmac('sha256', 'WebAppData')
.update(TELEGRAM_BOT_TOKEN);
// Data-check-string is a chain of all received fields'.
const arr = encoded.split('&');
const hashIndex = arr.findIndex(str => str.startsWith('hash='));
const hash = arr.splice(hashIndex)[0].split('=')[1];
// sorted alphabetically
arr.sort((a, b) => a.localeCompare(b));
// in the format key=<value> with a line feed character ('\n', 0x0A) used as separator
// e.g., 'auth_date=<auth_date>\nquery_id=<query_id>\nuser=<user>
const dataCheckString = arr.join('\n');
// The hexadecimal representation of the HMAC-SHA-256 signature of the data-check-string with the secret key
const _hash = crypto
.createHmac('sha256', secret.digest())
.update(dataCheckString)
.digest('hex');
// if hash are equal the data may be used on your server.
// Complex data types are represented as JSON-serialized objects.
return _hash === hash;
};
@brzhex
Copy link

brzhex commented May 13, 2024

Here's a variant for initDataUnsafe, which will create the right string for validation from the object and check the hash

const verifyDataIntegrity = (initDataUnsafe, hash) => {
        const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
            if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }

            if (typeof v === "string" && /(https?:\/\/[^\s]+)/.test(v)) {
                v = v.replace(/\//g, "\\/");
            }
            
            return `${k}=${v}`;
        }).join("\n");

        const secret = crypto.createHmac("sha256", "WebAppData").update(process.env.API_TOKEN ?? "");
        const calculatedHash = crypto.createHmac("sha256", secret.digest()).update(dataCheckString).digest("hex");
        
        return calculatedHash === hash;
};

Example of use

const { hash, ...rest } = window.Telegram.WebApp.initDataUnsafe;
console.log(verifyDataIntegrity(rest, hash));

@aka-nez
Copy link

aka-nez commented Jun 11, 2024

Here is an example with ruby

def validate(init_data)
      parsed_data = CGI.parse(init_data)

      # Extract the hash from the parsed data
      return false unless parsed_data['hash']
      received_hash = parsed_data.delete('hash').first

      # Create the data_check_string
      data_check_string = parsed_data.keys.sort.map do |key|
        "#{key}=#{parsed_data[key].first}"
      end.join("\n")

      # Compute the secret_key
      secret_key = OpenSSL::HMAC.digest(
        'SHA256',
        'WebAppData',
        bot_token # your bot token
      )

      # Compute the HMAC-SHA-256 of the data_check_string with the secret key
      check_hash = OpenSSL::HMAC.hexdigest(
        'SHA256',
        secret_key,
        data_check_string
      )

      # Compare the computed hash with the received hash
      check_hash == received_hash
end

@ivanstnsk
Copy link

Thank you man it works!

@Matevos7
Copy link

NestJS people

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import TelegramBot from 'node-telegram-bot-api';

function verifyInitData(telegramInitData: string, botToken: string): { isVerified: boolean, urlParams: URLSearchParams } {
  const urlParams: URLSearchParams = new URLSearchParams(telegramInitData);

  const hash = urlParams.get('hash');
  urlParams.delete('hash');
  urlParams.sort();

  let dataCheckString = '';
  for (const [key, value] of urlParams.entries()) {
    dataCheckString += `${key}=${value}\n`;
  }
  dataCheckString = dataCheckString.slice(0, -1);

  const secret = crypto.createHmac('sha256', 'WebAppData').update(botToken);
  const calculatedHash = crypto.createHmac('sha256', secret.digest()).update(dataCheckString).digest('hex');

  const isVerified = calculatedHash === hash;

  return { isVerified, urlParams };
}

@Injectable()
export class ValidateTelegramDataMiddleware implements NestMiddleware {
  use(req: Request & { user: any }, res: Response, next: NextFunction) {
    const telegramInitData = ((req.headers.initdata ?? req.query.initData ?? req.query.initdata) as string);
    const botToken = process.env.TELEGRAM_TOKEN;

    if (!telegramInitData || !botToken) {
      return res.status(400).send('Invalid request');
    }

    const { urlParams, isVerified } = verifyInitData(telegramInitData, botToken);

    if (!isVerified) {
      return res.status(403).send('Unauthorized request');
    }

    const user: TelegramBot.User = typeof urlParams.get('user') === 'string' ? JSON.parse(urlParams.get('user')) : urlParams.get('user');

    req.user = user;

    next();
  }
}

@S0mbre
Copy link

S0mbre commented Jul 8, 2024

Python impl here ))

import hmac

BOT_TOKEN = 'my-bot-token'

def hmac_256(key: str | bytes, value: str | bytes, as_hex: bool = False) -> str | bytes:
    """Makes HMAX digest of key, value as bytes or a hex string"""
    if isinstance(key, str):
        key = key.encode()
    if isinstance(value, str):
        value = value.encode()
    if as_hex: return hmac.new(key, value, 'sha256').hexdigest()
    return hmac.digest(key, value, 'sha256')

def hmac_validate(digest1: str | bytes, digest2: str | bytes) -> bool:
    """Validates a pair of HMAC hashes - must use this instead of simple == for security reasons!"""
    if type(digest1) != type(digest2): return False
    return hmac.compare_digest(digest1, digest2)

def validate_web_app(initdata: str) -> bool:
    # see https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
    their_hash = None
    vals = sorted(initdata.split('&'))
    for val in vals:
        if val.startswith('hash='):
            their_hash = val.split('=')[1].strip() or None
            vals.remove(val)
            break
    if not their_hash: return False
    initdata = '\n'.join(vals)
    secret_key = hmac_256('WebAppData', BOT_TOKEN)
    my_hash = hmac_256(secret_key, initdata, True)
    return hmac_validate(my_hash, their_hash)

@AMAT0RY
Copy link

AMAT0RY commented Jul 29, 2024

@S0mbre Thanks!!

@xiaoyugary
Copy link

Thx

@TheBlackHacker
Copy link

TheBlackHacker commented Aug 12, 2024

Python implementation
For anyone who have tried @S0mbre's solution - but NOT WORK

import hmac
import hashlib
from urllib.parse import parse_qs

TELEGRAM_BOT_TOKEN = ""

def verify_telegram_web_app_data(telegram_init_data):
    # Get hash_value from the query string
    init_data = parse_qs(telegram_init_data)
    hash_value = init_data.get('hash', [None])[0]
    data_to_check = []

    # Sort key-value pair by alphabet
    sorted_items = sorted((key, val[0]) for key, val in init_data.items() if key != 'hash')
    data_to_check = [f"{key}={value}" for key, value in sorted_items]

    # HMAC Caculation
    secret = hmac.new(b"WebAppData", TELEGRAM_BOT_TOKEN.encode(), hashlib.sha256).digest()
    _hash = hmac.new(secret, "\n".join(data_to_check).encode(), hashlib.sha256).hexdigest()
    
    return _hash == hash_value

@Pro100-Almaz
Copy link

@TheBlackHacker thanks, that one works great!

@nimaxin
Copy link

nimaxin commented Aug 15, 2024

You can use the init-data-py library for Python.
Install init-data-py library:

pip install init-data-py

This library allows you to validate, parse, create, and sign Telegram Mini App data. below is an example of how to validate the data:

from init_data_py import InitData

bot_token = "" # Bot token from which the mini app is launched
query_string = "" # window.Telrgram.WebApp.initData

init_data = InitData.parse(query_string)

init_data.validate(bot_token, lifetime=60)

@IvanAdmaers
Copy link

Thanks man

@brainstormsrl
Copy link

someone have php version?

@painkkiller
Copy link

Does anybody has got broken validation in your apps? I used this variant, but during this week it got broken.

Here's a variant for initDataUnsafe, which will create the right string for validation from the object and check the hash

const verifyDataIntegrity = (initDataUnsafe, hash) => {
        const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
            if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            return `${k}=${v}`;
        }).join("\n");

        const secret = crypto.createHmac("sha256", "WebAppData").update(process.env.API_TOKEN ?? "");
        const calculatedHash = crypto.createHmac("sha256", secret.digest()).update(dataCheckString).digest("hex");
        
        return calculatedHash === hash;
};

Example of use

const { hash, ...rest } = window.Telegram.WebApp.initDataUnsafe;
console.log(verifyDataIntegrity(rest, hash));

@stasovlas
Copy link

Does anybody has got broken validation in your apps? I used this variant, but during this week it got broken.

Here's a variant for initDataUnsafe, which will create the right string for validation from the object and check the hash

const verifyDataIntegrity = (initDataUnsafe, hash) => {
        const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
            if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            return `${k}=${v}`;
        }).join("\n");

        const secret = crypto.createHmac("sha256", "WebAppData").update(process.env.API_TOKEN ?? "");
        const calculatedHash = crypto.createHmac("sha256", secret.digest()).update(dataCheckString).digest("hex");
        
        return calculatedHash === hash;
};

Example of use

const { hash, ...rest } = window.Telegram.WebApp.initDataUnsafe;
console.log(verifyDataIntegrity(rest, hash));

its broken for me too

@painkkiller
Copy link

I had guess that initDataUnsafe has got new field(s), but so far I have no success in guessing how the hash is created

@nimaxin
Copy link

nimaxin commented Nov 17, 2024

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com.
To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

@stasovlas
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

@painkkiller
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

For me it isn't the case. I pass data as JSON in body of the POST query, and I don't see in prepared string any abnormalities.

@painkkiller
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

@nimaxin
Copy link

nimaxin commented Nov 17, 2024

@stasovlas
Copy link

stasovlas commented Nov 17, 2024

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

just replace "/" in photo_url value by "/", like:

const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
           if (typeof v === "object" && v !== null) {
                if (k === "user") {
                    v = { ...v, photo_url: v.photo_url.replace("/", "\/") };
                }

                v = JSON.stringify(v);
            }
            
            return `${k}=${v}`;
        }).join("\n");

But better do not use initDataUnsafe

@painkkiller
Copy link

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

just replace "/" in photo_url value by "/", like:

const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
           if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            if(k === "photo_url") {
                return `${k}=${v.replace("/", "\/")}`;
            }
            
            return `${k}=${v}`;
        }).join("\n");

But better do not use initDataUnsafe

So from your code looks like initDataUnsafe is flatten? I use it as it is, and in my code user is a separate object inside initDataUnsafe

@painkkiller
Copy link

allows_write_to_pm=true
auth_date=XXXXXXXXXX
first_name=Dmitry
id=XXXXXXXXXX
language_code=ru
last_name=Malugin
photo_url=https://t.me/i/userpic/320/foto.svg
signature=XXXXXXXXX
username=PainKKKiller

This is the final string I am getting to be hashed, but it isn't working

@nimaxin
Copy link

nimaxin commented Nov 17, 2024

allows_write_to_pm=true
auth_date=XXXXXXXXXX
first_name=Dmitry
id=XXXXXXXXXX
language_code=ru
last_name=Malugin
photo_url=https://t.me/i/userpic/320/foto.svg
signature=XXXXXXXXX
username=PainKKKiller

This is the final string I am getting to be hashed, but it isn't working

replace / with \/
the final photo_url should look like this:
photo_url=https:\/\/t.me\/i\/userpic\/320\/foto.svg

@stasovlas
Copy link

stasovlas commented Nov 17, 2024

       v = JSON.stringify(v);

This issue occurs because of the escape characters (e.g., backslashes) inside the initData query string. For example, the user's photo_url is a URL like this: https:\/\/domain.com. When you stringify it using the JSON.stringify method, it changes to https://domain.com. To prevent the backslashes from being removed, you need to handle this differently. I solved this problem in Python using the replace method to replace / with \/. python fix example

thank you! initDataUnsafe - is really unsafe =)

How did you solve it? Could you show the code?

just replace "/" in photo_url value by "/", like:

const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
           if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }
            
            if(k === "photo_url") {
                return `${k}=${v.replace("/", "\/")}`;
            }
            
            return `${k}=${v}`;
        }).join("\n");

But better do not use initDataUnsafe

So from your code looks like initDataUnsafe is flatten? I use it as it is, and in my code user is a separate object inside initDataUnsafe

sorry, my mistake. I update code

@painkkiller
Copy link

initDataUnsafe

@stasovlas could you show your initDataUnsafe object? I still can't make it work (((

@brzhex
Copy link

brzhex commented Nov 17, 2024

Does anybody has got broken validation in your apps? I used this variant, but during this week it got broken.

Yes, after adding the photo_url parameter to initDataUnsafe, my code stopped working correctly. Here is its updated version:

const verifyDataIntegrity = (initDataUnsafe, hash) => {
        const dataCheckString = Object.entries(initDataUnsafe).sort().map(([k, v]) => {
            if (typeof v === "object" && v !== null) {
                v = JSON.stringify(v);
            }

            if (typeof v === "string" && /(https?:\/\/[^\s]+)/.test(v)) {
                v = v.replace(/\//g, "\\/");
            }
            
            return `${k}=${v}`;
        }).join("\n");

        const secret = crypto.createHmac("sha256", "WebAppData").update(process.env.API_TOKEN ?? "");
        const calculatedHash = crypto.createHmac("sha256", secret.digest()).update(dataCheckString).digest("hex");
        
        return calculatedHash === hash;
};

Example of use

const { hash, ...rest } = window.Telegram.WebApp.initDataUnsafe;
console.log(verifyDataIntegrity(rest, hash));

@painkkiller
Copy link

Object.entries(object).sort().map(([k, v]) => {
if (typeof v === "object" && v !== null) {
v = JSON.stringify(v);
}

        if (typeof v === "string" && /(https?:\/\/[^\s]+)/.test(v)) {
            v = v.replace(/\//g, "\\/");
        }

        return `${k}=${v}`;
    }).join("\n");

Thanks a lot! It work like a charm!

@ulug-sodikov
Copy link

ulug-sodikov commented Jan 30, 2025

Actual python implementation (for django).

def index(request):
    tg_oauth_data = request.POST.dict()
    try:
        tg_oauth_data.pop('csrfmiddlewaretoken')
    except KeyError:
        return HttpResponseBadRequest()

    data_check_string = '\n'.join(
        f'{k}={v}' for k, v in sorted(tg_oauth_data.items()) 
        if k != 'hash'
    )
    tg_bot_token = os.getenv('TG_BOT_TOKEN')
    secret_key = hashlib.sha256(tg_bot_token.encode()).digest()
    generated_hash = hmac.new(
        secret_key,
        data_check_string.encode(),
        hashlib.sha256
    ).hexdigest()
    authorization_is_valid = generated_hash == tg_oauth_data.get('hash')
    # ...

@js-bot-a
Copy link

js-bot-a commented Feb 4, 2025

Could you please provide nodejs example for .login() method not WebApp.
I use this but get an error with hashes

`import express from "express";
import crypto from "crypto";
import { fileURLToPath } from "url";
import path from "path";
import { config } from "../config/config.js";

const app = express();
const PORT = config.web.port;
const BOT_TOKEN = config.bot.token;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, "public")));

/**

  • Validates the Telegram WebApp signature, ensuring proper URL escaping.

  • @param {string} botToken - The bot token

  • @param {string} telegramInitData - Query string from the request

  • @returns {boolean} - true if the signature is valid, otherwise false
    */
    function validateTelegramHash(botToken, telegramInitData) {
    try {
    const initData = new URLSearchParams(telegramInitData);
    initData.sort(); // 1️⃣ Sort parameters alphabetically

     const receivedHash = initData.get("hash");
     initData.delete("hash"); // 2️⃣ Remove hash before calculation
    
     // 3️⃣ Construct the data check string with proper escaping
     const dataToCheck = [...initData.entries()]
         .map(([key, value]) => {
             value = decodeURIComponent(value);
    
             // Escape slashes in URLs
             if (typeof value === "string" && /(https?:\/\/[^\s]+)/.test(value)) {
                 value = value.replace(/\//g, "\\/");
             }
    
             return `${key}=${value}`;
         })
         .join("\n");
    
     console.log("🔹 Data Check String:", dataToCheck);
    
     // 4️⃣ Generate secret key (SHA256 of the bot token with "WebAppData")
     const secretKey = crypto.createHmac("sha256", "WebAppData")
         .update(botToken)
         .digest();
    
     // 5️⃣ Compute the hash
     const generatedHash = crypto.createHmac("sha256", secretKey)
         .update(dataToCheck)
         .digest("hex");
    
     console.log("🔍 Generated Hash:", generatedHash);
     console.log("🔍 Received Hash:", receivedHash);
    
     return receivedHash === generatedHash; // 6️⃣ Compare hashes
    

    } catch (error) {
    console.error("❌ Error during validation:", error);
    return false;
    }
    }

// Authentication route
app.get("/auth", (req, res) => {
console.log("🔹 Incoming parameters:", req.query);

const queryString = req.url.split("?")[1] || "";
if (!req.query.hash || !req.query.auth_date) {
    console.error("❌ Error: Missing hash or auth_date parameters!");
    return res.sendFile(path.join(__dirname, "public", "error.html"));
}

if (!validateTelegramHash(BOT_TOKEN, queryString)) {
    console.error("❌ Error: Telegram signature is invalid!");
    return res.sendFile(path.join(__dirname, "public", "error.html"));
}

console.log("✅ Authentication successful:", req.query);
res.sendFile(path.join(__dirname, "public", "auth.html"));

});

// Start server
app.listen(PORT, () => {
console.log(🚀 Server is running at ${config.web.url});
});
`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment