-
-
Save konstantin24121/49da5d8023532d66cc4db1136435a885 to your computer and use it in GitHub Desktop.
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; | |
}; |
Fixed the problem with '&' not working correctly:
const verifyInitData = (telegramInitData: string): boolean => { const urlParams = 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(process.env.API_TOKEN ?? ''); const calculatedHash = crypto.createHmac('sha256', secret.digest()).update(dataCheckString).digest('hex'); return calculatedHash === hash; }
thanks. it's works
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));
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
Thank you man it works!
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();
}
}
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)
@S0mbre Thanks!!
Thx
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
@TheBlackHacker thanks, that one works great!
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)
Thanks man
someone have php version?
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));
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
I had guess that initDataUnsafe has got new field(s), but so far I have no success in guessing how the hash is created
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
v = JSON.stringify(v);
This issue occurs because of the escape characters (e.g., backslashes) inside the
initData
query string. For example, the user'sphoto_url
is a URL like this:https:\/\/domain.com
. When you stringify it using theJSON.stringify
method, it changes tohttps://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 =)
v = JSON.stringify(v);
This issue occurs because of the escape characters (e.g., backslashes) inside the
initData
query string. For example, the user'sphoto_url
is a URL like this:https:\/\/domain.com
. When you stringify it using theJSON.stringify
method, it changes tohttps://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.
v = JSON.stringify(v);
This issue occurs because of the escape characters (e.g., backslashes) inside the
initData
query string. For example, the user'sphoto_url
is a URL like this:https:\/\/domain.com
. When you stringify it using theJSON.stringify
method, it changes tohttps://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 examplethank you! initDataUnsafe - is really unsafe =)
How did you solve it? Could you show the code?
v = JSON.stringify(v);
This issue occurs because of the escape characters (e.g., backslashes) inside the
initData
query string. For example, the user'sphoto_url
is a URL like this:https:\/\/domain.com
. When you stringify it using theJSON.stringify
method, it changes tohttps://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 examplethank 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
v = JSON.stringify(v);
This issue occurs because of the escape characters (e.g., backslashes) inside the
initData
query string. For example, the user'sphoto_url
is a URL like this:https:\/\/domain.com
. When you stringify it using theJSON.stringify
method, it changes tohttps://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 examplethank 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
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
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
v = JSON.stringify(v);
This issue occurs because of the escape characters (e.g., backslashes) inside the
initData
query string. For example, the user'sphoto_url
is a URL like this:https:\/\/domain.com
. When you stringify it using theJSON.stringify
method, it changes tohttps://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 examplethank 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
initDataUnsafe
@stasovlas could you show your initDataUnsafe object? I still can't make it work (((
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));
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!
@motsgar Thanks! <3