Diil.initData is a query string:
app_id=21&auth_date=1791083707&user=%7B%22id%22%3A4%2C%22firstName%22%3A%22Anna%22%2C…%7D&hash=5f0c…
How to verify #
- Parse it. Take
hashout. - Sort the remaining fields by key and join them as
key=valuelines with\n, using the decoded values. That is thedata_check_string. secret_key = HMAC_SHA256(key = "DiilMiniApp", message = client_secret).hashmust equalhex(HMAC_SHA256(key = secret_key, message = data_check_string)). Compare in constant time.- Check that
app_idis yours and thatauth_date(Unix seconds) is recent. Reject anything older than 24 hours. Shorter is fine.
Only then trust user. Never trust Diil.initDataUnsafe (the parsed copy) without this check.
Node.js #
const crypto = require('crypto')
function verifyInitData(initData, clientSecret, appId, maxAgeSec = 86400) {
const params = new URLSearchParams(initData)
const hash = params.get('hash')
params.delete('hash')
const dataCheckString = [...params.entries()]
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([k, v]) => `${k}=${v}`).join('\n')
const secretKey = crypto.createHmac('sha256', 'DiilMiniApp').update(clientSecret).digest()
const expected = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex')
if (!hash || hash.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expected))) return null
if (params.get('app_id') !== String(appId)) return null
if (Date.now() / 1000 - Number(params.get('auth_date')) > maxAgeSec) return null
return JSON.parse(params.get('user'))
}
Python #
import hashlib, hmac, json, time
from urllib.parse import parse_qsl
def verify_init_data(init_data: str, client_secret: str, app_id: str, max_age: int = 86400):
fields = dict(parse_qsl(init_data, keep_blank_values=True))
received = fields.pop("hash", "")
check = "\n".join(f"{k}={v}" for k, v in sorted(fields.items()))
secret_key = hmac.new(b"DiilMiniApp", client_secret.encode(), hashlib.sha256).digest()
expected = hmac.new(secret_key, check.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(received, expected) or fields.get("app_id") != str(app_id):
return None
if time.time() - int(fields["auth_date"]) > max_age:
return None
return json.loads(fields["user"])
What is inside #
user contains id plus the fields of the scopes the user granted (firstName, lastName, languageCode, username, avatarUrl). Phone and email are never in launch data.