使用Telegram 做OAuth是一件很容易的事情,毕竟整个 Telegram API 都相当开放。
需要注意的一件事情就是,我们需要验证用户的数据确实是来自于 Telegram而不是第三方伪造的。无论是在回调函数还是回调URL中,Telegram都会提供如下参数:
id, first_name, last_name, username, photo_url, auth_date, hash
根据官方文档的说法,验证数据分如下几步:
- 把这些字段按照字母顺序排序,组成一个字符串,每一个键值对之间用换行符隔开,如
'auth_date=<auth_date>\nfirst_name=<first_name>\nid=<id>\nusername=<username>'
- 计算 bot token 的 sha256
- 计算 HMAC_SHA256,明文是上面的这个字符串,密码是bot token 的sha256,然后计算这个结果的hex,比对与他提供的 hash是否相等
看起来很简单是不是,随便丢给 ChatGPT就能出结果。甚至官方还给了个PHP的代码
但是很抱歉的是,这样并不能给保证100%验证成功,甚至可能会完全无法验证。以下是我踩的两个坑
hash字段要排除
在组装字符串的时候,记得排除hash字段,要不然是不可能计算出正确的结果的。
值是空的字段要排除
在 Telegram 中,username、photo_url 等字段不是必须的。在计算时,这种类型的字段如果为空,那么不需要考虑。
可怕的是,无论是官方文档,还是PHP示例代码,都没有提到值是空的字段不参与计算的事情。😢
示例代码
Go 的示例代码
type TelegramAuthData struct { Id int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"username"` PhotoUrl string `json:"photo_url"` AuthDate int `json:"auth_date"` Hash string `json:"hash"` } func verifyTelegramAuthData(data TelegramAuthData) bool { // Extracting fields and preparing for sorting. fields := map[string]string{ "auth_date": strconv.Itoa(data.AuthDate), "first_name": data.FirstName, "last_name": data.LastName, "id": strconv.Itoa(data.Id), "photo_url": data.PhotoUrl, "username": data.Username, } // Sorting the fields to create the data-check-string. var keys []string for key := range fields { keys = append(keys, key) } sort.Strings(keys) var dataCheckString string for _, key := range keys { if fields[key] != "" { dataCheckString += fmt.Sprintf("%s=%s\n", key, fields[key]) } } dataCheckString = dataCheckString[:len(dataCheckString)-1] // Remove the last newline character // Calculate the SHA256 hash of the bot's token. hasher := sha256.New() hasher.Write([]byte(botToken)) secretKey := hasher.Sum(nil) // Compute the HMAC-SHA-256 signature. hmacHasher := hmac.New(sha256.New, secretKey) hmacHasher.Write([]byte(dataCheckString)) computedHash := hex.EncodeToString(hmacHasher.Sum(nil)) // Compare the computed HMAC with the received hash. return computedHash == data.Hash }
Python 版本
def verify_telegram_authdata(data: dict) -> bool: # Extracting fields and preparing for sorting fields = { "auth_date": data.get("auth_date"), "first_name": data.get("first_name"), "last_name": data.get("last_name"), "id": data.get("id"), "photo_url": data.get("photo_url"), "username": data.get("username"), } # Sorting the fields to create the data-check-string keys = sorted(fields) data_check_string = "\n".join(f"{key}={fields[key]}" for key in keys if fields[key]) # Calculate the SHA256 hash of the bot's token hasher = hashlib.sha256() # bot token token hasher.update("token".encode("utf-8")) secret_key = hasher.digest() # Compute the HMAC-SHA-256 signature hmac_hasher = hmac.new(secret_key, data_check_string.encode("utf-8"), hashlib.sha256) computed_hash = hmac_hasher.hexdigest() # Compare the computed HMAC with the received hash return computed_hash == data.get("hash")