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:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user