Files
household-bot/apps/bot/src/telegram-miniapp-auth.ts

92 lines
2.3 KiB
TypeScript

import { createHmac, timingSafeEqual } from 'node:crypto'
import { instantToEpochSeconds, nowInstant, type Instant } from '@household/domain'
interface TelegramUserPayload {
id: number
first_name?: string
last_name?: string
username?: string
language_code?: string
}
export interface VerifiedMiniAppUser {
id: string
firstName: string | null
lastName: string | null
username: string | null
languageCode: string | null
}
export function verifyTelegramMiniAppInitData(
initData: string,
botToken: string,
now: Instant = nowInstant(),
maxAgeSeconds = 3600
): VerifiedMiniAppUser | null {
const params = new URLSearchParams(initData)
const hash = params.get('hash')
if (!hash) {
return null
}
const authDateRaw = params.get('auth_date')
if (!authDateRaw || !/^\d+$/.test(authDateRaw)) {
return null
}
const authDateSeconds = Number(authDateRaw)
const nowSeconds = instantToEpochSeconds(now)
if (authDateSeconds > nowSeconds) {
return null
}
if (nowSeconds - authDateSeconds > maxAgeSeconds) {
return null
}
const userRaw = params.get('user')
if (!userRaw) {
return null
}
const payloadEntries = [...params.entries()]
.filter(([key]) => key !== 'hash')
.sort(([left], [right]) => left.localeCompare(right))
const dataCheckString = payloadEntries.map(([key, value]) => `${key}=${value}`).join('\n')
const secretKey = createHmac('sha256', 'WebAppData').update(botToken).digest()
const expectedHash = createHmac('sha256', secretKey).update(dataCheckString).digest('hex')
const expectedBuffer = Buffer.from(expectedHash, 'hex')
const actualBuffer = Buffer.from(hash, 'hex')
if (expectedBuffer.length !== actualBuffer.length) {
return null
}
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
return null
}
let parsedUser: TelegramUserPayload
try {
parsedUser = JSON.parse(userRaw) as TelegramUserPayload
} catch {
return null
}
if (!Number.isInteger(parsedUser.id) || parsedUser.id <= 0) {
return null
}
return {
id: parsedUser.id.toString(),
firstName: parsedUser.first_name?.trim() || null,
lastName: parsedUser.last_name?.trim() || null,
username: parsedUser.username?.trim() || null,
languageCode: parsedUser.language_code?.trim() || null
}
}