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> { async function verifyMiniAppAuth(initData: string, botToken: string): Promise<number | null> {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
try { try {
const token = authHeader.slice(7); const params = new URLSearchParams(initData);
const decoded = JSON.parse(atob(token)); const hash = params.get('hash');
if (!hash) return null;
// Check token age (24 hours) // Remove hash from params and sort
if (Date.now() - decoded.ts > 24 * 60 * 60 * 1000) { 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 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 { } catch {
return null; 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 * Get dashboard stats
*/ */