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