Безопасность: bot-токены выделены в отдельный тип bot_login (этап 0)

- POST /api/bot/auth/login выдаёт bot_login/bot_login_refresh (audience workflow-bots, TTL 30d/90d)
- authenticateBot принимает bot_login и bot_service
- authenticateToken/authenticateFileToken отклоняют bot-токены (401) — закрыта коллизия bot.id с user.id
This commit is contained in:
2026-07-22 14:32:11 +03:00
parent 72de9a25fb
commit 1fae441a09
4 changed files with 99 additions and 18 deletions

View File

@@ -6,7 +6,7 @@ import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth
import { tenantIsolation } from "../middleware/tenant.middleware";
import { validateRequest } from "../middleware/validation.middleware";
import { botMessageSchema, conversations, conversationMessages, conversationMembers, users, bots } from "@shared/schema";
import { verifyAccessToken } from "../utils/jwt";
import { verifyBotLoginToken, verifyBotServiceToken } from "../utils/jwt";
import { sendWebhook } from "../utils/webhook";
import { eventBus } from "./shared";
import { pushTaskUpdated } from "../utils/pushTaskUpdated";
@@ -23,7 +23,7 @@ export function registerBotApiRoutes(app: import("express").Express): void {
// Middleware для аутентификации ботов
// Поддерживает два типа токенов:
// 1. Обычный токен бота (role: 'bot') - получается через /api/bot/auth/login
// 1. Токен логина бота (type: 'bot_login') - получается через /api/bot/auth/login
// 2. Сервисный токен (type: 'bot_service') - передаётся в webhook при @mention
const authenticateBot = async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res: Response, next: NextFunction) => {
try {
@@ -37,23 +37,19 @@ export function registerBotApiRoutes(app: import("express").Express): void {
const token = authHeader.substring(7);
// Попробуем сначала как обычный токен бота
// Принимаются два типа токенов:
// 1. bot_login (POST /api/bot/auth/login) — через verifyBotLoginToken
// 2. bot_service (webhook @mention) — через verifyBotServiceToken
let botId: number;
let organizationId: number;
try {
const decoded = verifyAccessToken(token);
if (decoded.appRole === 'bot') {
// Обычный токен бота (userId содержит botId)
botId = decoded.userId;
organizationId = decoded.organizationId;
} else {
throw new Error('Not a bot token');
}
const decoded = verifyBotLoginToken(token);
botId = decoded.botId;
organizationId = decoded.organizationId;
} catch {
// Попробуем как сервисный токен
try {
const { verifyBotServiceToken } = await import('../utils/jwt');
const serviceDecoded = verifyBotServiceToken(token);
botId = serviceDecoded.botId;
organizationId = serviceDecoded.organizationId;

View File

@@ -9,7 +9,7 @@ import {
createBotSchema, updateBotSchema,
botLoginSchema, mcpServerSchema,
} from "@shared/schema";
import { generateTokens } from "../utils/jwt";
import { generateBotLoginTokens } from "../utils/jwt";
import { verifyPassword } from "../utils/password";
import { authLimiter } from "./shared";
import { encrypt, decrypt } from "../crypto";
@@ -75,11 +75,9 @@ export function registerBotCrudRoutes(app: import("express").Express): void {
return res.status(401).json({ success: false, error: 'Неверный логин или пароль' });
}
const tokens = generateTokens({
userId: bot.id,
organizationId: organization.id,
appRole: 'bot'
});
// Отдельный тип токена bot_login: НЕ пользовательский JWT — бот не может
// войти как пользователь с совпадающим числовым id (закрыта коллизия id).
const tokens = generateBotLoginTokens(bot.id, organization.id);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await storage.createBotSession({