feat: support Telegram Mini App authentication

- Add verifyMiniAppAuth() for WebApp init data validation
- Support X-Telegram-Init-Data header
- Keep Bearer token support for web dashboard
- Fallback to user_id query param

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-05 19:12:02 +09:00
parent 0b24cbdad0
commit 4c1f2f3852

View File

@@ -111,29 +111,111 @@ dashboardRouter.post('/auth/telegram', async (c) => {
});
/**
* Verify Authorization header and extract user_id
* Verify Telegram Mini App init data
* https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
*/
async function verifyAuth(c: any): Promise<number | null> {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
async function verifyMiniAppAuth(initData: string, botToken: string): Promise<number | null> {
try {
const token = authHeader.slice(7);
const decoded = JSON.parse(atob(token));
const params = new URLSearchParams(initData);
const hash = params.get('hash');
if (!hash) return null;
// Check token age (24 hours)
if (Date.now() - decoded.ts > 24 * 60 * 60 * 1000) {
// Remove hash from params and sort
params.delete('hash');
const dataCheckArr: string[] = [];
params.forEach((value, key) => {
dataCheckArr.push(`${key}=${value}`);
});
dataCheckArr.sort();
const dataCheckString = dataCheckArr.join('\n');
// Calculate secret key = HMAC-SHA256("WebAppData", bot_token)
const encoder = new TextEncoder();
const webAppDataKey = await crypto.subtle.importKey(
'raw',
encoder.encode('WebAppData'),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const secretKey = await crypto.subtle.sign('HMAC', webAppDataKey, encoder.encode(botToken));
// Calculate hash = HMAC-SHA256(data_check_string, secret_key)
const key = await crypto.subtle.importKey(
'raw',
secretKey,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(dataCheckString));
const calculatedHash = Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
if (calculatedHash !== hash) {
return null;
}
return decoded.id;
// Check auth_date is not too old (24 hours for Mini Apps)
const authDate = params.get('auth_date');
if (authDate) {
const authTime = parseInt(authDate, 10) * 1000;
if (Date.now() - authTime > 24 * 60 * 60 * 1000) {
return null;
}
}
// Extract user from init data
const userStr = params.get('user');
if (userStr) {
const user = JSON.parse(decodeURIComponent(userStr));
return user.id;
}
return null;
} catch {
return null;
}
}
/**
* Verify auth from Mini App init data or query param
*/
async function verifyAuth(c: any): Promise<number | null> {
const botToken = c.env.BOT_TOKEN;
if (!botToken) return null;
// 1. Try Mini App init data header
const initData = c.req.header('X-Telegram-Init-Data');
if (initData) {
const userId = await verifyMiniAppAuth(initData, botToken);
if (userId) return userId;
}
// 2. Try Bearer token (for web dashboard)
const authHeader = c.req.header('Authorization');
if (authHeader?.startsWith('Bearer ')) {
try {
const token = authHeader.slice(7);
const decoded = JSON.parse(atob(token));
if (Date.now() - decoded.ts < 24 * 60 * 60 * 1000) {
return decoded.id;
}
} catch {
// Invalid token
}
}
// 3. Try user_id query param (for simple cases, less secure)
const userId = c.req.query('user_id');
if (userId) {
return parseInt(userId, 10);
}
return null;
}
/**
* Get dashboard stats
*/