-
-
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; | |
}; |
Thank you!
Example suggested by @TopYar wrapped in middleware for Nestjs/Fastify with nodejs native crypto
declarations.d.ts
declare module 'http' {
interface IncomingHttpHeaders {
'tg-init-data'?: string
}
}
check-init-data.middleware
import {Injectable, Logger, NestMiddleware, UnauthorizedException} from '@nestjs/common';
import {FastifyRequest, FastifyReply} from 'fastify';
import * as crypto from 'crypto';
const {
env: {
TELEGRAM_BOT_TOKEN
}
} = process;
@Injectable()
export class CheckInitDataMiddleware implements NestMiddleware {
private readonly logger: Logger = new Logger(CheckInitDataMiddleware.name);
use(req: FastifyRequest['raw'], res: FastifyReply['raw'], next: () => void) {
this.logger.log(`Checking webapp init data`);
const {headers: {['tg-init-data']: tgInitData}} = req;
const initData = new URLSearchParams(tgInitData);
const hashFromClient = initData.get('hash');
const dataToCheck: string[] = [];
initData.sort();
initData.forEach((v, k) => k !== 'hash' && dataToCheck.push(`${k}=${v}`));
const secret = crypto
.createHmac('sha256', 'WebAppData')
.update(TELEGRAM_BOT_TOKEN);
const signature = crypto
.createHmac('sha256', secret.digest())
.update(dataToCheck.join('\n'));
const referenceHash = signature.digest('hex');
if (hashFromClient === referenceHash) {
return next();
}
throw new UnauthorizedException('Invalid init data');
}
}
I love you! 💘
Note that it seems to doesn't work when the user in telegram has a special character in its firstname or lastname.
Try it on your own :
- In telegram add '&' in your firstname or lastname
- try to connect on your webapp (with this auth mechanism)
You'll see that the hash generated by this method is different from the one generated by telegram
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;
}
@motsgar Thanks! <3
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);
}
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
@Fuchsoria thank you so much!